ruff
893f5727 - [`flake8-type-checking`, `pyupgrade`, `ruff`] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) (#19100)

Commit
62 days ago
[`flake8-type-checking`, `pyupgrade`, `ruff`] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) (#19100) ## 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 343c422 < = help: Convert to `T | None` --- > = help: Convert to `Optional[T]` 354,356c433,440 < 201 201 | pass < 202 202 | < 203 203 | --- > 1 |+from __future__ import annotations > 1 2 | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable > 2 3 | > 3 4 | > -------------------------------------------------------------------------------- > 201 202 | pass > 202 203 | > 203 204 | 358,361c442,445 < 204 |+def f(arg: Union["int", "str"] | None = None): # RUF013 < 205 205 | pass < 206 206 | < 207 207 | --- > 205 |+def f(arg: Union["int", "str"] | None = None): # RUF013 > 205 206 | pass > 206 207 | > 207 208 | ``` </details> <details><summary>RUF013_1.py</summary> ```diff 3d2 < snapshot_kind: text 15,16c14,16 < 2 2 | < 3 3 | --- > 2 |+from __future__ import annotations > 2 3 | > 3 4 | 18,19c18,19 < 4 |+def f(arg: int | None = None): # RUF013 < 5 5 | pass --- > 5 |+def f(arg: int | None = None): # RUF013 > 5 6 | pass ``` </details> <details><summary>RUF013_3.py</summary> ```diff 3d2 < snapshot_kind: text 14,16c13,16 < 1 1 | import typing < 2 2 | < 3 3 | --- > 1 |+from __future__ import annotations > 1 2 | import typing > 2 3 | > 3 4 | 18,21c18,21 < 4 |+def f(arg: typing.List[str] | None = None): # RUF013 < 5 5 | pass < 6 6 | < 7 7 | --- > 5 |+def f(arg: typing.List[str] | None = None): # RUF013 > 5 6 | pass > 6 7 | > 7 8 | 32,34c32,39 < 19 19 | pass < 20 20 | < 21 21 | --- > 1 |+from __future__ import annotations > 1 2 | import typing > 2 3 | > 3 4 | > -------------------------------------------------------------------------------- > 19 20 | pass > 20 21 | > 21 22 | 36,39c41,44 < 22 |+def f(arg: typing.Union[int, str] | None = None): # RUF013 < 23 23 | pass < 24 24 | < 25 25 | --- > 23 |+def f(arg: typing.Union[int, str] | None = None): # RUF013 > 23 24 | pass > 24 25 | > 25 26 | 50,52c55,62 < 26 26 | # Literal < 27 27 | < 28 28 | --- > 1 |+from __future__ import annotations > 1 2 | import typing > 2 3 | > 3 4 | > -------------------------------------------------------------------------------- > 26 27 | # Literal > 27 28 | > 28 29 | 54,55c64,65 < 29 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF013 < 30 30 | pass --- > 30 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF013 > 30 31 | pass ``` </details> <details><summary>RUF013_4.py</summary> ```diff 3d2 < snapshot_kind: text 13,15c12,20 < 12 12 | def multiple_1(arg1: Optional, arg2: Optional = None): ... < 13 13 | < 14 14 | --- > 1 1 | # https://github.com/astral-sh/ruff/issues/13833 > 2 |+from __future__ import annotations > 2 3 | > 3 4 | from typing import Optional > 4 5 | > -------------------------------------------------------------------------------- > 12 13 | def multiple_1(arg1: Optional, arg2: Optional = None): ... > 13 14 | > 14 15 | 17,20c22,25 < 15 |+def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int | None = None): ... < 16 16 | < 17 17 | < 18 18 | def return_type(arg: Optional = None) -> Optional: ... --- > 16 |+def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int | None = None): ... > 16 17 | > 17 18 | > 18 19 | def return_type(arg: Optional = None) -> Optional: ... ``` </details> ## Future work This PR does not touch UP006, UP007, or UP045, which are currently coupled to FA100. If this new approach turns out well, we may eventually want to deprecate FA100 and add a `__future__` import in those rules' fixes too. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Author
Parents
Loading