Skip to content

Commit fd2bae8

Browse files
authored
Forbid imports of collections.abc aliases from typing (#213)
A regression test for python/typeshed#7635 Refs #46 (but doesn't quite close it). Changes made: * Expand Y027 to cover all objects in typing that are aliases to objects in collections.abc, except for AbstractSet * Add new Y038 error code to forbid import AbstractSet from typing * Change the error message in Y023 to make it clear that imports from collections.abc are now preferred to imports from typing. * Refactor Y023 logic in pyi.py so as to quell flake8 from complaining that _check_import_or_attribute was getting too complex. * Some small refactorings of test files to make them compatible with the new checks.
1 parent 70c143d commit fd2bae8

File tree

8 files changed

+164
-103
lines changed

8 files changed

+164
-103
lines changed

.flake8

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@
1616
# E701 multiple statements on one line (colon) -- disallows "..." on the same line
1717
# E704 multiple statements on one line (def) -- disallows function body on the same line as the def
1818
#
19-
# tests/imports.pyi adds the following ignore codes for that specific file:
20-
# F401 module imported but unused
21-
# F811 redefinition of unused name
22-
#
2319
# tests/type_comments.pyi adds the following ignore codes for that specific file:
2420
# F723 syntax error in type comment
2521
# E261 at least two spaces before inline comment
@@ -32,5 +28,4 @@ select = B,C,E,F,W,Y,B9
3228
per-file-ignores =
3329
*.py: E203, E501, W503, W291, W293
3430
*.pyi: E301, E302, E305, E501, E701, E704, W503
35-
tests/imports.pyi: E301, E302, E305, E501, E701, E704, F401, F811, W503
3631
tests/type_comments.pyi: E261, E262, E301, E302, E305, E501, E701, E704, F723, W503

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## Unreleased
44

5+
Features:
6+
* Expand Y027 check to prohibit importing any objects from the `typing` module that are
7+
aliases for objects living `collections.abc` (except for `typing.AbstractSet`, which
8+
is special-cased).
9+
* Introduce Y038: Use `from collections.abc import Set as AbstractSet` instead of
10+
`from typing import AbstractSet`.
11+
512
Bugfixes:
613
* Improve inaccurate error messages for Y036.
714

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ currently emitted:
6969
| Y035 | `__all__` and `__match_args__` in a stub file should always have values, as these special variables in a `.pyi` file have identical semantics to `__all__` and `__match_args__` in a `.py` file. E.g. write `__all__ = ["foo", "bar"]` instead of `__all__: list[str]`.
7070
| Y036 | Y036 detects common errors in `__exit__` and `__aexit__` methods. For example, the first argument in an `__exit__` method should either be annotated with `object` or `type[BaseException] \| None`.
7171
| Y037 | Use PEP 604 syntax instead of `typing.Union` and `typing.Optional`. E.g. use `str \| int` instead of `Union[str, int]`, and use `str \| None` instead of `Optional[str]`.
72+
| Y038 | Use `from collections.abc import Set as AbstractSet` instead of `from typing import AbstractSet`. Like Y027, this error code should be switched off in your config file if your stubs support Python 2.
7273

7374
Many error codes enforce modern conventions, and some cannot yet be used in
7475
all cases:
7576

76-
* Y027 is incompatible with Python 2 and should only be used in stubs
77+
* Y027 and Y038 are incompatible with Python 2. These should only be used in stubs
7778
that are meant only for Python 3.
7879
* Y037 (enforcing PEP 604 syntax everywhere) is not yet fully compatible with
7980
the mypy type checker, which has

pyi.py

Lines changed: 112 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -58,68 +58,93 @@ class TypeVarInfo(NamedTuple):
5858
_MAPPING_SLICE = "KeyType, ValueType"
5959
_TYPE_SLICE = "MyClass"
6060
_COUNTER_SLICE = "KeyType"
61-
_CONTEXTLIB_SLICE = "T"
62-
_SET_SLICE = "T"
63-
_SEQUENCE_SLICE = "T"
61+
_COROUTINE_SLICE = "YieldType, SendType, ReturnType"
62+
_ASYNCGEN_SLICE = "YieldType, SendType"
6463

6564

6665
# ChainMap and AsyncContextManager do not exist in typing or typing_extensions in Python 2,
6766
# so we can disallow importing them from anywhere except collections and contextlib respectively.
67+
# A Python 2-compatible check
6868
_BAD_Y022_IMPORTS = {
6969
# typing aliases for collections
7070
"typing.Counter": ("collections.Counter", _COUNTER_SLICE),
71-
"typing.Deque": ("collections.deque", _SEQUENCE_SLICE),
71+
"typing.Deque": ("collections.deque", "T"),
7272
"typing.DefaultDict": ("collections.defaultdict", _MAPPING_SLICE),
7373
"typing.ChainMap": ("collections.ChainMap", _MAPPING_SLICE),
7474
# typing aliases for builtins
7575
"typing.Dict": ("dict", _MAPPING_SLICE),
76-
"typing.FrozenSet": ("frozenset", _SET_SLICE),
77-
"typing.List": ("list", _SEQUENCE_SLICE),
78-
"typing.Set": ("set", _SET_SLICE),
76+
"typing.FrozenSet": ("frozenset", "T"),
77+
"typing.List": ("list", "T"),
78+
"typing.Set": ("set", "T"),
7979
"typing.Tuple": ("tuple", "Foo, Bar"),
8080
"typing.Type": ("type", _TYPE_SLICE),
8181
# One typing alias for contextlib
82-
"typing.AsyncContextManager": (
83-
"contextlib.AbstractAsyncContextManager",
84-
_CONTEXTLIB_SLICE,
85-
),
82+
"typing.AsyncContextManager": ("contextlib.AbstractAsyncContextManager", "T"),
8683
# typing_extensions aliases for collections
8784
"typing_extensions.Counter": ("collections.Counter", _COUNTER_SLICE),
88-
"typing_extensions.Deque": ("collections.deque", _SEQUENCE_SLICE),
85+
"typing_extensions.Deque": ("collections.deque", "T"),
8986
"typing_extensions.DefaultDict": ("collections.defaultdict", _MAPPING_SLICE),
9087
"typing_extensions.ChainMap": ("collections.ChainMap", _MAPPING_SLICE),
9188
# One typing_extensions alias for a builtin
9289
"typing_extensions.Type": ("type", _TYPE_SLICE),
9390
# one typing_extensions alias for contextlib
9491
"typing_extensions.AsyncContextManager": (
9592
"contextlib.AbstractAsyncContextManager",
96-
_CONTEXTLIB_SLICE,
93+
"T",
9794
),
9895
}
9996

100-
# typing_extensions.ContextManager is omitted from the Y023 and Y027 collections - special-cased
101-
# We use `None` to signify that the object shouldn't be parameterised.
102-
_BAD_Y023_IMPORTS = {
103-
# collections.abc aliases
97+
# Objects you should import from collections.abc/typing instead of typing_extensions
98+
# A Python 2-compatible check
99+
_BAD_COLLECTIONSABC_Y023_IMPORTS = {
104100
"Awaitable": "T",
105-
"Coroutine": "YieldType, SendType, ReturnType",
101+
"Coroutine": _COROUTINE_SLICE,
106102
"AsyncIterable": "T",
107103
"AsyncIterator": "T",
108-
"AsyncGenerator": "YieldType, SendType",
109-
# typing aliases
110-
"Protocol": None,
111-
"runtime_checkable": None,
112-
"ClassVar": "T",
113-
"NewType": None,
114-
"overload": None,
115-
"Text": None,
116-
"NoReturn": None,
104+
"AsyncGenerator": _ASYNCGEN_SLICE,
117105
}
106+
_BAD_TYPING_Y023_IMPORTS = frozenset(
107+
{
108+
"Protocol",
109+
"runtime_checkable",
110+
"NewType",
111+
"overload",
112+
"Text",
113+
"NoReturn",
114+
# ClassVar deliberately omitted, as it's the only one in this group that should be parameterised
115+
# It is special-case elsewhere
116+
}
117+
)
118118

119+
# Objects you should import from collections.abc instead of typing(_extensions)
120+
# A Python 2-incompatible check
121+
# typing.AbstractSet is deliberately omitted (special-cased)
122+
# We use `None` to signify that the object shouldn't be parameterised.
119123
_BAD_Y027_IMPORTS = {
120-
"typing.ContextManager": ("contextlib.AbstractContextManager", _CONTEXTLIB_SLICE),
121-
"typing.OrderedDict": ("collections.OrderedDict", _MAPPING_SLICE),
122-
"typing_extensions.OrderedDict": ("collections.OrderedDict", _MAPPING_SLICE),
124+
"ByteString": None,
125+
"Collection": "T",
126+
"ItemsView": _MAPPING_SLICE,
127+
"KeysView": "KeyType",
128+
"Mapping": _MAPPING_SLICE,
129+
"MappingView": None,
130+
"MutableMapping": _MAPPING_SLICE,
131+
"MutableSequence": "T",
132+
"MutableSet": "T",
133+
"Sequence": "T",
134+
"ValuesView": "ValueType",
135+
"Iterable": "T",
136+
"Iterator": "T",
137+
"Generator": "YieldType, SendType, ReturnType",
138+
"Hashable": None,
139+
"Reversible": "T",
140+
"Sized": None,
141+
"Coroutine": _COROUTINE_SLICE,
142+
"AsyncGenerator": _ASYNCGEN_SLICE,
143+
"AsyncIterator": "T",
144+
"AsyncIterable": "T",
145+
"Awaitable": "T",
146+
"Callable": None,
147+
"Container": "T",
123148
}
124149

125150

@@ -586,6 +611,38 @@ def __init__(self, filename: Path | None = None) -> None:
586611
def __repr__(self) -> str:
587612
return f"{self.__class__.__name__}(filename={self.filename!r})"
588613

614+
@staticmethod
615+
def _get_Y023_error_message(object_name: str) -> str | None:
616+
"""
617+
Return the appropriate error message for a bad import/attribute-access from typing_extensions.
618+
Return `None` if it's an OK import/attribute-access.
619+
"""
620+
if object_name in _BAD_COLLECTIONSABC_Y023_IMPORTS:
621+
slice_contents = _BAD_COLLECTIONSABC_Y023_IMPORTS[object_name]
622+
suggestion = (
623+
f'"collections.abc.{object_name}[{slice_contents}]" '
624+
f'(or "typing.{object_name}[{slice_contents}]" '
625+
f"in Python 2-compatible code)"
626+
)
627+
bad_syntax = f'"typing_extensions.{object_name}[{slice_contents}]"'
628+
elif object_name in _BAD_TYPING_Y023_IMPORTS:
629+
suggestion = f'"typing.{object_name}"'
630+
bad_syntax = f'"typing_extensions.{object_name}"'
631+
elif object_name == "ClassVar":
632+
suggestion = '"typing.ClassVar[T]"'
633+
bad_syntax = '"typing_extensions.ClassVar[T]"'
634+
elif object_name == "ContextManager":
635+
suggestion = (
636+
'"contextlib.AbstractContextManager[T]" '
637+
'(or "typing.ContextManager[T]" '
638+
"in Python 2-compatible code)"
639+
)
640+
bad_syntax = '"typing_extensions.ContextManager[T]"'
641+
else:
642+
return None
643+
644+
return Y023.format(good_syntax=suggestion, bad_syntax=bad_syntax)
645+
589646
def _check_import_or_attribute(
590647
self, node: ast.Attribute | ast.ImportFrom, module_name: str, object_name: str
591648
) -> None:
@@ -600,35 +657,31 @@ def _check_import_or_attribute(
600657
)
601658

602659
# Y027 errors
603-
elif fullname in _BAD_Y027_IMPORTS:
604-
good_cls_name, params = _BAD_Y027_IMPORTS[fullname]
660+
elif module_name == "typing" and object_name in _BAD_Y027_IMPORTS:
661+
slice_contents = _BAD_Y027_IMPORTS[object_name]
662+
params = "" if slice_contents is None else f"[{slice_contents}]"
605663
error_message = Y027.format(
606-
good_syntax=f'"{good_cls_name}[{params}]"',
607-
bad_syntax=f'"{fullname}[{params}]"',
664+
good_syntax=f'"collections.abc.{object_name}{params}"',
665+
bad_syntax=f'"typing.{object_name}{params}"',
666+
)
667+
elif module_name in _TYPING_MODULES and object_name == "OrderedDict":
668+
error_message = Y027.format(
669+
good_syntax=f'"collections.OrderedDict[{_MAPPING_SLICE}]"',
670+
bad_syntax=f'"{fullname}[{_MAPPING_SLICE}]"',
671+
)
672+
elif fullname == "typing.ContextManager":
673+
error_message = Y027.format(
674+
good_syntax='"contextlib.AbstractContextManager[T]"',
675+
bad_syntax='"typing.ContextManager[T]"',
608676
)
609677

610678
# Y023 errors
611679
elif module_name == "typing_extensions":
612-
if object_name in _BAD_Y023_IMPORTS:
613-
slice_contents = _BAD_Y023_IMPORTS[object_name]
614-
params = "" if slice_contents is None else f"[{slice_contents}]"
615-
error_message = Y023.format(
616-
good_syntax=f'"typing.{object_name}{params}"',
617-
bad_syntax=f'"typing_extensions.{object_name}{params}"',
618-
)
619-
elif object_name == "ContextManager":
620-
suggested_syntax = (
621-
f'"contextlib.AbstractContextManager[{_CONTEXTLIB_SLICE}]" '
622-
f'(or "typing.ContextManager[{_CONTEXTLIB_SLICE}]" '
623-
f"in Python 2-compatible code)"
624-
)
625-
error_message = Y023.format(
626-
good_syntax=suggested_syntax,
627-
bad_syntax=f'"typing_extensions.ContextManager[{_CONTEXTLIB_SLICE}]"',
628-
)
629-
error_message += " (PEP 585 syntax)"
630-
else:
680+
analysis = self._get_Y023_error_message(object_name)
681+
if analysis is None:
631682
return
683+
else:
684+
error_message = analysis
632685

633686
# Y024 errors
634687
elif fullname == "collections.namedtuple":
@@ -639,7 +692,6 @@ def _check_import_or_attribute(
639692
error_message = Y037.format(
640693
old_syntax=fullname, example='"int | None" instead of "Optional[int]"'
641694
)
642-
643695
elif fullname == "typing.Union":
644696
error_message = Y037.format(
645697
old_syntax=fullname, example='"int | str" instead of "Union[int, str]"'
@@ -677,6 +729,11 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
677729
node=node, module_name=module_name, object_name=obj.name
678730
)
679731

732+
if module_name == "typing" and any(
733+
obj.name == "AbstractSet" for obj in imported_objects
734+
):
735+
self.error(node, Y038)
736+
680737
def _check_assignment_to_function(
681738
self, node: ast.Assign, function: ast.expr, object_name: str
682739
) -> None:
@@ -1531,3 +1588,4 @@ def parse_options(
15311588
Y035 = 'Y035 "{var}" in a stub file must have a value, as it has the same semantics as "{var}" at runtime.'
15321589
Y036 = "Y036 Badly defined {method_name} method: {details}"
15331590
Y037 = "Y037 Use PEP 604 union types instead of {old_syntax} (e.g. {example})."
1591+
Y038 = 'Y038 Use "from collections.abc import Set as AbstractSet" instead of "from typing import AbstractSet" (PEP 585 syntax)'

tests/classdefs.pyi

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import abc
22
import collections.abc
33
import typing
44
from abc import abstractmethod
5-
from typing import Any, AsyncIterator, Iterator, overload
5+
from collections.abc import AsyncIterator, Iterator
6+
from typing import Any, overload
67

7-
import typing_extensions
88
from _typeshed import Self
99
from typing_extensions import final
1010

@@ -69,14 +69,14 @@ class InvalidButPluginDoesNotCrash:
6969
class BadIterator1(Iterator[int]):
7070
def __iter__(self) -> Iterator[int]: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "_typeshed.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self: Self) -> Self: ..."
7171

72-
class BadIterator2(typing.Iterator[int]):
72+
class BadIterator2(typing.Iterator[int]): # Y027 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
7373
def __iter__(self) -> Iterator[int]: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "_typeshed.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self: Self) -> Self: ..."
7474

75-
class BadIterator3(typing_extensions.Iterator[int]):
75+
class BadIterator3(typing.Iterator[int]): # Y027 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
7676
def __iter__(self) -> collections.abc.Iterator[int]: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "_typeshed.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self: Self) -> Self: ..."
7777

7878
class BadAsyncIterator(collections.abc.AsyncIterator[str]):
79-
def __aiter__(self) -> typing.AsyncIterator[str]: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "_typeshed.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self: Self) -> Self: ..."
79+
def __aiter__(self) -> typing.AsyncIterator[str]: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "_typeshed.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self: Self) -> Self: ..." # Y027 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
8080

8181
class Abstract(Iterator[str]):
8282
@abstractmethod

tests/exit_methods.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import types
22
import typing
3+
from collections.abc import Awaitable
34
from types import TracebackType
45
from typing import ( # Y022 Use "type[MyClass]" instead of "typing.Type[MyClass]" (PEP 585 syntax)
56
Any,
6-
Awaitable,
77
Type,
88
)
99

0 commit comments

Comments
 (0)