Skip to content

Commit ebfb33c

Browse files
authored
[ruff] Extend FA102 with listed PEP 585-compatible APIs (#20659)
Resolves #20512 This PR expands FA102’s preview coverage to flag every PEP 585-compatible API that breaks without from `from __future__ import annotations`, including `collections.abc`. The rule now treats asyncio futures, pathlib-style queues, weakref containers, shelve proxies, and the full `collections.abc` family as generics once preview mode is enabled. Stable behavior is unchanged; the broader matching runs behind `is_future_required_preview_generics_enabled`, letting us vet the new diagnostics before marking them as stable. I've also added a snapshot test that covers all of the newly supported types. Check out https://docs.python.org/3/library/stdtypes.html#standard-generic-classes for a list of commonly used PEP 585-compatible APIs.
1 parent 7d7237c commit ebfb33c

File tree

8 files changed

+1049
-27
lines changed

8 files changed

+1049
-27
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import asyncio
2+
import collections
3+
import contextlib
4+
import dataclasses
5+
import functools
6+
import os
7+
import queue
8+
import re
9+
import shelve
10+
import types
11+
import weakref
12+
from collections.abc import (
13+
AsyncGenerator,
14+
AsyncIterable,
15+
AsyncIterator,
16+
Awaitable,
17+
ByteString,
18+
Callable,
19+
Collection,
20+
Container,
21+
Coroutine,
22+
Generator,
23+
Iterable,
24+
Iterator,
25+
ItemsView,
26+
KeysView,
27+
Mapping,
28+
MappingView,
29+
MutableMapping,
30+
MutableSequence,
31+
MutableSet,
32+
Reversible,
33+
Sequence,
34+
Set,
35+
ValuesView,
36+
)
37+
38+
39+
def takes_preview_generics(
40+
future: asyncio.Future[int],
41+
task: asyncio.Task[str],
42+
deque_object: collections.deque[int],
43+
defaultdict_object: collections.defaultdict[str, int],
44+
ordered_dict: collections.OrderedDict[str, int],
45+
counter_obj: collections.Counter[str],
46+
chain_map: collections.ChainMap[str, int],
47+
context_manager: contextlib.AbstractContextManager[str],
48+
async_context_manager: contextlib.AbstractAsyncContextManager[int],
49+
dataclass_field: dataclasses.Field[int],
50+
cached_prop: functools.cached_property[int],
51+
partial_method: functools.partialmethod[int],
52+
path_like: os.PathLike[str],
53+
lifo_queue: queue.LifoQueue[int],
54+
regular_queue: queue.Queue[int],
55+
priority_queue: queue.PriorityQueue[int],
56+
simple_queue: queue.SimpleQueue[int],
57+
regex_pattern: re.Pattern[str],
58+
regex_match: re.Match[str],
59+
bsd_db_shelf: shelve.BsdDbShelf[str, int],
60+
db_filename_shelf: shelve.DbfilenameShelf[str, int],
61+
shelf_obj: shelve.Shelf[str, int],
62+
mapping_proxy: types.MappingProxyType[str, int],
63+
weak_key_dict: weakref.WeakKeyDictionary[object, int],
64+
weak_method: weakref.WeakMethod[int],
65+
weak_set: weakref.WeakSet[int],
66+
weak_value_dict: weakref.WeakValueDictionary[object, int],
67+
awaitable: Awaitable[int],
68+
coroutine: Coroutine[int, None, str],
69+
async_iterable: AsyncIterable[int],
70+
async_iterator: AsyncIterator[int],
71+
async_generator: AsyncGenerator[int, None],
72+
iterable: Iterable[int],
73+
iterator: Iterator[int],
74+
generator: Generator[int, None, None],
75+
reversible: Reversible[int],
76+
container: Container[int],
77+
collection: Collection[int],
78+
callable_obj: Callable[[int], str],
79+
set_obj: Set[int],
80+
mutable_set: MutableSet[int],
81+
mapping: Mapping[str, int],
82+
mutable_mapping: MutableMapping[str, int],
83+
sequence: Sequence[int],
84+
mutable_sequence: MutableSequence[int],
85+
byte_string: ByteString[int],
86+
mapping_view: MappingView[str, int],
87+
keys_view: KeysView[str],
88+
items_view: ItemsView[str, int],
89+
values_view: ValuesView[int],
90+
) -> None:
91+
...

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use ruff_text_size::Ranged;
88

99
use crate::checkers::ast::Checker;
1010
use crate::preview::{
11-
is_optional_as_none_in_union_enabled, is_unnecessary_default_type_args_stubs_enabled,
11+
is_future_required_preview_generics_enabled, is_optional_as_none_in_union_enabled,
12+
is_unnecessary_default_type_args_stubs_enabled,
1213
};
1314
use crate::registry::Rule;
1415
use crate::rules::{
@@ -69,7 +70,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
6970
&& checker.semantic.in_annotation()
7071
&& checker.semantic.in_runtime_evaluated_annotation()
7172
&& !checker.semantic.in_string_type_definition()
72-
&& typing::is_pep585_generic(value, &checker.semantic)
73+
&& typing::is_pep585_generic(
74+
value,
75+
&checker.semantic,
76+
is_future_required_preview_generics_enabled(checker.settings()),
77+
)
7378
{
7479
flake8_future_annotations::rules::future_required_type_annotation(
7580
checker,

crates/ruff_linter/src/preview.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ pub(crate) const fn is_optional_as_none_in_union_enabled(settings: &LinterSettin
200200
settings.preview.is_enabled()
201201
}
202202

203+
// https://github.com/astral-sh/ruff/pull/20659
204+
pub(crate) const fn is_future_required_preview_generics_enabled(settings: &LinterSettings) -> bool {
205+
settings.preview.is_enabled()
206+
}
207+
203208
// https://github.com/astral-sh/ruff/pull/18683
204209
pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
205210
settings: &LinterSettings,

crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod tests {
99
use test_case::test_case;
1010

1111
use crate::registry::Rule;
12+
use crate::settings::types::PreviewMode;
1213
use crate::test::test_path;
1314
use crate::{assert_diagnostics, settings};
1415
use ruff_python_ast::PythonVersion;
@@ -39,6 +40,7 @@ mod tests {
3940
}
4041

4142
#[test_case(Path::new("no_future_import_uses_lowercase.py"))]
43+
#[test_case(Path::new("no_future_import_uses_preview_generics.py"))]
4244
#[test_case(Path::new("no_future_import_uses_union.py"))]
4345
#[test_case(Path::new("no_future_import_uses_union_inner.py"))]
4446
#[test_case(Path::new("ok_no_types.py"))]
@@ -56,4 +58,19 @@ mod tests {
5658
assert_diagnostics!(snapshot, diagnostics);
5759
Ok(())
5860
}
61+
62+
#[test_case(Path::new("no_future_import_uses_preview_generics.py"))]
63+
fn fa102_preview(path: &Path) -> Result<()> {
64+
let snapshot = format!("fa102_preview_{}", path.to_string_lossy());
65+
let diagnostics = test_path(
66+
Path::new("flake8_future_annotations").join(path).as_path(),
67+
&settings::LinterSettings {
68+
unresolved_target_version: PythonVersion::PY37.into(),
69+
preview: PreviewMode::Enabled,
70+
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
71+
},
72+
)?;
73+
assert_diagnostics!(snapshot, diagnostics);
74+
Ok(())
75+
}
5976
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
3+
---
4+
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
5+
--> no_future_import_uses_preview_generics.py:42:19
6+
|
7+
40 | future: asyncio.Future[int],
8+
41 | task: asyncio.Task[str],
9+
42 | deque_object: collections.deque[int],
10+
| ^^^^^^^^^^^^^^^^^^^^^^
11+
43 | defaultdict_object: collections.defaultdict[str, int],
12+
44 | ordered_dict: collections.OrderedDict[str, int],
13+
|
14+
help: Add `from __future__ import annotations`
15+
1 + from __future__ import annotations
16+
2 | import asyncio
17+
3 | import collections
18+
4 | import contextlib
19+
note: This is an unsafe fix and may change runtime behavior
20+
21+
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
22+
--> no_future_import_uses_preview_generics.py:43:25
23+
|
24+
41 | task: asyncio.Task[str],
25+
42 | deque_object: collections.deque[int],
26+
43 | defaultdict_object: collections.defaultdict[str, int],
27+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28+
44 | ordered_dict: collections.OrderedDict[str, int],
29+
45 | counter_obj: collections.Counter[str],
30+
|
31+
help: Add `from __future__ import annotations`
32+
1 + from __future__ import annotations
33+
2 | import asyncio
34+
3 | import collections
35+
4 | import contextlib
36+
note: This is an unsafe fix and may change runtime behavior

0 commit comments

Comments
 (0)