mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 21:28:04 +00:00
![]() ## Summary
This is a second attempt at addressing
https://github.com/astral-sh/ruff/issues/18502 instead of reusing
`FA100` (#18919).
This PR:
- adds a new `lint.allow-importing-future-annotations` option
- uses the option to add a `__future__` import when it would trigger
`TC001`, `TC002`, or `TC003`
- uses the option to add an import when it would allow unquoting more
annotations in [quoted-annotation
(UP037)](https://docs.astral.sh/ruff/rules/quoted-annotation/#quoted-annotation-up037)
- uses the option to allow the `|` union syntax before 3.10 in
[implicit-optional
(RUF013)](https://docs.astral.sh/ruff/rules/implicit-optional/#implicit-optional-ruf013)
I started adding a fix for [runtime-string-union
(TC010)](https://docs.astral.sh/ruff/rules/runtime-string-union/#runtime-string-union-tc010)
too, as mentioned in my previous
[comment](https://github.com/astral-sh/ruff/issues/18502#issuecomment-3005238092),
but some of the existing tests already imported `from __future__ import
annotations`, so I think we intentionally flag these cases for the user
to inspect. Adding the import is _a_ fix but probably not the best one.
## Test Plan
Existing `TC` tests, new copies of them with the option enabled, and new
tests based on ideas in
https://github.com/astral-sh/ruff/pull/18919#discussion_r2166292705 and
the following thread. For UP037 and RUF013, the new tests are also
copies of the existing tests, with the new option enabled. The easiest
way to review them is probably by their diffs from the existing
snapshots:
### UP037
`UP037_0.py` and `UP037_2.pyi` have no diffs. The diff for `UP037_1.py`
is below. It correctly unquotes an annotation in module scope that would
otherwise be invalid.
<details><summary>UP037_1.py</summary>
```diff
3d2
< snapshot_kind: text
23c22,42
< 12 12 |
---
> 12 12 |
>
> UP037_1.py:14:4: UP037 [*] Remove quotes from type annotation
> |
> 13 | # OK
> 14 | X: "Tuple[int, int]" = (0, 0)
> | ^^^^^^^^^^^^^^^^^ UP037
> |
> = help: Remove quotes
>
> ℹ Unsafe fix
> 1 |+from __future__ import annotations
> 1 2 | from typing import TYPE_CHECKING
> 2 3 |
> 3 4 | if TYPE_CHECKING:
> --------------------------------------------------------------------------------
> 11 12 |
> 12 13 |
> 13 14 | # OK
> 14 |-X: "Tuple[int, int]" = (0, 0)
> 15 |+X: Tuple[int, int] = (0, 0)
```
</details>
### RUF013
The diffs here are mostly just the imports because the original snaps
were on 3.13. So we're getting the same fixes now on 3.9.
<details><summary>RUF013_0.py</summary>
```diff
3d2
< snapshot_kind: text
14,16c13,20
< 17 17 | pass
< 18 18 |
< 19 19 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 17 18 | pass
> 18 19 |
> 19 20 |
18,21c22,25
< 20 |+def f(arg: int | None = None): # RUF013
< 21 21 | pass
< 22 22 |
< 23 23 |
---
> 21 |+def f(arg: int | None = None): # RUF013
> 21 22 | pass
> 22 23 |
> 23 24 |
32,34c36,43
< 21 21 | pass
< 22 22 |
< 23 23 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 21 22 | pass
> 22 23 |
> 23 24 |
36,39c45,48
< 24 |+def f(arg: str | None = None): # RUF013
< 25 25 | pass
< 26 26 |
< 27 27 |
---
> 25 |+def f(arg: str | None = None): # RUF013
> 25 26 | pass
> 26 27 |
> 27 28 |
50,52c59,66
< 25 25 | pass
< 26 26 |
< 27 27 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 25 26 | pass
> 26 27 |
> 27 28 |
54,57c68,71
< 28 |+def f(arg: Tuple[str] | None = None): # RUF013
< 29 29 | pass
< 30 30 |
< 31 31 |
---
> 29 |+def f(arg: Tuple[str] | None = None): # RUF013
> 29 30 | pass
> 30 31 |
> 31 32 |
68,70c82,89
< 55 55 | pass
< 56 56 |
< 57 57 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 55 56 | pass
> 56 57 |
> 57 58 |
72,75c91,94
< 58 |+def f(arg: Union | None = None): # RUF013
< 59 59 | pass
< 60 60 |
< 61 61 |
---
> 59 |+def f(arg: Union | None = None): # RUF013
> 59 60 | pass
> 60 61 |
> 61 62 |
86,88c105,112
< 59 59 | pass
< 60 60 |
< 61 61 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 59 60 | pass
> 60 61 |
> 61 62 |
90,93c114,117
< 62 |+def f(arg: Union[int] | None = None): # RUF013
< 63 63 | pass
< 64 64 |
< 65 65 |
---
> 63 |+def f(arg: Union[int] | None = None): # RUF013
> 63 64 | pass
> 64 65 |
> 65 66 |
104,106c128,135
< 63 63 | pass
< 64 64 |
< 65 65 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 63 64 | pass
> 64 65 |
> 65 66 |
108,111c137,140
< 66 |+def f(arg: Union[int, str] | None = None): # RUF013
< 67 67 | pass
< 68 68 |
< 69 69 |
---
> 67 |+def f(arg: Union[int, str] | None = None): # RUF013
> 67 68 | pass
> 68 69 |
> 69 70 |
122,124c151,158
< 82 82 | pass
< 83 83 |
< 84 84 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 82 83 | pass
> 83 84 |
> 84 85 |
126,129c160,163
< 85 |+def f(arg: int | float | None = None): # RUF013
< 86 86 | pass
< 87 87 |
< 88 88 |
---
> 86 |+def f(arg: int | float | None = None): # RUF013
> 86 87 | pass
> 87 88 |
> 88 89 |
140,142c174,181
< 86 86 | pass
< 87 87 |
< 88 88 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 86 87 | pass
> 87 88 |
> 88 89 |
144,147c183,186
< 89 |+def f(arg: int | float | str | bytes | None = None): # RUF013
< 90 90 | pass
< 91 91 |
< 92 92 |
---
> 90 |+def f(arg: int | float | str | bytes | None = None): # RUF013
> 90 91 | pass
> 91 92 |
> 92 93 |
158,160c197,204
< 105 105 | pass
< 106 106 |
< 107 107 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 105 106 | pass
> 106 107 |
> 107 108 |
162,165c206,209
< 108 |+def f(arg: Literal[1] | None = None): # RUF013
< 109 109 | pass
< 110 110 |
< 111 111 |
---
> 109 |+def f(arg: Literal[1] | None = None): # RUF013
> 109 110 | pass
> 110 111 |
> 111 112 |
176,178c220,227
< 109 109 | pass
< 110 110 |
< 111 111 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 109 110 | pass
> 110 111 |
> 111 112 |
180,183c229,232
< 112 |+def f(arg: Literal[1, "foo"] | None = None): # RUF013
< 113 113 | pass
< 114 114 |
< 115 115 |
---
> 113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF013
> 113 114 | pass
> 114 115 |
> 115 116 |
194,196c243,250
< 128 128 | pass
< 129 129 |
< 130 130 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 128 129 | pass
> 129 130 |
> 130 131 |
198,201c252,255
< 131 |+def f(arg: Annotated[int | None, ...] = None): # RUF013
< 132 132 | pass
< 133 133 |
< 134 134 |
---
> 132 |+def f(arg: Annotated[int | None, ...] = None): # RUF013
> 132 133 | pass
> 133 134 |
> 134 135 |
212,214c266,273
< 132 132 | pass
< 133 133 |
< 134 134 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 132 133 | pass
> 133 134 |
> 134 135 |
216,219c275,278
< 135 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF013
< 136 136 | pass
< 137 137 |
< 138 138 |
---
> 136 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF013
> 136 137 | pass
> 137 138 |
> 138 139 |
232,234c291,298
< 148 148 |
< 149 149 |
< 150 150 | def f(
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 148 149 |
> 149 150 |
> 150 151 | def f(
236,239c300,303
< 151 |+ arg1: int | None = None, # RUF013
< 152 152 | arg2: Union[int, float] = None, # RUF013
< 153 153 | arg3: Literal[1, 2, 3] = None, # RUF013
< 154 154 | ):
---
> 152 |+ arg1: int | None = None, # RUF013
> 152 153 | arg2: Union[int, float] = None, # RUF013
> 153 154 | arg3: Literal[1, 2, 3] = None, # RUF013
> 154 155 | ):
253,255c317,324
< 149 149 |
< 150 150 | def f(
< 151 151 | arg1: int = None, # RUF013
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 149 150 |
> 150 151 | def f(
> 151 152 | arg1: int = None, # RUF013
257,260c326,329
< 152 |+ arg2: Union[int, float] | None = None, # RUF013
< 153 153 | arg3: Literal[1, 2, 3] = None, # RUF013
< 154 154 | ):
< 155 155 | pass
---
> 153 |+ arg2: Union[int, float] | None = None, # RUF013
> 153 154 | arg3: Literal[1, 2, 3] = None, # RUF013
> 154 155 | ):
> 155 156 | pass
274,276c343,350
< 150 150 | def f(
< 151 151 | arg1: int = None, # RUF013
< 152 152 | arg2: Union[int, float] = None, # RUF013
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 150 151 | def f(
> 151 152 | arg1: int = None, # RUF013
> 152 153 | arg2: Union[int, float] = None, # RUF013
278,281c352,355
< 153 |+ arg3: Literal[1, 2, 3] | None = None, # RUF013
< 154 154 | ):
< 155 155 | pass
< 156 156 |
---
> 154 |+ arg3: Literal[1, 2, 3] | None = None, # RUF013
> 154 155 | ):
> 155 156 | pass
> 156 157 |
292,294c366,373
< 178 178 | pass
< 179 179 |
< 180 180 |
---
> 1 |+from __future__ import annotations
> 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2 3 |
> 3 4 |
> --------------------------------------------------------------------------------
> 178 179 | pass
> 179 180 |
> 180 181 |
296,299c375,378
< 181 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF013
< 182 182 | pass
< 183 183 |
< 184 184 |
---
> 182 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF013
> 182 183 | pass
> 183 184 |
> 184 185 |
307c386
< = help: Convert to `T | None`
---
> = help: Convert to `Optional[T]`
314c393
< 188 |+def f(arg: "int | None" = None): # RUF013
---
> 188 |+def f(arg: "Optional[int]" = None): # RUF013
325c404
< = help: Convert to `T | None`
---
> = help: Convert to `Optional[T]`
332c411
< 192 |+def f(arg: "str | None" = None): # RUF013
---
> 192 |+def f(arg: "Optional[str]" = None): # RUF013
|
||
---|---|---|
.. | ||
src | ||
Cargo.toml |