Skip to content

Commit de1f20c

Browse files
Introduce Y022/Y023/Y024/Y025: Imports linting error codes (#97)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent eda08c8 commit de1f20c

File tree

7 files changed

+321
-4
lines changed

7 files changed

+321
-4
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ ignore = E302, E501, E701, W503, E203
55
max-line-length = 80
66
max-complexity = 12
77
select = B,C,E,F,W,Y,B9
8+
per-file-ignores =
9+
tests/imports.pyi: F401, F811

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ unreleased
2323
* introduce Y016 (duplicate union member)
2424
* support Python 3.10
2525
* discontinue support for Python 3.6
26+
* introduce Y022 (prefer stdlib classes over ``typing`` aliases)
27+
* introduce Y023 (prefer ``typing`` over ``typing_extensions``)
28+
* introduce Y024 (prefer ``typing.NamedTuple`` to ``collections.namedtuple``)
29+
* introduce Y025 (always alias ``collections.abc.Set``)
2630

2731
20.10.0
2832
~~~~~~~

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ currently emitted:
8181
an instance of ``cls``, and ``__new__`` methods.
8282
* Y020: Quoted annotations should never be used in stubs.
8383
* Y021: Docstrings should not be included in stubs.
84+
* Y022: Imports linting: use typing-module aliases to stdlib objects as little
85+
as possible (e.g. ``builtins.list`` over ``typing.List``,
86+
``collections.Counter`` over ``typing.Counter``, etc.).
87+
* Y023: Where there is no detriment to backwards compatibility, import objects
88+
such as ``ClassVar`` and ``NoReturn`` from ``typing`` rather than
89+
``typing_extensions``.
90+
* Y024: Use ``typing.NamedTuple`` instead of ``collections.namedtuple``, as it allows
91+
for more precise type inference.
92+
* Y025: Always alias ``collections.abc.Set`` when importing it, so as to avoid
93+
confusion with ``builtins.set``.
8494

8595
The following warnings are disabled by default:
8696

pyi.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,59 @@ class TypeVarInfo(NamedTuple):
4747
name: str
4848

4949

50+
# OrderedDict is omitted from this blacklist:
51+
# -- In Python 3, we'd rather import it from collections, not typing or typing_extensions
52+
# -- But in Python 2, it cannot be imported from collections or typing, only from typing_extensions
53+
#
54+
# ChainMap does not exist in typing or typing_extensions in Python 2,
55+
# so we can disallow importing it from anywhere except collections
56+
_BAD_Y022_IMPORTS = {
57+
# typing aliases for collections
58+
"typing.Counter": "collections.Counter",
59+
"typing.Deque": "collections.deque",
60+
"typing.DefaultDict": "collections.defaultdict",
61+
"typing.ChainMap": "collections.ChainMap",
62+
# typing aliases for builtins
63+
"typing.Dict": "builtins.dict",
64+
"typing.FrozenSet": "builtins.frozenset",
65+
"typing.List": "builtins.list",
66+
"typing.Set": "builtins.set",
67+
"typing.Tuple": "builtins.tuple",
68+
"typing.Type": "builtins.type",
69+
# One typing alias for contextlib
70+
"typing.AsyncContextManager": "contextlib.AbstractAsyncContextManager",
71+
# typing_extensions aliases for collections
72+
"typing_extensions.Counter": "collections.Counter",
73+
"typing_extensions.Deque": "collections.deque",
74+
"typing_extensions.DefaultDict": "collections.defaultdict",
75+
"typing_extensions.ChainMap": "collections.ChainMap",
76+
# One typing_extensions alias for a builtin
77+
"typing_extensions.Type": "builtins.type",
78+
# one typing_extensions alias for contextlib
79+
"typing_extensions.AsyncContextManager": "contextlib.AbstractAsyncContextManager",
80+
}
81+
82+
# typing_extensions.ContextManager is omitted from this collection - special-cased
83+
_BAD_Y023_IMPORTS = frozenset(
84+
{
85+
# collections.abc aliases
86+
"Awaitable",
87+
"Coroutine",
88+
"AsyncIterable",
89+
"AsyncIterator",
90+
"AsyncGenerator",
91+
# typing aliases
92+
"Protocol",
93+
"runtime_checkable",
94+
"ClassVar",
95+
"NewType",
96+
"overload",
97+
"Text",
98+
"NoReturn",
99+
}
100+
)
101+
102+
50103
class PyiAwareFlakesChecker(FlakesChecker):
51104
def deferHandleNode(self, node, parent):
52105
self.deferFunction(lambda: self.handleNode(node, parent))
@@ -192,6 +245,73 @@ def in_class(self) -> bool:
192245
"""Determine whether we are inside a `class` statement"""
193246
return bool(self._class_nesting)
194247

248+
def _check_import_or_attribute(
249+
self, node: ast.Attribute | ast.ImportFrom, module_name: str, object_name: str
250+
) -> None:
251+
fullname = f"{module_name}.{object_name}"
252+
253+
# Y022 errors
254+
if fullname in _BAD_Y022_IMPORTS:
255+
error_message = Y022.format(
256+
good_cls_name=f'"{_BAD_Y022_IMPORTS[fullname]}"',
257+
bad_cls_alias=fullname,
258+
)
259+
260+
# Y023 errors
261+
elif module_name == "typing_extensions":
262+
if object_name in _BAD_Y023_IMPORTS:
263+
error_message = Y023.format(
264+
good_cls_name=f'"typing.{object_name}"',
265+
bad_cls_alias=f"typing_extensions.{object_name}",
266+
)
267+
elif object_name == "ContextManager":
268+
suggested_syntax = (
269+
'"contextlib.AbstractContextManager" '
270+
'(or "typing.ContextManager" in Python 2-compatible code)'
271+
)
272+
error_message = Y023.format(
273+
good_cls_name=suggested_syntax,
274+
bad_cls_alias="typing_extensions.ContextManager",
275+
)
276+
else:
277+
return
278+
279+
# Y024 errors
280+
elif fullname == "collections.namedtuple":
281+
error_message = Y024
282+
283+
else:
284+
return
285+
286+
self.error(node, error_message)
287+
288+
def visit_Attribute(self, node: ast.Attribute) -> None:
289+
self.generic_visit(node)
290+
thing = node.value
291+
if not isinstance(thing, ast.Name):
292+
return
293+
294+
self._check_import_or_attribute(
295+
node=node, module_name=thing.id, object_name=node.attr
296+
)
297+
298+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
299+
module_name, imported_objects = node.module, node.names
300+
301+
if module_name is None:
302+
return
303+
304+
if module_name == "collections.abc" and any(
305+
obj.name == "Set" and obj.asname != "AbstractSet"
306+
for obj in imported_objects
307+
):
308+
return self.error(node, Y025)
309+
310+
for obj in imported_objects:
311+
self._check_import_or_attribute(
312+
node=node, module_name=module_name, object_name=obj.name
313+
)
314+
195315
def visit_Assign(self, node: ast.Assign) -> None:
196316
if self.in_function:
197317
# We error for unexpected things within functions separately.
@@ -267,6 +387,7 @@ def visit_Expr(self, node: ast.Expr) -> None:
267387
self.generic_visit(node)
268388

269389
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
390+
self.generic_visit(node)
270391
if isinstance(node.annotation, ast.Name) and node.annotation.id == "TypeAlias":
271392
return
272393
if node.value and not isinstance(node.value, ast.Ellipsis):
@@ -752,6 +873,13 @@ def should_warn(self, code):
752873
Y019 = 'Y019 Use "_typeshed.Self" instead of "{typevar_name}", e.g. "{new_syntax}"'
753874
Y020 = "Y020 Quoted annotations should never be used in stubs"
754875
Y021 = "Y021 Docstrings should not be included in stubs"
876+
Y022 = 'Y022 Use {good_cls_name} instead of "{bad_cls_alias}"'
877+
Y023 = 'Y023 Use {good_cls_name} instead of "{bad_cls_alias}"'
878+
Y024 = 'Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"'
879+
Y025 = (
880+
'Y025 Use "from collections.abc import Set as AbstractSet" '
881+
'to avoid confusion with "builtins.set"'
882+
)
755883
Y093 = "Y093 Use typing_extensions.TypeAlias for type aliases"
756884

757885
DISABLED_BY_DEFAULT = [Y093]

tests/del.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import List, Union
1+
from typing import Union
22

33

4-
ManyStr = List[EitherStr]
4+
ManyStr = list[EitherStr]
55
EitherStr = Union[str, bytes]
66

77

tests/imports.pyi

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# NOTE: F401 & F811 are ignored in this file in the .flake8 config file
2+
3+
# GOOD IMPORTS
4+
import typing
5+
import typing_extensions
6+
import collections
7+
import collections.abc
8+
from collections import ChainMap, Counter, OrderedDict, UserDict, UserList, UserString, defaultdict, deque
9+
from collections.abc import (
10+
Awaitable,
11+
Coroutine,
12+
AsyncIterable,
13+
AsyncIterator,
14+
AsyncGenerator,
15+
Hashable,
16+
Iterable,
17+
Iterator,
18+
Generator,
19+
Reversible,
20+
Set as AbstractSet,
21+
Sized,
22+
Container,
23+
Callable,
24+
Collection,
25+
MutableSet,
26+
MutableMapping,
27+
MappingView,
28+
KeysView,
29+
ItemsView,
30+
ValuesView,
31+
Sequence,
32+
MutableSequence,
33+
ByteString
34+
)
35+
36+
# Things that are of no use for stub files are intentionally omitted.
37+
from typing import (
38+
Any,
39+
Callable,
40+
ClassVar,
41+
Generic,
42+
Optional,
43+
Protocol,
44+
TypeVar,
45+
Union,
46+
AbstractSet,
47+
ByteString,
48+
Container,
49+
Hashable,
50+
ItemsView,
51+
Iterable,
52+
Iterator,
53+
KeysView,
54+
Mapping,
55+
MappingView,
56+
MutableMapping,
57+
MutableSequence,
58+
MutableSet,
59+
Sequence,
60+
Sized,
61+
ValuesView,
62+
Awaitable,
63+
AsyncIterator,
64+
AsyncIterable,
65+
Coroutine,
66+
Collection,
67+
AsyncGenerator,
68+
Reversible,
69+
SupportsAbs,
70+
SupportsBytes,
71+
SupportsComplex,
72+
SupportsFloat,
73+
SupportsIndex,
74+
SupportsInt,
75+
SupportsRound,
76+
Generator,
77+
BinaryIO,
78+
IO,
79+
NamedTuple,
80+
Match,
81+
Pattern,
82+
TextIO,
83+
AnyStr,
84+
NewType,
85+
NoReturn,
86+
overload,
87+
ContextManager # ContextManager must be importable from typing (but not typing_extensions) for Python 2 compabitility
88+
)
89+
from typing_extensions import (
90+
Concatenate,
91+
Final,
92+
ParamSpec,
93+
SupportsIndex,
94+
final,
95+
Literal,
96+
TypeAlias,
97+
TypeGuard,
98+
Annotated,
99+
TypedDict,
100+
OrderedDict # OrderedDict must be importable from typing_extensions (but not typing) for Python 2 compatibility
101+
)
102+
103+
104+
# BAD IMPORTS (Y022 code)
105+
from typing import Dict # Y022 Use "builtins.dict" instead of "typing.Dict"
106+
from typing import Counter # Y022 Use "collections.Counter" instead of "typing.Counter"
107+
from typing import AsyncContextManager # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing.AsyncContextManager"
108+
from typing import ChainMap # Y022 Use "collections.ChainMap" instead of "typing.ChainMap"
109+
from typing_extensions import Type # Y022 Use "builtins.type" instead of "typing_extensions.Type"
110+
from typing_extensions import DefaultDict # Y022 Use "collections.defaultdict" instead of "typing_extensions.DefaultDict"
111+
from typing_extensions import ChainMap # Y022 Use "collections.ChainMap" instead of "typing_extensions.ChainMap"
112+
from typing_extensions import AsyncContextManager # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing_extensions.AsyncContextManager"
113+
114+
# BAD IMPORTS (Y023 code)
115+
from typing_extensions import ClassVar # Y023 Use "typing.ClassVar" instead of "typing_extensions.ClassVar"
116+
from typing_extensions import Awaitable # Y023 Use "typing.Awaitable" instead of "typing_extensions.Awaitable"
117+
from typing_extensions import ContextManager # Y023 Use "contextlib.AbstractContextManager" (or "typing.ContextManager" in Python 2-compatible code) instead of "typing_extensions.ContextManager"
118+
119+
# BAD IMPORTS: OTHER
120+
from collections import namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
121+
from collections.abc import Set # Y025 Use "from collections.abc import Set as AbstractSet" to avoid confusion with "builtins.set"
122+
123+
# GOOD ATTRIBUTE ACCESS
124+
foo: typing.SupportsIndex
125+
126+
@typing_extensions.final
127+
def bar(arg: collections.abc.Sized) -> typing_extensions.Literal[True]:
128+
...
129+
130+
131+
class Fish:
132+
blah: collections.deque[int]
133+
134+
def method(self, arg: typing.SupportsInt = ...) -> None:
135+
...
136+
137+
138+
# BAD ATTRIBUTE ACCESS (Y022 code)
139+
a: typing.Dict[str, int] # Y022 Use "builtins.dict" instead of "typing.Dict"
140+
141+
def func1() -> typing.Counter[float]: # Y022 Use "collections.Counter" instead of "typing.Counter"
142+
...
143+
144+
145+
def func2(c: typing.AsyncContextManager[None]) -> None: # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing.AsyncContextManager"
146+
...
147+
148+
149+
def func3(d: typing.ChainMap[int, str] = ...) -> None: # Y022 Use "collections.ChainMap" instead of "typing.ChainMap"
150+
...
151+
152+
153+
class Spam:
154+
def meth1() -> typing_extensions.DefaultDict[bytes, bytes]: # Y022 Use "collections.defaultdict" instead of "typing_extensions.DefaultDict"
155+
...
156+
157+
def meth2(self, f: typing_extensions.ChainMap[str, str]) -> None: # Y022 Use "collections.ChainMap" instead of "typing_extensions.ChainMap"
158+
...
159+
160+
def meth3(self, g: typing_extensions.AsyncContextManager[Any] = ...) -> None: # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing_extensions.AsyncContextManager"
161+
...
162+
163+
164+
# BAD ATTRIBUTE ACCESS (Y023 code)
165+
class Foo:
166+
attribute: typing_extensions.ClassVar[int] # Y023 Use "typing.ClassVar" instead of "typing_extensions.ClassVar"
167+
168+
169+
h: typing_extensions.Awaitable[float] # Y023 Use "typing.Awaitable" instead of "typing_extensions.Awaitable"
170+
i: typing_extensions.ContextManager[None] # Y023 Use "contextlib.AbstractContextManager" (or "typing.ContextManager" in Python 2-compatible code) instead of "typing_extensions.ContextManager"
171+
172+
# BAD ATTRIBUTE ACCESS: OTHER
173+
j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"

tests/vanilla_flake8_not_clean_forward_refs.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# flags: --no-pyi-aware-file-checker
2-
from typing import List, Union
2+
from typing import Union
33

44

5-
ManyStr = List[EitherStr] # F821 undefined name 'EitherStr'
5+
ManyStr = list[EitherStr] # F821 undefined name 'EitherStr'
66
EitherStr = Union[str, bytes]
77

88

0 commit comments

Comments
 (0)