Skip to content

[flake8-future-annotations] Add optional integration with TC rules (FA100) #18919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented Jun 24, 2025

Summary

This PR covers the next step in #18502 after adding an autofix in #18903: checking for opportunities to emit FA100 that would cause TC001, TC002, and TC003 to activate. The main changes here are:

  • A new lint.flake8-future-annotations.aggressive config option to enable this behavior (not sold on the name, so this is basically a placeholder)
  • A new return type from the helper method is_typing_reference to help distinguish the cases where applying FA100 would change the applicability of the TC rules
  • A slightly modified future_rewritable_type_annotation implementation to accept anything Ranged and customize the error message
    • This changed some snapshots (and the ecosystem hits), but I think using the actual text from the file instead of the resolved name makes sense.

Test Plan

New tests with both FA100 and the TC rules active.

I opted only to emit FA100 in these cases, but we should double check that applying its autofix then also triggers the TC rules after #18903 lands, maybe with a CLI test.

I also did some manual testing for duplicates with the setup below. Everything seems okay when selecting combinations of the FA and TC rules, even when backporting the fix from #18903, but maybe someone else has a better idea for a test here.

Manual testing setup
# try.py
import collections

import pandas

from . import local_module


# FA102
def func(obj: dict[str, int | None]) -> None: ...

# FA102
def func(obj: dict[str, int | None]) -> None: ...

# TC003
def func(c: collections.Counter) -> int: ...

# TC002
def func(df: pandas.DataFrame) -> int: ...

# TC001
def func(sized: local_module.Container) -> int: ...
# run.sh
set -x

cp try.py input.py

~/astral/ruff/target/debug/ruff check input.py --no-cache --select FA100 --select TC \
        --config lint.flake8-future-annotations.aggressive=true \
        --config lint.flake8-type-checking.strict=true --fix --unsafe-fixes

diff try.py input.py

Output:

Found 6 errors (6 fixed, 0 remaining).
1c1
< import collections
---
> from __future__ import annotations
3d2
< import pandas
5c4,9
< from . import local_module
---
> from typing import TYPE_CHECKING
> 
> if TYPE_CHECKING:
>     from . import local_module
>     import pandas
>     import collections

Copy link
Contributor

github-actions bot commented Jun 24, 2025

ruff-ecosystem results

Linter (stable)

ℹ️ ecosystem check detected linter changes. (+5 -5 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

apache/airflow (+5 -5 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --no-preview --select ALL

+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:58:26: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:58:26: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:59:26: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:59:26: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:69:10: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:69:10: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ providers/standard/tests/unit/standard/decorators/test_python.py:134:21: FA100 Add `from __future__ import annotations` to simplify `Union`
- providers/standard/tests/unit/standard/decorators/test_python.py:134:21: FA100 Add `from __future__ import annotations` to simplify `typing.Union`
+ providers/standard/tests/unit/standard/decorators/test_python.py:964:23: FA100 Add `from __future__ import annotations` to simplify `Union`
- providers/standard/tests/unit/standard/decorators/test_python.py:964:23: FA100 Add `from __future__ import annotations` to simplify `typing.Union`

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
FA100 10 5 5 0 0

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+5 -5 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

apache/airflow (+5 -5 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:58:26: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:58:26: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:59:26: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:59:26: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ airflow-core/src/airflow/api_fastapi/execution_api/deps.py:69:10: FA100 Add `from __future__ import annotations` to simplify `Optional`
- airflow-core/src/airflow/api_fastapi/execution_api/deps.py:69:10: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
+ providers/standard/tests/unit/standard/decorators/test_python.py:134:21: FA100 Add `from __future__ import annotations` to simplify `Union`
- providers/standard/tests/unit/standard/decorators/test_python.py:134:21: FA100 Add `from __future__ import annotations` to simplify `typing.Union`
+ providers/standard/tests/unit/standard/decorators/test_python.py:964:23: FA100 Add `from __future__ import annotations` to simplify `Union`
- providers/standard/tests/unit/standard/decorators/test_python.py:964:23: FA100 Add `from __future__ import annotations` to simplify `typing.Union`

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
FA100 10 5 5 0 0

@ntBre ntBre added configuration Related to settings and configuration rule Implementing or modifying a lint rule labels Jun 24, 2025
@ntBre ntBre marked this pull request as ready for review June 24, 2025 17:57
@ntBre ntBre requested review from MichaReiser and AlexWaygood June 24, 2025 17:57
@MichaReiser
Copy link
Member

MichaReiser commented Jun 25, 2025

I think this should be behind a configuration option, as the TC rules are very opinionated: not all users will want FA100 to also apply to these situations. Alternatively, we could add a new FA* rule that sits alongside FA100 and flags these situations -- #13273 (comment) argues in favour of doing that instead.

Can you tell me more why you opted for a configuration option over a new rule?

Edit: Maybe something to answer on the issue. I think there are at least 3 possible designs and I find it hard to think through if the approach chosen here is the most ideal without looking at the problem holsiticly: Some rules change their behavior based on a future import.

Comment on lines +74 to +76
if !reference.in_typing_only_annotation() {
return IsTypingReference::Maybe;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to check whether there is a future import, if there is, the answer here is definitely No and not Maybe. The same goes for if the target Version is 3.14 or above.

I also don't quite recall how Ruff treats PEP 695 type parameter bounds or PEP 696 type parameter defaults. They probably could relatively safely be treated as typing only references, but I'm not sure that's currently how they are treated. A similar, although weaker, argument could be made for type statements. See #16981 for further discussions.

Treating them as Maybe seems correct on the surface, but Maybe here seems to mean that the decisions will (or at least likely will) change based on a future annotation or the target python version, which is not the case for type statements or type parameter bounds/defaults. Maybe a different name would be more appropriate? Do we want a fourth value?

Also #14787 is relevant to some degree here as well. I'm also a little worried about the interaction with the quote_annotations setting, since quoting annotations is antithetical to adding a future import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look!

You might want to check whether there is a future import, if there is, the answer here is definitely No and not Maybe. The same goes for if the target Version is 3.14 or above.

I think if there's already a future import or the version is 3.14+ we will have already returned Yes a few lines above, right? That's what I was thinking while writing this at least. This logic does feel tricky, so I could certainly be wrong here.

I also don't quite recall how Ruff treats PEP 695 type parameter bounds or PEP 696 type parameter defaults. They probably could relatively safely be treated as typing only references, but I'm not sure that's currently how they are treated. A similar, although weaker, argument could be made for type statements. See #16981 for further discussions.

Treating them as Maybe seems correct on the surface, but Maybe here seems to mean that the decisions will (or at least likely will) change based on a future annotation or the target python version, which is not the case for type statements or type parameter bounds/defaults. Maybe a different name would be more appropriate? Do we want a fourth value?

Hmm, I think I see what you mean. This might just not be the best place to perform this check. The integration with some other rules looked more repetitive, but if we just re-checked the condition that was here originally (or modified slightly to fit our needs), we could avoid trying to overload the yes/no/maybe meaning.

Also #14787 is relevant to some degree here as well. I'm also a little worried about the interaction with the quote_annotations setting, since quoting annotations is antithetical to adding a future import.

Related to my first point, I think we will have already returned Yes if we're in a string annotation, but, again, I could be wrong. It sounds like I should add more test cases anyway to be sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if there's already a future import or the version is 3.14+ we will have already returned Yes a few lines above, right? That's what I was thinking while writing this at least. This logic does feel tricky, so I could certainly be wrong here.

You're probably correct for annotations, for some reason I thought in_typing_only_annotation() was less reliable than it actually is. But that's probably only because I personally would like it to apply to type statements and type parameters as well, but currently it does not.

Copy link
Contributor

@Daverball Daverball Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But either way, this function gets called for a lot more type definitions than annotations, so Maybe is too generous for generic subscripts, type aliases, casts, type parameters bounds/defaults. So you should definitely make sure the reference stems from an annotation (i.e. in_runtime_evaluated_annotation() for Maybe rather than !in_typing_only_annotation(), it's worth noting here that runtime required annotations also can't be re-written with modern typing syntax).

@ntBre
Copy link
Contributor Author

ntBre commented Jun 25, 2025

(no changes, just rebasing onto the autofix)

@ntBre
Copy link
Contributor Author

ntBre commented Jun 25, 2025

I'll put this back into draft while we discuss the design on the issue.

@ntBre ntBre marked this pull request as draft June 25, 2025 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
configuration Related to settings and configuration rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants