[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>
This commit is contained in:
Brent Westbrook 2025-07-16 08:50:52 -04:00 committed by GitHub
parent b8dddd514f
commit 893f5727e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2487 additions and 169 deletions

View file

@ -993,6 +993,7 @@ fn value_given_to_table_key_is_not_inline_table_2() {
- `lint.exclude`
- `lint.preview`
- `lint.typing-extensions`
- `lint.future-annotations`
For more information, try '--help'.
");
@ -5744,3 +5745,25 @@ match 42: # invalid-syntax
Ok(())
}
#[test]
fn future_annotations_preview_warning() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "lint.future-annotations = true"])
.args(["--select", "F"])
.arg("--no-preview")
.arg("-")
.pass_stdin("1"),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: The `lint.future-annotations` setting will have no effect because `preview` is disabled
",
);
}

View file

@ -0,0 +1,10 @@
from collections import Counter
from elsewhere import third_party
from . import first_party
def f(x: first_party.foo): ...
def g(x: third_party.bar): ...
def h(x: Counter): ...

View file

@ -0,0 +1,68 @@
def f():
from . import first_party
def f(x: first_party.foo): ...
# Type parameter bounds
def g():
from . import foo
class C[T: foo.Ty]: ...
def h():
from . import foo
def f[T: foo.Ty](x: T): ...
def i():
from . import foo
type Alias[T: foo.Ty] = list[T]
# Type parameter defaults
def j():
from . import foo
class C[T = foo.Ty]: ...
def k():
from . import foo
def f[T = foo.Ty](x: T): ...
def l():
from . import foo
type Alias[T = foo.Ty] = list[T]
# non-generic type alias
def m():
from . import foo
type Alias = foo.Ty
# unions
from typing import Union
def n():
from . import foo
def f(x: Union[foo.Ty, int]): ...
def g(x: foo.Ty | int): ...
# runtime and typing usage
def o():
from . import foo
def f(x: foo.Ty):
return foo.Ty()

View file

@ -0,0 +1,6 @@
from __future__ import annotations
from . import first_party
def f(x: first_party.foo): ...

View file

@ -71,7 +71,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
flake8_type_checking::helpers::is_valid_runtime_import(
binding,
&checker.semantic,
&checker.settings().flake8_type_checking,
checker.settings(),
)
})
.collect()

View file

@ -2770,11 +2770,10 @@ impl<'a> Checker<'a> {
self.semantic.restore(snapshot);
if self.semantic.in_typing_only_annotation() {
if self.is_rule_enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, annotation, range);
}
}
if self.source_type.is_stub() {
if self.is_rule_enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(

View file

@ -527,6 +527,17 @@ impl<'a> Importer<'a> {
None
}
}
/// Add a `from __future__ import annotations` import.
pub(crate) fn add_future_import(&self) -> Edit {
let import = &NameImport::ImportFrom(MemberNameImport::member(
"__future__".to_string(),
"annotations".to_string(),
));
// Note that `TextSize::default` should ensure that the import is added at the very
// beginning of the file via `Insertion::start_of_file`.
self.add_import(import, TextSize::default())
}
}
/// An edit to the top-level of a module, making it available at runtime.

View file

@ -195,3 +195,8 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19100
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View file

@ -2,8 +2,7 @@ use std::fmt;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::Expr;
use ruff_python_semantic::{MemberNameImport, NameImport};
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Fix};
@ -85,15 +84,7 @@ impl AlwaysFixableViolation for FutureRequiredTypeAnnotation {
/// FA102
pub(crate) fn future_required_type_annotation(checker: &Checker, expr: &Expr, reason: Reason) {
let mut diagnostic =
checker.report_diagnostic(FutureRequiredTypeAnnotation { reason }, expr.range());
let required_import = NameImport::ImportFrom(MemberNameImport::member(
"__future__".to_string(),
"annotations".to_string(),
));
diagnostic.set_fix(Fix::unsafe_edit(
checker
.importer()
.add_import(&required_import, TextSize::default()),
));
.report_diagnostic(FutureRequiredTypeAnnotation { reason }, expr.range())
.set_fix(Fix::unsafe_edit(checker.importer().add_future_import()));
}

View file

@ -1,12 +1,10 @@
use ruff_diagnostics::Fix;
use ruff_python_ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::{MemberNameImport, NameImport};
use ruff_text_size::Ranged;
use crate::AlwaysFixableViolation;
use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Fix};
/// ## What it does
/// Checks for missing `from __future__ import annotations` imports upon
@ -95,15 +93,7 @@ pub(crate) fn future_rewritable_type_annotation(checker: &Checker, expr: &Expr)
let Some(name) = name else { return };
let import = &NameImport::ImportFrom(MemberNameImport::member(
"__future__".to_string(),
"annotations".to_string(),
));
checker
.report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range())
.set_fix(Fix::unsafe_edit(
checker
.importer()
.add_import(import, ruff_text_size::TextSize::default()),
));
.set_fix(Fix::unsafe_edit(checker.importer().add_future_import()));
}

View file

@ -8,41 +8,110 @@ use ruff_python_ast::{self as ast, Decorator, Expr, StringLiteralFlags};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::{
Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel, analyze,
Binding, BindingKind, Modules, NodeId, ScopeKind, SemanticModel, analyze,
};
use ruff_text_size::{Ranged, TextRange};
use crate::Edit;
use crate::Locator;
use crate::rules::flake8_type_checking::settings::Settings;
use crate::settings::LinterSettings;
/// Represents the kind of an existing or potential typing-only annotation.
///
/// Note that the order of variants is important here. `Runtime` has the highest precedence when
/// calling [`TypingReference::combine`] on two references, followed by `Future`, `Quote`, and
/// `TypingOnly` with the lowest precedence.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum TypingReference {
/// The reference is in a runtime-evaluated context.
Runtime,
/// The reference is in a runtime-evaluated context, but the
/// `lint.future-annotations` setting is enabled.
///
/// This takes precedence if both quoting and future imports are enabled.
Future,
/// The reference is in a runtime-evaluated context, but the
/// `lint.flake8-type-checking.quote-annotations` setting is enabled.
Quote,
/// The reference is in a typing-only context.
TypingOnly,
}
impl TypingReference {
/// Determine the kind of [`TypingReference`] for all references to a binding.
pub(crate) fn from_references(
binding: &Binding,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> Self {
let references = binding
.references()
.map(|reference_id| semantic.reference(reference_id));
let mut kind = Self::TypingOnly;
for reference in references {
if reference.in_type_checking_block() {
kind = kind.combine(Self::TypingOnly);
continue;
}
/// Returns `true` if the [`ResolvedReference`] is in a typing-only context _or_ a runtime-evaluated
/// context (with quoting enabled).
pub(crate) fn is_typing_reference(reference: &ResolvedReference, settings: &Settings) -> bool {
reference.in_type_checking_block()
// if we're not in a type checking block, we necessarily need to be within a
// type definition to be considered a typing reference
|| (reference.in_type_definition()
&& (reference.in_typing_only_annotation()
|| reference.in_string_type_definition()
|| (settings.quote_annotations && reference.in_runtime_evaluated_annotation())))
if !reference.in_type_definition() {
return Self::Runtime;
}
if reference.in_typing_only_annotation() || reference.in_string_type_definition() {
kind = kind.combine(Self::TypingOnly);
continue;
}
// prefer `from __future__ import annotations` to quoting
if settings.future_annotations()
&& !reference.in_typing_only_annotation()
&& reference.in_runtime_evaluated_annotation()
{
kind = kind.combine(Self::Future);
continue;
}
if settings.flake8_type_checking.quote_annotations
&& reference.in_runtime_evaluated_annotation()
{
kind = kind.combine(Self::Quote);
continue;
}
return Self::Runtime;
}
kind
}
/// Logically combine two `TypingReference`s into one.
///
/// `TypingReference::Runtime` has the highest precedence, followed by
/// `TypingReference::Future`, `TypingReference::Quote`, and then `TypingReference::TypingOnly`.
fn combine(self, other: TypingReference) -> TypingReference {
self.min(other)
}
fn is_runtime(self) -> bool {
matches!(self, Self::Runtime)
}
}
/// Returns `true` if the [`Binding`] represents a runtime-required import.
pub(crate) fn is_valid_runtime_import(
binding: &Binding,
semantic: &SemanticModel,
settings: &Settings,
settings: &LinterSettings,
) -> bool {
if matches!(
binding.kind,
BindingKind::Import(..) | BindingKind::FromImport(..) | BindingKind::SubmoduleImport(..)
) {
binding.context.is_runtime()
&& binding
.references()
.map(|reference_id| semantic.reference(reference_id))
.any(|reference| !is_typing_reference(reference, settings))
&& TypingReference::from_references(binding, semantic, settings).is_runtime()
} else {
false
}

View file

@ -13,6 +13,8 @@ pub(crate) struct ImportBinding<'a> {
pub(crate) range: TextRange,
/// The range of the import's parent statement.
pub(crate) parent_range: Option<TextRange>,
/// Whether the binding needs `from __future__ import annotations` to be imported.
pub(crate) needs_future_import: bool,
}
impl Ranged for ImportBinding<'_> {

View file

@ -9,10 +9,12 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use itertools::Itertools;
use ruff_python_ast::PythonVersion;
use test_case::test_case;
use crate::registry::{Linter, Rule};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_diagnostics, settings};
@ -64,6 +66,40 @@ mod tests {
Ok(())
}
#[test_case(&[Rule::TypingOnlyFirstPartyImport], Path::new("TC001.py"))]
#[test_case(&[Rule::TypingOnlyThirdPartyImport], Path::new("TC002.py"))]
#[test_case(&[Rule::TypingOnlyStandardLibraryImport], Path::new("TC003.py"))]
#[test_case(
&[
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyThirdPartyImport,
Rule::TypingOnlyStandardLibraryImport,
],
Path::new("TC001-3_future.py")
)]
#[test_case(&[Rule::TypingOnlyFirstPartyImport], Path::new("TC001_future.py"))]
#[test_case(&[Rule::TypingOnlyFirstPartyImport], Path::new("TC001_future_present.py"))]
fn add_future_import(rules: &[Rule], path: &Path) -> Result<()> {
let name = rules.iter().map(Rule::noqa_code).join("-");
let snapshot = format!("add_future_import__{}_{}", name, path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
future_annotations: true,
preview: PreviewMode::Enabled,
// also enable quoting annotations to check the interaction. the future import
// should take precedence.
flake8_type_checking: super::settings::Settings {
quote_annotations: true,
..Default::default()
},
..settings::LinterSettings::for_rules(rules.iter().copied())
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
// we test these rules as a pair, since they're opposites of one another
// so we want to make sure their fixes are not going around in circles.
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]

View file

@ -139,6 +139,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
binding,
range: binding.range(),
parent_range: binding.parent_range(checker.semantic()),
needs_future_import: false, // TODO(brent) See #19359.
};
if checker.rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, import.start())

View file

@ -13,7 +13,7 @@ use crate::fix;
use crate::importer::ImportedMembers;
use crate::preview::is_full_path_match_source_strategy_enabled;
use crate::rules::flake8_type_checking::helpers::{
filter_contained, is_typing_reference, quote_annotation,
TypingReference, filter_contained, quote_annotation,
};
use crate::rules::flake8_type_checking::imports::ImportBinding;
use crate::rules::isort::categorize::MatchSourceStrategy;
@ -71,12 +71,19 @@ use crate::{Fix, FixAvailability, Violation};
/// the criterion for determining whether an import is first-party
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
/// - `lint.flake8-type-checking.runtime-evaluated-decorators`
/// - `lint.flake8-type-checking.strict`
/// - `lint.typing-modules`
/// - `lint.future-annotations`
///
/// ## References
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
@ -151,12 +158,19 @@ impl Violation for TypingOnlyFirstPartyImport {
/// the criterion for determining whether an import is first-party
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
/// - `lint.flake8-type-checking.runtime-evaluated-decorators`
/// - `lint.flake8-type-checking.strict`
/// - `lint.typing-modules`
/// - `lint.future-annotations`
///
/// ## References
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
@ -226,12 +240,22 @@ impl Violation for TypingOnlyThirdPartyImport {
/// return str(path)
/// ```
///
/// ## Preview
///
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, if
/// [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
/// - `lint.flake8-type-checking.runtime-evaluated-decorators`
/// - `lint.flake8-type-checking.strict`
/// - `lint.typing-modules`
/// - `lint.future-annotations`
///
/// ## References
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
@ -271,9 +295,10 @@ pub(crate) fn typing_only_runtime_import(
for binding_id in scope.binding_ids() {
let binding = checker.semantic().binding(binding_id);
// If we're in un-strict mode, don't flag typing-only imports that are
// implicitly loaded by way of a valid runtime import.
if !checker.settings().flake8_type_checking.strict
// If we can't add a `__future__` import and in un-strict mode, don't flag typing-only
// imports that are implicitly loaded by way of a valid runtime import.
if !checker.settings().future_annotations()
&& !checker.settings().flake8_type_checking.strict
&& runtime_imports
.iter()
.any(|import| is_implicit_import(binding, import))
@ -289,14 +314,21 @@ pub(crate) fn typing_only_runtime_import(
continue;
};
if binding.context.is_runtime()
&& binding
.references()
.map(|reference_id| checker.semantic().reference(reference_id))
.all(|reference| {
is_typing_reference(reference, &checker.settings().flake8_type_checking)
})
{
if !binding.context.is_runtime() {
continue;
}
let typing_reference =
TypingReference::from_references(binding, checker.semantic(), checker.settings());
let needs_future_import = match typing_reference {
TypingReference::Runtime => continue,
// We can only get the `Future` variant if `future_annotations` is
// enabled, so we can unconditionally set this here.
TypingReference::Future => true,
TypingReference::TypingOnly | TypingReference::Quote => false,
};
let qualified_name = import.qualified_name();
if is_exempt(
@ -361,6 +393,7 @@ pub(crate) fn typing_only_runtime_import(
binding,
range: binding.range(),
parent_range: binding.parent_range(checker.semantic()),
needs_future_import,
};
if checker.rule_is_ignored(rule_for(import_type), import.start())
@ -379,7 +412,6 @@ pub(crate) fn typing_only_runtime_import(
.push(import);
}
}
}
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
@ -509,6 +541,8 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
.min()
.expect("Expected at least one import");
let add_future_import = imports.iter().any(|binding| binding.needs_future_import);
// Step 1) Remove the import.
let remove_import_edit = fix::edits::remove_unused_imports(
member_names.iter().map(AsRef::as_ref),
@ -532,7 +566,21 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?
.into_edits();
// Step 3) Quote any runtime usages of the referenced symbol.
// Step 3) Either add a `__future__` import or quote any runtime usages of the referenced
// symbol.
let fix = if add_future_import {
let future_import = checker.importer().add_future_import();
// The order here is very important. We first need to add the `__future__` import, if
// needed, since it's a syntax error to come later. Then `type_checking_edit` imports
// `TYPE_CHECKING`, if available. Then we can add and/or remove existing imports.
Fix::unsafe_edits(
future_import,
std::iter::once(type_checking_edit)
.chain(add_import_edit)
.chain(std::iter::once(remove_import_edit)),
)
} else {
let quote_reference_edits = filter_contained(
imports
.iter()
@ -554,15 +602,16 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
})
.collect::<Vec<_>>(),
);
Ok(Fix::unsafe_edits(
Fix::unsafe_edits(
type_checking_edit,
add_import_edit
.into_iter()
.chain(std::iter::once(remove_import_edit))
.chain(quote_reference_edits),
)
.isolate(Checker::isolation(
};
Ok(fix.isolate(Checker::isolation(
checker.semantic().parent_statement_id(node_id),
)))
}

View file

@ -0,0 +1,76 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC001-3_future.py:1:25: TC003 [*] Move standard library import `collections.Counter` into a type-checking block
|
1 | from collections import Counter
| ^^^^^^^ TC003
2 |
3 | from elsewhere import third_party
|
= help: Move into type-checking block
Unsafe fix
1 |-from collections import Counter
1 |+from __future__ import annotations
2 2 |
3 3 | from elsewhere import third_party
4 4 |
5 5 | from . import first_party
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from collections import Counter
6 10 |
7 11 |
8 12 | def f(x: first_party.foo): ...
TC001-3_future.py:3:23: TC002 [*] Move third-party import `elsewhere.third_party` into a type-checking block
|
1 | from collections import Counter
2 |
3 | from elsewhere import third_party
| ^^^^^^^^^^^ TC002
4 |
5 | from . import first_party
|
= help: Move into type-checking block
Unsafe fix
1 |+from __future__ import annotations
1 2 | from collections import Counter
2 3 |
3 |-from elsewhere import third_party
4 4 |
5 5 | from . import first_party
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from elsewhere import third_party
6 10 |
7 11 |
8 12 | def f(x: first_party.foo): ...
TC001-3_future.py:5:15: TC001 [*] Move application import `.first_party` into a type-checking block
|
3 | from elsewhere import third_party
4 |
5 | from . import first_party
| ^^^^^^^^^^^ TC001
|
= help: Move into type-checking block
Unsafe fix
1 |+from __future__ import annotations
1 2 | from collections import Counter
2 3 |
3 4 | from elsewhere import third_party
4 5 |
5 |-from . import first_party
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from . import first_party
6 10 |
7 11 |
8 12 | def f(x: first_party.foo): ...

View file

@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC001.py:20:19: TC001 [*] Move application import `.TYP001` into a type-checking block
|
19 | def f():
20 | from . import TYP001
| ^^^^^^ TC001
21 |
22 | x: TYP001
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | For typing-only import detection tests, see `TC002.py`.
4 4 | """
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ from . import TYP001
5 9 |
6 10 |
7 11 | def f():
--------------------------------------------------------------------------------
17 21 |
18 22 |
19 23 | def f():
20 |- from . import TYP001
21 24 |
22 25 | x: TYP001
23 26 |

View file

@ -0,0 +1,56 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC001_future.py:2:19: TC001 [*] Move application import `.first_party` into a type-checking block
|
1 | def f():
2 | from . import first_party
| ^^^^^^^^^^^ TC001
3 |
4 | def f(x: first_party.foo): ...
|
= help: Move into type-checking block
Unsafe fix
1 |-def f():
1 |+from __future__ import annotations
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
2 5 | from . import first_party
6 |+def f():
3 7 |
4 8 | def f(x: first_party.foo): ...
5 9 |
TC001_future.py:57:19: TC001 [*] Move application import `.foo` into a type-checking block
|
56 | def n():
57 | from . import foo
| ^^^ TC001
58 |
59 | def f(x: Union[foo.Ty, int]): ...
|
= help: Move into type-checking block
Unsafe fix
1 |+from __future__ import annotations
1 2 | def f():
2 3 | from . import first_party
3 4 |
--------------------------------------------------------------------------------
50 51 |
51 52 |
52 53 | # unions
53 |-from typing import Union
54 |+from typing import Union, TYPE_CHECKING
54 55 |
56 |+if TYPE_CHECKING:
57 |+ from . import foo
58 |+
55 59 |
56 60 | def n():
57 |- from . import foo
58 61 |
59 62 | def f(x: Union[foo.Ty, int]): ...
60 63 | def g(x: foo.Ty | int): ...

View file

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC001_future_present.py:3:15: TC001 [*] Move application import `.first_party` into a type-checking block
|
1 | from __future__ import annotations
2 |
3 | from . import first_party
| ^^^^^^^^^^^ TC001
|
= help: Move into type-checking block
Unsafe fix
1 1 | from __future__ import annotations
2 2 |
3 |-from . import first_party
3 |+from typing import TYPE_CHECKING
4 |+
5 |+if TYPE_CHECKING:
6 |+ from . import first_party
4 7 |
5 8 |
6 9 | def f(x: first_party.foo): ...

View file

@ -0,0 +1,251 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC002.py:5:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
4 | def f():
5 | import pandas as pd # TC002
| ^^ TC002
6 |
7 | x: pd.DataFrame
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ import pandas as pd
2 6 |
3 7 |
4 8 | def f():
5 |- import pandas as pd # TC002
6 9 |
7 10 | x: pd.DataFrame
8 11 |
TC002.py:11:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
10 | def f():
11 | from pandas import DataFrame # TC002
| ^^^^^^^^^ TC002
12 |
13 | x: DataFrame
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ from pandas import DataFrame
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
8 12 |
9 13 |
10 14 | def f():
11 |- from pandas import DataFrame # TC002
12 15 |
13 16 | x: DataFrame
14 17 |
TC002.py:17:37: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
16 | def f():
17 | from pandas import DataFrame as df # TC002
| ^^ TC002
18 |
19 | x: df
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ from pandas import DataFrame as df
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
14 18 |
15 19 |
16 20 | def f():
17 |- from pandas import DataFrame as df # TC002
18 21 |
19 22 | x: df
20 23 |
TC002.py:23:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
22 | def f():
23 | import pandas as pd # TC002
| ^^ TC002
24 |
25 | x: pd.DataFrame = 1
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ import pandas as pd
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
20 24 |
21 25 |
22 26 | def f():
23 |- import pandas as pd # TC002
24 27 |
25 28 | x: pd.DataFrame = 1
26 29 |
TC002.py:29:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
28 | def f():
29 | from pandas import DataFrame # TC002
| ^^^^^^^^^ TC002
30 |
31 | x: DataFrame = 2
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ from pandas import DataFrame
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
26 30 |
27 31 |
28 32 | def f():
29 |- from pandas import DataFrame # TC002
30 33 |
31 34 | x: DataFrame = 2
32 35 |
TC002.py:35:37: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
34 | def f():
35 | from pandas import DataFrame as df # TC002
| ^^ TC002
36 |
37 | x: df = 3
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ from pandas import DataFrame as df
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
32 36 |
33 37 |
34 38 | def f():
35 |- from pandas import DataFrame as df # TC002
36 39 |
37 40 | x: df = 3
38 41 |
TC002.py:41:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
40 | def f():
41 | import pandas as pd # TC002
| ^^ TC002
42 |
43 | x: "pd.DataFrame" = 1
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ import pandas as pd
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
38 42 |
39 43 |
40 44 | def f():
41 |- import pandas as pd # TC002
42 45 |
43 46 | x: "pd.DataFrame" = 1
44 47 |
TC002.py:47:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
46 | def f():
47 | import pandas as pd # TC002
| ^^ TC002
48 |
49 | x = dict["pd.DataFrame", "pd.DataFrame"]
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ import pandas as pd
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
44 48 |
45 49 |
46 50 | def f():
47 |- import pandas as pd # TC002
48 51 |
49 52 | x = dict["pd.DataFrame", "pd.DataFrame"]
50 53 |
TC002.py:172:24: TC002 [*] Move third-party import `module.Member` into a type-checking block
|
170 | global Member
171 |
172 | from module import Member
| ^^^^^^ TC002
173 |
174 | x: Member = 1
|
= help: Move into type-checking block
Unsafe fix
1 1 | """Tests to determine accurate detection of typing-only imports."""
2 |+from typing import TYPE_CHECKING
3 |+
4 |+if TYPE_CHECKING:
5 |+ from module import Member
2 6 |
3 7 |
4 8 | def f():
--------------------------------------------------------------------------------
169 173 | def f():
170 174 | global Member
171 175 |
172 |- from module import Member
173 176 |
174 177 | x: Member = 1
175 178 |

View file

@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC003.py:8:12: TC003 [*] Move standard library import `os` into a type-checking block
|
7 | def f():
8 | import os
| ^^ TC003
9 |
10 | x: os
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | For typing-only import detection tests, see `TC002.py`.
4 4 | """
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ import os
5 9 |
6 10 |
7 11 | def f():
8 |- import os
9 12 |
10 13 | x: os
11 14 |

View file

@ -136,6 +136,23 @@ mod tests {
Ok(())
}
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]
fn up037_add_future_annotation(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("add_future_annotation_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
future_annotations: true,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test]
fn async_timeout_error_alias_not_applied_py310() -> Result<()> {
let diagnostics = test_path(

View file

@ -57,6 +57,22 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// bar: Bar
/// ```
///
/// ## Preview
///
/// When [preview] is enabled, if [`lint.future-annotations`] is set to `true`,
/// `from __future__ import annotations` will be added if doing so would allow an annotation to be
/// unquoted.
///
/// ## Fix safety
///
/// The rule's fix is marked as safe, unless [preview] and
/// [`lint.future_annotations`] are enabled and a `from __future__ import
/// annotations` import is added. Such an import may change the behavior of all annotations in the
/// file.
///
/// ## Options
/// - `lint.future-annotations`
///
/// ## See also
/// - [`quoted-annotation-in-stub`][PYI020]: A rule that
/// removes all quoted annotations from stub files
@ -69,6 +85,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
///
/// [PYI020]: https://docs.astral.sh/ruff/rules/quoted-annotation-in-stub/
/// [TC008]: https://docs.astral.sh/ruff/rules/quoted-type-alias/
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct QuotedAnnotation;
@ -85,6 +102,13 @@ impl AlwaysFixableViolation for QuotedAnnotation {
/// UP037
pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: TextRange) {
let add_future_import = checker.settings().future_annotations()
&& checker.semantic().in_runtime_evaluated_annotation();
if !(checker.semantic().in_typing_only_annotation() || add_future_import) {
return;
}
let placeholder_range = TextRange::up_to(annotation.text_len());
let spans_multiple_lines = annotation.contains_line_break(placeholder_range);
@ -103,8 +127,14 @@ pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: Text
(true, false) => format!("({annotation})"),
(_, true) => format!("({annotation}\n)"),
};
let edit = Edit::range_replacement(new_content, range);
let fix = Fix::safe_edit(edit);
let unquote_edit = Edit::range_replacement(new_content, range);
let fix = if add_future_import {
let import_edit = checker.importer().add_future_import();
Fix::unsafe_edits(unquote_edit, [import_edit])
} else {
Fix::safe_edit(unquote_edit)
};
checker
.report_diagnostic(QuotedAnnotation, range)

View file

@ -0,0 +1,625 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037_0.py:18:14: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
| ^^^^^^^^^ UP037
19 | x: "MyClass"
|
= help: Remove quotes
Safe fix
15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg
16 16 |
17 17 |
18 |-def foo(var: "MyClass") -> "MyClass":
18 |+def foo(var: MyClass) -> "MyClass":
19 19 | x: "MyClass"
20 20 |
21 21 |
UP037_0.py:18:28: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
| ^^^^^^^^^ UP037
19 | x: "MyClass"
|
= help: Remove quotes
Safe fix
15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg
16 16 |
17 17 |
18 |-def foo(var: "MyClass") -> "MyClass":
18 |+def foo(var: "MyClass") -> MyClass:
19 19 | x: "MyClass"
20 20 |
21 21 |
UP037_0.py:19:8: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
19 | x: "MyClass"
| ^^^^^^^^^ UP037
|
= help: Remove quotes
Safe fix
16 16 |
17 17 |
18 18 | def foo(var: "MyClass") -> "MyClass":
19 |- x: "MyClass"
19 |+ x: MyClass
20 20 |
21 21 |
22 22 | def foo(*, inplace: "bool"):
UP037_0.py:22:21: UP037 [*] Remove quotes from type annotation
|
22 | def foo(*, inplace: "bool"):
| ^^^^^^ UP037
23 | pass
|
= help: Remove quotes
Safe fix
19 19 | x: "MyClass"
20 20 |
21 21 |
22 |-def foo(*, inplace: "bool"):
22 |+def foo(*, inplace: bool):
23 23 | pass
24 24 |
25 25 |
UP037_0.py:26:16: UP037 [*] Remove quotes from type annotation
|
26 | def foo(*args: "str", **kwargs: "int"):
| ^^^^^ UP037
27 | pass
|
= help: Remove quotes
Safe fix
23 23 | pass
24 24 |
25 25 |
26 |-def foo(*args: "str", **kwargs: "int"):
26 |+def foo(*args: str, **kwargs: "int"):
27 27 | pass
28 28 |
29 29 |
UP037_0.py:26:33: UP037 [*] Remove quotes from type annotation
|
26 | def foo(*args: "str", **kwargs: "int"):
| ^^^^^ UP037
27 | pass
|
= help: Remove quotes
Safe fix
23 23 | pass
24 24 |
25 25 |
26 |-def foo(*args: "str", **kwargs: "int"):
26 |+def foo(*args: "str", **kwargs: int):
27 27 | pass
28 28 |
29 29 |
UP037_0.py:30:10: UP037 [*] Remove quotes from type annotation
|
30 | x: Tuple["MyClass"]
| ^^^^^^^^^ UP037
31 |
32 | x: Callable[["MyClass"], None]
|
= help: Remove quotes
Safe fix
27 27 | pass
28 28 |
29 29 |
30 |-x: Tuple["MyClass"]
30 |+x: Tuple[MyClass]
31 31 |
32 32 | x: Callable[["MyClass"], None]
33 33 |
UP037_0.py:32:14: UP037 [*] Remove quotes from type annotation
|
30 | x: Tuple["MyClass"]
31 |
32 | x: Callable[["MyClass"], None]
| ^^^^^^^^^ UP037
|
= help: Remove quotes
Safe fix
29 29 |
30 30 | x: Tuple["MyClass"]
31 31 |
32 |-x: Callable[["MyClass"], None]
32 |+x: Callable[[MyClass], None]
33 33 |
34 34 |
35 35 | class Foo(NamedTuple):
UP037_0.py:36:8: UP037 [*] Remove quotes from type annotation
|
35 | class Foo(NamedTuple):
36 | x: "MyClass"
| ^^^^^^^^^ UP037
|
= help: Remove quotes
Safe fix
33 33 |
34 34 |
35 35 | class Foo(NamedTuple):
36 |- x: "MyClass"
36 |+ x: MyClass
37 37 |
38 38 |
39 39 | class D(TypedDict):
UP037_0.py:40:27: UP037 [*] Remove quotes from type annotation
|
39 | class D(TypedDict):
40 | E: TypedDict("E", foo="int", total=False)
| ^^^^^ UP037
|
= help: Remove quotes
Safe fix
37 37 |
38 38 |
39 39 | class D(TypedDict):
40 |- E: TypedDict("E", foo="int", total=False)
40 |+ E: TypedDict("E", foo=int, total=False)
41 41 |
42 42 |
43 43 | class D(TypedDict):
UP037_0.py:44:31: UP037 [*] Remove quotes from type annotation
|
43 | class D(TypedDict):
44 | E: TypedDict("E", {"foo": "int"})
| ^^^^^ UP037
|
= help: Remove quotes
Safe fix
41 41 |
42 42 |
43 43 | class D(TypedDict):
44 |- E: TypedDict("E", {"foo": "int"})
44 |+ E: TypedDict("E", {"foo": int})
45 45 |
46 46 |
47 47 | x: Annotated["str", "metadata"]
UP037_0.py:47:14: UP037 [*] Remove quotes from type annotation
|
47 | x: Annotated["str", "metadata"]
| ^^^^^ UP037
48 |
49 | x: Arg("str", "name")
|
= help: Remove quotes
Safe fix
44 44 | E: TypedDict("E", {"foo": "int"})
45 45 |
46 46 |
47 |-x: Annotated["str", "metadata"]
47 |+x: Annotated[str, "metadata"]
48 48 |
49 49 | x: Arg("str", "name")
50 50 |
UP037_0.py:49:8: UP037 [*] Remove quotes from type annotation
|
47 | x: Annotated["str", "metadata"]
48 |
49 | x: Arg("str", "name")
| ^^^^^ UP037
50 |
51 | x: DefaultArg("str", "name")
|
= help: Remove quotes
Safe fix
46 46 |
47 47 | x: Annotated["str", "metadata"]
48 48 |
49 |-x: Arg("str", "name")
49 |+x: Arg(str, "name")
50 50 |
51 51 | x: DefaultArg("str", "name")
52 52 |
UP037_0.py:51:15: UP037 [*] Remove quotes from type annotation
|
49 | x: Arg("str", "name")
50 |
51 | x: DefaultArg("str", "name")
| ^^^^^ UP037
52 |
53 | x: NamedArg("str", "name")
|
= help: Remove quotes
Safe fix
48 48 |
49 49 | x: Arg("str", "name")
50 50 |
51 |-x: DefaultArg("str", "name")
51 |+x: DefaultArg(str, "name")
52 52 |
53 53 | x: NamedArg("str", "name")
54 54 |
UP037_0.py:53:13: UP037 [*] Remove quotes from type annotation
|
51 | x: DefaultArg("str", "name")
52 |
53 | x: NamedArg("str", "name")
| ^^^^^ UP037
54 |
55 | x: DefaultNamedArg("str", "name")
|
= help: Remove quotes
Safe fix
50 50 |
51 51 | x: DefaultArg("str", "name")
52 52 |
53 |-x: NamedArg("str", "name")
53 |+x: NamedArg(str, "name")
54 54 |
55 55 | x: DefaultNamedArg("str", "name")
56 56 |
UP037_0.py:55:20: UP037 [*] Remove quotes from type annotation
|
53 | x: NamedArg("str", "name")
54 |
55 | x: DefaultNamedArg("str", "name")
| ^^^^^ UP037
56 |
57 | x: DefaultNamedArg("str", name="name")
|
= help: Remove quotes
Safe fix
52 52 |
53 53 | x: NamedArg("str", "name")
54 54 |
55 |-x: DefaultNamedArg("str", "name")
55 |+x: DefaultNamedArg(str, "name")
56 56 |
57 57 | x: DefaultNamedArg("str", name="name")
58 58 |
UP037_0.py:57:20: UP037 [*] Remove quotes from type annotation
|
55 | x: DefaultNamedArg("str", "name")
56 |
57 | x: DefaultNamedArg("str", name="name")
| ^^^^^ UP037
58 |
59 | x: VarArg("str")
|
= help: Remove quotes
Safe fix
54 54 |
55 55 | x: DefaultNamedArg("str", "name")
56 56 |
57 |-x: DefaultNamedArg("str", name="name")
57 |+x: DefaultNamedArg(str, name="name")
58 58 |
59 59 | x: VarArg("str")
60 60 |
UP037_0.py:59:11: UP037 [*] Remove quotes from type annotation
|
57 | x: DefaultNamedArg("str", name="name")
58 |
59 | x: VarArg("str")
| ^^^^^ UP037
60 |
61 | x: List[List[List["MyClass"]]]
|
= help: Remove quotes
Safe fix
56 56 |
57 57 | x: DefaultNamedArg("str", name="name")
58 58 |
59 |-x: VarArg("str")
59 |+x: VarArg(str)
60 60 |
61 61 | x: List[List[List["MyClass"]]]
62 62 |
UP037_0.py:61:19: UP037 [*] Remove quotes from type annotation
|
59 | x: VarArg("str")
60 |
61 | x: List[List[List["MyClass"]]]
| ^^^^^^^^^ UP037
62 |
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
= help: Remove quotes
Safe fix
58 58 |
59 59 | x: VarArg("str")
60 60 |
61 |-x: List[List[List["MyClass"]]]
61 |+x: List[List[List[MyClass]]]
62 62 |
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
UP037_0.py:63:29: UP037 [*] Remove quotes from type annotation
|
61 | x: List[List[List["MyClass"]]]
62 |
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
= help: Remove quotes
Safe fix
60 60 |
61 61 | x: List[List[List["MyClass"]]]
62 62 |
63 |-x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
63 |+x: NamedTuple("X", [("foo", int), ("bar", "str")])
64 64 |
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
UP037_0.py:63:45: UP037 [*] Remove quotes from type annotation
|
61 | x: List[List[List["MyClass"]]]
62 |
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
= help: Remove quotes
Safe fix
60 60 |
61 61 | x: List[List[List["MyClass"]]]
62 62 |
63 |-x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
63 |+x: NamedTuple("X", [("foo", "int"), ("bar", str)])
64 64 |
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
UP037_0.py:65:29: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
= help: Remove quotes
Safe fix
62 62 |
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
65 |-x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
65 |+x: NamedTuple("X", fields=[(foo, "int"), ("bar", "str")])
66 66 |
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:36: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
= help: Remove quotes
Safe fix
62 62 |
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
65 |-x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
65 |+x: NamedTuple("X", fields=[("foo", int), ("bar", "str")])
66 66 |
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:45: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
= help: Remove quotes
Safe fix
62 62 |
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
65 |-x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
65 |+x: NamedTuple("X", fields=[("foo", "int"), (bar, "str")])
66 66 |
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:52: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
| ^^^^^ UP037
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
= help: Remove quotes
Safe fix
62 62 |
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
65 |-x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
65 |+x: NamedTuple("X", fields=[("foo", "int"), ("bar", str)])
66 66 |
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:67:24: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
| ^^^ UP037
68 |
69 | X: MyCallable("X")
|
= help: Remove quotes
Safe fix
64 64 |
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
67 |-x: NamedTuple(typename="X", fields=[("foo", "int")])
67 |+x: NamedTuple(typename=X, fields=[("foo", "int")])
68 68 |
69 69 | X: MyCallable("X")
70 70 |
UP037_0.py:67:38: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
| ^^^^^ UP037
68 |
69 | X: MyCallable("X")
|
= help: Remove quotes
Safe fix
64 64 |
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
67 |-x: NamedTuple(typename="X", fields=[("foo", "int")])
67 |+x: NamedTuple(typename="X", fields=[(foo, "int")])
68 68 |
69 69 | X: MyCallable("X")
70 70 |
UP037_0.py:67:45: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
| ^^^^^ UP037
68 |
69 | X: MyCallable("X")
|
= help: Remove quotes
Safe fix
64 64 |
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
67 |-x: NamedTuple(typename="X", fields=[("foo", "int")])
67 |+x: NamedTuple(typename="X", fields=[("foo", int)])
68 68 |
69 69 | X: MyCallable("X")
70 70 |
UP037_0.py:112:12: UP037 [*] Remove quotes from type annotation
|
110 | # Handle end of line comment in string annotation
111 | # See https://github.com/astral-sh/ruff/issues/15816
112 | def f() -> "Literal[0]#":
| ^^^^^^^^^^^^^ UP037
113 | return 0
|
= help: Remove quotes
Safe fix
109 109 |
110 110 | # Handle end of line comment in string annotation
111 111 | # See https://github.com/astral-sh/ruff/issues/15816
112 |-def f() -> "Literal[0]#":
112 |+def f() -> (Literal[0]#
113 |+):
113 114 | return 0
114 115 |
115 116 | def g(x: "Literal['abc']#") -> None:
UP037_0.py:115:10: UP037 [*] Remove quotes from type annotation
|
113 | return 0
114 |
115 | def g(x: "Literal['abc']#") -> None:
| ^^^^^^^^^^^^^^^^^ UP037
116 | return
|
= help: Remove quotes
Safe fix
112 112 | def f() -> "Literal[0]#":
113 113 | return 0
114 114 |
115 |-def g(x: "Literal['abc']#") -> None:
115 |+def g(x: (Literal['abc']#
116 |+)) -> None:
116 117 | return
117 118 |
118 119 | def f() -> """Literal[0]
UP037_0.py:118:12: UP037 [*] Remove quotes from type annotation
|
116 | return
117 |
118 | def f() -> """Literal[0]
| ____________^
119 | | #
120 | |
121 | | """:
| |_______^ UP037
122 | return 0
|
= help: Remove quotes
Safe fix
115 115 | def g(x: "Literal['abc']#") -> None:
116 116 | return
117 117 |
118 |-def f() -> """Literal[0]
118 |+def f() -> (Literal[0]
119 119 | #
120 120 |
121 |- """:
121 |+ ):
122 122 | return 0

View file

@ -0,0 +1,42 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037_1.py:9:8: UP037 [*] Remove quotes from type annotation
|
7 | def foo():
8 | # UP037
9 | x: "Tuple[int, int]" = (0, 0)
| ^^^^^^^^^^^^^^^^^ UP037
10 | print(x)
|
= help: Remove quotes
Safe fix
6 6 |
7 7 | def foo():
8 8 | # UP037
9 |- x: "Tuple[int, int]" = (0, 0)
9 |+ x: Tuple[int, int] = (0, 0)
10 10 | print(x)
11 11 |
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)

View file

@ -0,0 +1,232 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037_2.pyi:3:14: UP037 [*] Remove quotes from type annotation
|
1 | # https://github.com/astral-sh/ruff/issues/7102
2 |
3 | def f(a: Foo['SingleLine # Comment']): ...
| ^^^^^^^^^^^^^^^^^^^^^^^ UP037
|
= help: Remove quotes
Safe fix
1 1 | # https://github.com/astral-sh/ruff/issues/7102
2 2 |
3 |-def f(a: Foo['SingleLine # Comment']): ...
3 |+def f(a: Foo[(SingleLine # Comment
4 |+)]): ...
4 5 |
5 6 |
6 7 | def f(a: Foo['''Bar[
UP037_2.pyi:6:14: UP037 [*] Remove quotes from type annotation
|
6 | def f(a: Foo['''Bar[
| ______________^
7 | | Multi |
8 | | Line]''']): ...
| |____________^ UP037
|
= help: Remove quotes
Safe fix
3 3 | def f(a: Foo['SingleLine # Comment']): ...
4 4 |
5 5 |
6 |-def f(a: Foo['''Bar[
6 |+def f(a: Foo[Bar[
7 7 | Multi |
8 |- Line]''']): ...
8 |+ Line]]): ...
9 9 |
10 10 |
11 11 | def f(a: Foo['''Bar[
UP037_2.pyi:11:14: UP037 [*] Remove quotes from type annotation
|
11 | def f(a: Foo['''Bar[
| ______________^
12 | | Multi |
13 | | Line # Comment
14 | | ]''']): ...
| |____^ UP037
|
= help: Remove quotes
Safe fix
8 8 | Line]''']): ...
9 9 |
10 10 |
11 |-def f(a: Foo['''Bar[
11 |+def f(a: Foo[Bar[
12 12 | Multi |
13 13 | Line # Comment
14 |-]''']): ...
14 |+]]): ...
15 15 |
16 16 |
17 17 | def f(a: Foo['''Bar[
UP037_2.pyi:17:14: UP037 [*] Remove quotes from type annotation
|
17 | def f(a: Foo['''Bar[
| ______________^
18 | | Multi |
19 | | Line] # Comment''']): ...
| |_______________________^ UP037
|
= help: Remove quotes
Safe fix
14 14 | ]''']): ...
15 15 |
16 16 |
17 |-def f(a: Foo['''Bar[
17 |+def f(a: Foo[(Bar[
18 18 | Multi |
19 |- Line] # Comment''']): ...
19 |+ Line] # Comment
20 |+)]): ...
20 21 |
21 22 |
22 23 | def f(a: Foo['''
UP037_2.pyi:22:14: UP037 [*] Remove quotes from type annotation
|
22 | def f(a: Foo['''
| ______________^
23 | | Bar[
24 | | Multi |
25 | | Line] # Comment''']): ...
| |_______________________^ UP037
|
= help: Remove quotes
Safe fix
19 19 | Line] # Comment''']): ...
20 20 |
21 21 |
22 |-def f(a: Foo['''
22 |+def f(a: Foo[(
23 23 | Bar[
24 24 | Multi |
25 |- Line] # Comment''']): ...
25 |+ Line] # Comment
26 |+)]): ...
26 27 |
27 28 |
28 29 | def f(a: '''list[int]
UP037_2.pyi:28:10: UP037 [*] Remove quotes from type annotation
|
28 | def f(a: '''list[int]
| __________^
29 | | ''' = []): ...
| |_______^ UP037
|
= help: Remove quotes
Safe fix
25 25 | Line] # Comment''']): ...
26 26 |
27 27 |
28 |-def f(a: '''list[int]
29 |- ''' = []): ...
28 |+def f(a: list[int]
29 |+ = []): ...
30 30 |
31 31 |
32 32 | a: '''\\
UP037_2.pyi:32:4: UP037 [*] Remove quotes from type annotation
|
32 | a: '''\\
| ____^
33 | | list[int]''' = [42]
| |____________^ UP037
|
= help: Remove quotes
Safe fix
29 29 | ''' = []): ...
30 30 |
31 31 |
32 |-a: '''\\
33 |-list[int]''' = [42]
32 |+a: (\
33 |+list[int]) = [42]
34 34 |
35 35 |
36 36 | def f(a: '''
UP037_2.pyi:36:10: UP037 [*] Remove quotes from type annotation
|
36 | def f(a: '''
| __________^
37 | | list[int]
38 | | ''' = []): ...
| |_______^ UP037
|
= help: Remove quotes
Safe fix
33 33 | list[int]''' = [42]
34 34 |
35 35 |
36 |-def f(a: '''
36 |+def f(a:
37 37 | list[int]
38 |- ''' = []): ...
38 |+ = []): ...
39 39 |
40 40 |
41 41 | def f(a: Foo['''
UP037_2.pyi:41:14: UP037 [*] Remove quotes from type annotation
|
41 | def f(a: Foo['''
| ______________^
42 | | Bar
43 | | [
44 | | Multi |
45 | | Line
46 | | ] # Comment''']): ...
| |___________________^ UP037
|
= help: Remove quotes
Safe fix
38 38 | ''' = []): ...
39 39 |
40 40 |
41 |-def f(a: Foo['''
41 |+def f(a: Foo[(
42 42 | Bar
43 43 | [
44 44 | Multi |
45 45 | Line
46 |- ] # Comment''']): ...
46 |+ ] # Comment
47 |+)]): ...
47 48 |
48 49 |
49 50 | a: '''list
UP037_2.pyi:49:4: UP037 [*] Remove quotes from type annotation
|
49 | a: '''list
| ____^
50 | | [int]''' = [42]
| |________^ UP037
|
= help: Remove quotes
Safe fix
46 46 | ] # Comment''']): ...
47 47 |
48 48 |
49 |-a: '''list
50 |-[int]''' = [42]
49 |+a: (list
50 |+[int]) = [42]

View file

@ -599,4 +599,24 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_0.py"))]
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))]
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_2.py"))]
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_3.py"))]
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_4.py"))]
fn ruf013_add_future_import(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("add_future_import_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("ruff").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
future_annotations: true,
unresolved_target_version: PythonVersion::PY39.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View file

@ -71,6 +71,13 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none;
///
/// ## Options
/// - `target-version`
/// - `lint.future-annotations`
///
/// ## Preview
///
/// When [preview] is enabled, if [`lint.future-annotations`] is set to `true`,
/// `from __future__ import annotations` will be added if doing so would allow using the `|`
/// operator on a Python version before 3.10.
///
/// ## Fix safety
///
@ -136,10 +143,15 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr)
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
});
let content = checker.generator().expr(&new_expr);
Ok(Fix::unsafe_edit(Edit::range_replacement(
content,
expr.range(),
)))
let edit = Edit::range_replacement(content, expr.range());
if checker.target_version() < PythonVersion::PY310 {
Ok(Fix::unsafe_edits(
edit,
[checker.importer().add_future_import()],
))
} else {
Ok(Fix::unsafe_edit(edit))
}
}
ConversionType::Optional => {
let importer = checker
@ -187,6 +199,7 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
) else {
continue;
};
let conversion_type = checker.target_version().into();
let mut diagnostic =
@ -202,7 +215,14 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
else {
continue;
};
let conversion_type = checker.target_version().into();
let conversion_type = if checker.target_version() >= PythonVersion::PY310
|| checker.settings().future_annotations()
{
ConversionType::BinOpOr
} else {
ConversionType::Optional
};
let mut diagnostic =
checker.report_diagnostic(ImplicitOptional { conversion_type }, expr.range());

View file

@ -0,0 +1,445 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF013_0.py:20:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
20 | def f(arg: int = None): # RUF013
| ^^^ RUF013
21 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
20 |-def f(arg: int = None): # RUF013
21 |+def f(arg: int | None = None): # RUF013
21 22 | pass
22 23 |
23 24 |
RUF013_0.py:24:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
24 | def f(arg: str = None): # RUF013
| ^^^ RUF013
25 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
24 |-def f(arg: str = None): # RUF013
25 |+def f(arg: str | None = None): # RUF013
25 26 | pass
26 27 |
27 28 |
RUF013_0.py:28:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
28 | def f(arg: Tuple[str] = None): # RUF013
| ^^^^^^^^^^ RUF013
29 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
28 |-def f(arg: Tuple[str] = None): # RUF013
29 |+def f(arg: Tuple[str] | None = None): # RUF013
29 30 | pass
30 31 |
31 32 |
RUF013_0.py:58:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
58 | def f(arg: Union = None): # RUF013
| ^^^^^ RUF013
59 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
58 |-def f(arg: Union = None): # RUF013
59 |+def f(arg: Union | None = None): # RUF013
59 60 | pass
60 61 |
61 62 |
RUF013_0.py:62:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
62 | def f(arg: Union[int] = None): # RUF013
| ^^^^^^^^^^ RUF013
63 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
62 |-def f(arg: Union[int] = None): # RUF013
63 |+def f(arg: Union[int] | None = None): # RUF013
63 64 | pass
64 65 |
65 66 |
RUF013_0.py:66:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
66 | def f(arg: Union[int, str] = None): # RUF013
| ^^^^^^^^^^^^^^^ RUF013
67 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
66 |-def f(arg: Union[int, str] = None): # RUF013
67 |+def f(arg: Union[int, str] | None = None): # RUF013
67 68 | pass
68 69 |
69 70 |
RUF013_0.py:85:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
85 | def f(arg: int | float = None): # RUF013
| ^^^^^^^^^^^ RUF013
86 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
85 |-def f(arg: int | float = None): # RUF013
86 |+def f(arg: int | float | None = None): # RUF013
86 87 | pass
87 88 |
88 89 |
RUF013_0.py:89:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
89 | def f(arg: int | float | str | bytes = None): # RUF013
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013
90 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
89 |-def f(arg: int | float | str | bytes = None): # RUF013
90 |+def f(arg: int | float | str | bytes | None = None): # RUF013
90 91 | pass
91 92 |
92 93 |
RUF013_0.py:108:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
108 | def f(arg: Literal[1] = None): # RUF013
| ^^^^^^^^^^ RUF013
109 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
108 |-def f(arg: Literal[1] = None): # RUF013
109 |+def f(arg: Literal[1] | None = None): # RUF013
109 110 | pass
110 111 |
111 112 |
RUF013_0.py:112:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
112 | def f(arg: Literal[1, "foo"] = None): # RUF013
| ^^^^^^^^^^^^^^^^^ RUF013
113 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
112 |-def f(arg: Literal[1, "foo"] = None): # RUF013
113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF013
113 114 | pass
114 115 |
115 116 |
RUF013_0.py:131:22: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
131 | def f(arg: Annotated[int, ...] = None): # RUF013
| ^^^ RUF013
132 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
131 |-def f(arg: Annotated[int, ...] = None): # RUF013
132 |+def f(arg: Annotated[int | None, ...] = None): # RUF013
132 133 | pass
133 134 |
134 135 |
RUF013_0.py:135:32: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
135 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013
| ^^^^^^^^^ RUF013
136 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
135 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013
136 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF013
136 137 | pass
137 138 |
138 139 |
RUF013_0.py:151:11: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
150 | def f(
151 | arg1: int = None, # RUF013
| ^^^ RUF013
152 | arg2: Union[int, float] = None, # RUF013
153 | arg3: Literal[1, 2, 3] = None, # RUF013
|
= help: Convert to `T | None`
Unsafe fix
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(
151 |- arg1: int = None, # RUF013
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 | ):
RUF013_0.py:152:11: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
150 | def f(
151 | arg1: int = None, # RUF013
152 | arg2: Union[int, float] = None, # RUF013
| ^^^^^^^^^^^^^^^^^ RUF013
153 | arg3: Literal[1, 2, 3] = None, # RUF013
154 | ):
|
= help: Convert to `T | None`
Unsafe fix
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
152 |- arg2: Union[int, float] = None, # RUF013
153 |+ arg2: Union[int, float] | None = None, # RUF013
153 154 | arg3: Literal[1, 2, 3] = None, # RUF013
154 155 | ):
155 156 | pass
RUF013_0.py:153:11: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
151 | arg1: int = None, # RUF013
152 | arg2: Union[int, float] = None, # RUF013
153 | arg3: Literal[1, 2, 3] = None, # RUF013
| ^^^^^^^^^^^^^^^^ RUF013
154 | ):
155 | pass
|
= help: Convert to `T | None`
Unsafe fix
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
153 |- arg3: Literal[1, 2, 3] = None, # RUF013
154 |+ arg3: Literal[1, 2, 3] | None = None, # RUF013
154 155 | ):
155 156 | pass
156 157 |
RUF013_0.py:181:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
181 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013
182 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
181 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013
182 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF013
182 183 | pass
183 184 |
184 185 |
RUF013_0.py:188:13: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
188 | def f(arg: "int" = None): # RUF013
| ^^^ RUF013
189 | pass
|
= help: Convert to `Optional[T]`
Unsafe fix
185 185 | # Quoted
186 186 |
187 187 |
188 |-def f(arg: "int" = None): # RUF013
188 |+def f(arg: "Optional[int]" = None): # RUF013
189 189 | pass
190 190 |
191 191 |
RUF013_0.py:192:13: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
192 | def f(arg: "str" = None): # RUF013
| ^^^ RUF013
193 | pass
|
= help: Convert to `Optional[T]`
Unsafe fix
189 189 | pass
190 190 |
191 191 |
192 |-def f(arg: "str" = None): # RUF013
192 |+def f(arg: "Optional[str]" = None): # RUF013
193 193 | pass
194 194 |
195 195 |
RUF013_0.py:196:12: RUF013 PEP 484 prohibits implicit `Optional`
|
196 | def f(arg: "st" "r" = None): # RUF013
| ^^^^^^^^ RUF013
197 | pass
|
= help: Convert to `Optional[T]`
RUF013_0.py:204:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
204 | def f(arg: Union["int", "str"] = None): # RUF013
| ^^^^^^^^^^^^^^^^^^^ RUF013
205 | pass
|
= help: Convert to `T | None`
Unsafe fix
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 |
204 |-def f(arg: Union["int", "str"] = None): # RUF013
205 |+def f(arg: Union["int", "str"] | None = None): # RUF013
205 206 | pass
206 207 |
207 208 |

View file

@ -0,0 +1,19 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF013_1.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
4 | def f(arg: int = None): # RUF013
| ^^^ RUF013
5 | pass
|
= help: Convert to `T | None`
Unsafe fix
1 1 | # No `typing.Optional` import
2 |+from __future__ import annotations
2 3 |
3 4 |
4 |-def f(arg: int = None): # RUF013
5 |+def f(arg: int | None = None): # RUF013
5 6 | pass

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---

View file

@ -0,0 +1,65 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF013_3.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
4 | def f(arg: typing.List[str] = None): # RUF013
| ^^^^^^^^^^^^^^^^ RUF013
5 | pass
|
= help: Convert to `T | None`
Unsafe fix
1 |+from __future__ import annotations
1 2 | import typing
2 3 |
3 4 |
4 |-def f(arg: typing.List[str] = None): # RUF013
5 |+def f(arg: typing.List[str] | None = None): # RUF013
5 6 | pass
6 7 |
7 8 |
RUF013_3.py:22:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
22 | def f(arg: typing.Union[int, str] = None): # RUF013
| ^^^^^^^^^^^^^^^^^^^^^^ RUF013
23 | pass
|
= help: Convert to `T | None`
Unsafe fix
1 |+from __future__ import annotations
1 2 | import typing
2 3 |
3 4 |
--------------------------------------------------------------------------------
19 20 | pass
20 21 |
21 22 |
22 |-def f(arg: typing.Union[int, str] = None): # RUF013
23 |+def f(arg: typing.Union[int, str] | None = None): # RUF013
23 24 | pass
24 25 |
25 26 |
RUF013_3.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
29 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF013
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013
30 | pass
|
= help: Convert to `T | None`
Unsafe fix
1 |+from __future__ import annotations
1 2 | import typing
2 3 |
3 4 |
--------------------------------------------------------------------------------
26 27 | # Literal
27 28 |
28 29 |
29 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF013
30 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF013
30 31 | pass

View file

@ -0,0 +1,25 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF013_4.py:15:61: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ...
| ^^^ RUF013
|
= help: Convert to `T | None`
Unsafe fix
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 |
15 |-def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ...
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: ...

View file

@ -210,6 +210,7 @@ macro_rules! display_settings {
}
#[derive(Debug, Clone, CacheKey)]
#[expect(clippy::struct_excessive_bools)]
pub struct LinterSettings {
pub exclude: FilePatternSet,
pub extension: ExtensionMapping,
@ -251,6 +252,7 @@ pub struct LinterSettings {
pub task_tags: Vec<String>,
pub typing_modules: Vec<String>,
pub typing_extensions: bool,
pub future_annotations: bool,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
@ -453,6 +455,7 @@ impl LinterSettings {
explicit_preview_rules: false,
extension: ExtensionMapping::default(),
typing_extensions: true,
future_annotations: false,
}
}
@ -472,6 +475,11 @@ impl LinterSettings {
.is_match(path)
.map_or(self.unresolved_target_version, TargetVersion::from)
}
pub fn future_annotations(&self) -> bool {
// TODO(brent) we can just access the field directly once this is stabilized.
self.future_annotations && crate::preview::is_add_future_annotations_imports_enabled(self)
}
}
impl Default for LinterSettings {

View file

@ -250,6 +250,14 @@ impl Configuration {
conflicting_import_settings(&isort, &flake8_import_conventions)?;
let future_annotations = lint.future_annotations.unwrap_or_default();
if lint_preview.is_disabled() && future_annotations {
warn_user_once!(
"The `lint.future-annotations` setting will have no effect \
because `preview` is disabled"
);
}
Ok(Settings {
cache_dir: self
.cache_dir
@ -432,6 +440,7 @@ impl Configuration {
.map(RuffOptions::into_settings)
.unwrap_or_default(),
typing_extensions: lint.typing_extensions.unwrap_or(true),
future_annotations,
},
formatter,
@ -636,6 +645,7 @@ pub struct LintConfiguration {
pub task_tags: Option<Vec<String>>,
pub typing_modules: Option<Vec<String>>,
pub typing_extensions: Option<bool>,
pub future_annotations: Option<bool>,
// Plugins
pub flake8_annotations: Option<Flake8AnnotationsOptions>,
@ -752,6 +762,7 @@ impl LintConfiguration {
logger_objects: options.common.logger_objects,
typing_modules: options.common.typing_modules,
typing_extensions: options.typing_extensions,
future_annotations: options.future_annotations,
// Plugins
flake8_annotations: options.common.flake8_annotations,
@ -1179,6 +1190,7 @@ impl LintConfiguration {
pyupgrade: self.pyupgrade.combine(config.pyupgrade),
ruff: self.ruff.combine(config.ruff),
typing_extensions: self.typing_extensions.or(config.typing_extensions),
future_annotations: self.future_annotations.or(config.future_annotations),
}
}
}

View file

@ -529,6 +529,24 @@ pub struct LintOptions {
"#
)]
pub typing_extensions: Option<bool>,
/// Whether to allow rules to add `from __future__ import annotations` in cases where this would
/// simplify a fix or enable a new diagnostic.
///
/// For example, `TC001`, `TC002`, and `TC003` can move more imports into `TYPE_CHECKING` blocks
/// if `__future__` annotations are enabled.
///
/// This setting is currently in [preview](https://docs.astral.sh/ruff/preview/) and requires
/// preview mode to be enabled to have any effect.
#[option(
default = "false",
value_type = "bool",
example = r#"
# Enable `from __future__ import annotations` imports
future-annotations = true
"#
)]
pub future_annotations: Option<bool>,
}
/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`].
@ -3896,6 +3914,7 @@ pub struct LintOptionsWire {
ruff: Option<RuffOptions>,
preview: Option<bool>,
typing_extensions: Option<bool>,
future_annotations: Option<bool>,
}
impl From<LintOptionsWire> for LintOptions {
@ -3951,6 +3970,7 @@ impl From<LintOptionsWire> for LintOptions {
ruff,
preview,
typing_extensions,
future_annotations,
} = value;
LintOptions {
@ -4007,6 +4027,7 @@ impl From<LintOptionsWire> for LintOptions {
ruff,
preview,
typing_extensions,
future_annotations,
}
}
}

7
ruff.schema.json generated
View file

@ -2281,6 +2281,13 @@
}
]
},
"future-annotations": {
"description": "Whether to allow rules to add `from __future__ import annotations` in cases where this would simplify a fix or enable a new diagnostic.\n\nFor example, `TC001`, `TC002`, and `TC003` can move more imports into `TYPE_CHECKING` blocks if `__future__` annotations are enabled.\n\nThis setting is currently in [preview](https://docs.astral.sh/ruff/preview/) and requires preview mode to be enabled to have any effect.",
"type": [
"boolean",
"null"
]
},
"ignore": {
"description": "A list of rule codes or prefixes to ignore. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes. `ignore` takes precedence over `select` if the same prefix appears in both.",
"type": [