Skip to content

Introduce Y022/Y023/Y024/Y025: Imports linting error codes #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ ignore = E302, E501, E701, W503, E203
max-line-length = 80
max-complexity = 12
select = B,C,E,F,W,Y,B9
per-file-ignores =
tests/imports.pyi: F401, F811
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ unreleased
* introduce Y016 (duplicate union member)
* support Python 3.10
* discontinue support for Python 3.6
* introduce Y022 (prefer stdlib classes over ``typing`` aliases)
* introduce Y023 (prefer ``typing`` over ``typing_extensions``)
* introduce Y024 (prefer ``typing.NamedTuple`` to ``collections.namedtuple``)
* introduce Y025 (always alias ``collections.abc.Set``)

20.10.0
~~~~~~~
Expand Down
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ currently emitted:
an instance of ``cls``, and ``__new__`` methods.
* Y020: Quoted annotations should never be used in stubs.
* Y021: Docstrings should not be included in stubs.
* Y022: Imports linting: use typing-module aliases to stdlib objects as little
as possible (e.g. ``builtins.list`` over ``typing.List``,
``collections.Counter`` over ``typing.Counter``, etc.).
* Y023: Where there is no detriment to backwards compatibility, import objects
such as ``ClassVar`` and ``NoReturn`` from ``typing`` rather than
``typing_extensions``.
* Y024: Use ``typing.NamedTuple`` instead of ``collections.namedtuple``, as it allows
for more precise type inference.
* Y023: Always alias ``collections.abc.Set`` when importing it, so as to avoid
confusion with ``builtins.set``.

The following warnings are disabled by default:

Expand Down
131 changes: 131 additions & 0 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,58 @@ class TypeVarInfo(NamedTuple):
name: str


# The collections import blacklist is for typing & typing_extensions
#
# OrderedDict is omitted:
# -- In Python 3, we'd rather import it from collections, not typing or typing_extensions
# -- But in Python 2, it cannot be imported from collections or typing, only from typing_extensions
#
# ChainMap does not exist in typing or typing_extensions in Python 2,
# so we can disallow importing it from anywhere except collections
_BAD_COLLECTIONS_ALIASES = {
"Counter": "Counter",
"Deque": "deque",
"DefaultDict": "defaultdict",
"ChainMap": "ChainMap",
}
_BAD_COLLECTIONS_ALIASES = {
alias: f'"collections.{cls}"' for alias, cls in _BAD_COLLECTIONS_ALIASES.items()
}

# Just-for-typing blacklist
_BAD_BUILTINS_ALIASES = {
alias: f'"builtins.{alias.lower()}"'
for alias in ("Dict", "Frozenset", "List", "Set", "Tuple", "Type")
}

# collections.abc aliases: none of these exist in typing or typing_extensions in Python 2,
# so we disallow importing them from typing_extensions.
#
# We can't disallow importing collections.abc aliases from typing yet due to mypy/pytype errors.
_BAD_COLLECTIONS_ABC_ALIASES = {
alias: f'"collections.abc.{alias}" or "typing.{alias}"'
for alias in (
"Awaitable",
"Coroutine",
"AsyncIterable",
"AsyncIterator",
"AsyncGenerator",
)
}
_TYPING_NOT_TYPING_EXTENSIONS = {
alias: f'"typing.{alias}"'
for alias in (
"Protocol",
"runtime_checkable",
"ClassVar",
"NewType",
"overload",
"Text",
"NoReturn",
)
}


class PyiAwareFlakesChecker(FlakesChecker):
def deferHandleNode(self, node, parent):
self.deferFunction(lambda: self.handleNode(node, parent))
Expand Down Expand Up @@ -187,6 +239,76 @@ def in_class(self) -> bool:
"""Determine whether we are inside a `class` statement"""
return bool(self._class_nesting)

def _check_typing_object(
self, node: ast.Attribute | ast.ImportFrom, module_name: str, object_name: str
) -> None:
if object_name in _BAD_COLLECTIONS_ALIASES:
error_code, blacklist = Y022, _BAD_COLLECTIONS_ALIASES
elif object_name == "AsyncContextManager":
error_code = Y022
blacklist = {
"AsyncContextManager": '"contextlib.AbstractAsyncContextManager"'
}
elif module_name == "typing":
if object_name not in _BAD_BUILTINS_ALIASES:
return
error_code, blacklist = Y022, _BAD_BUILTINS_ALIASES
elif object_name in _BAD_COLLECTIONS_ABC_ALIASES:
error_code, blacklist = Y023, _BAD_COLLECTIONS_ABC_ALIASES
elif object_name in _TYPING_NOT_TYPING_EXTENSIONS:
error_code, blacklist = Y023, _TYPING_NOT_TYPING_EXTENSIONS
elif object_name == "ContextManager":
error_code = Y023
blacklist = {
"ContextManager": '"contextlib.AbstractContextManager" or "typing.ContextManager"'
}
else:
return

error_message = error_code.format(
good_cls_name=blacklist[object_name],
bad_cls_alias=f"{module_name}.{object_name}",
)
self.error(node, error_message)

def visit_Attribute(self, node: ast.Attribute) -> None:
self.generic_visit(node)
thing = node.value
if not isinstance(thing, ast.Name):
return
thingname, attribute = thing.id, node.attr

if thingname == "collections" and attribute == "namedtuple":
return self.error(node, Y024)
elif thingname not in {"typing", "typing_extensions"}:
return

self._check_typing_object(
node=node, module_name=thingname, object_name=attribute
)

def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
module_name, imported_objects = node.module, node.names

if module_name == "collections.abc" and any(
obj.name == "Set" and obj.asname != "AbstractSet"
for obj in imported_objects
):
return self.error(node, Y025)

elif module_name == "collections" and any(
obj.name == "namedtuple" for obj in imported_objects
):
return self.error(node, Y024)

elif module_name not in {"typing", "typing_extensions"}:
return

for obj in imported_objects:
self._check_typing_object(
node=node, module_name=module_name, object_name=obj.name
)

def visit_Assign(self, node: ast.Assign) -> None:
if self.in_function:
# We error for unexpected things within functions separately.
Expand Down Expand Up @@ -262,6 +384,7 @@ def visit_Expr(self, node: ast.Expr) -> None:
self.generic_visit(node)

def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
self.generic_visit(node)
if isinstance(node.annotation, ast.Name) and node.annotation.id == "TypeAlias":
return
if node.value and not isinstance(node.value, ast.Ellipsis):
Expand Down Expand Up @@ -753,6 +876,14 @@ def should_warn(self, code):
Y019 = 'Y019 Use "_typeshed.Self" instead of "{typevar_name}"'
Y020 = "Y020 Quoted annotations should never be used in stubs"
Y021 = "Y021 Docstrings should not be included in stubs"
Y022 = 'Y022 Use {good_cls_name} instead of "{bad_cls_alias}"'
Y023 = 'Y023 Use {good_cls_name} instead of "{bad_cls_alias}"'
Y024 = 'Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"'
Y025 = (
'Y025 Use "from collections.abc import Set as AbstractSet" '
'to avoid confusion with "builtins.set"'
)
Y092 = "Y092 Top-level attribute must not have a default value"
Y093 = "Y093 Use typing_extensions.TypeAlias for type aliases"

DISABLED_BY_DEFAULT = [Y093]
4 changes: 2 additions & 2 deletions tests/del.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Union
from typing import Union


ManyStr = List[EitherStr]
ManyStr = list[EitherStr]
EitherStr = Union[str, bytes]


Expand Down
146 changes: 146 additions & 0 deletions tests/imports.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# NOTE: F401 & F811 are ignored in this file in the .flake8 config file

# GOOD IMPORTS
import typing
import typing_extensions
import collections
import collections.abc
from collections import ChainMap, Counter, OrderedDict, UserDict, UserList, UserString, defaultdict, deque
from collections.abc import (
Awaitable,
Coroutine,
AsyncIterable,
AsyncIterator,
AsyncGenerator,
Hashable,
Iterable,
Iterator,
Generator,
Reversible,
Set as AbstractSet,
Sized,
Container,
Callable,
Collection,
MutableSet,
MutableMapping,
MappingView,
KeysView,
ItemsView,
ValuesView,
Sequence,
MutableSequence,
ByteString
)

# Things that are of no use for stub files are intentionally omitted.
from typing import (
Any,
Callable,
ClassVar,
Generic,
Optional,
Protocol,
TypeVar,
Union,
AbstractSet,
ByteString,
Container,
Hashable,
ItemsView,
Iterable,
Iterator,
KeysView,
Mapping,
MappingView,
MutableMapping,
MutableSequence,
MutableSet,
Sequence,
Sized,
ValuesView,
Awaitable,
AsyncIterator,
AsyncIterable,
Coroutine,
Collection,
AsyncGenerator,
Reversible,
SupportsAbs,
SupportsBytes,
SupportsComplex,
SupportsFloat,
SupportsIndex,
SupportsInt,
SupportsRound,
Generator,
BinaryIO,
IO,
NamedTuple,
Match,
Pattern,
TextIO,
AnyStr,
NewType,
NoReturn,
overload,
ContextManager # ContextManager must be importable from typing (but not typing_extensions) for Python 2 compabitility
)
from typing_extensions import (
Concatenate,
Final,
ParamSpec,
SupportsIndex,
final,
Literal,
TypeAlias,
TypeGuard,
Annotated,
TypedDict,
OrderedDict # OrderedDict must be importable from typing_extensions (but not typing) for Python 2 compatibility
)


# BAD IMPORTS (Y022 code)
from typing import Dict # Y022 Use "builtins.dict" instead of "typing.Dict"
from typing import Counter # Y022 Use "collections.Counter" instead of "typing.Counter"
from typing import AsyncContextManager # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing.AsyncContextManager"
from typing import ChainMap # Y022 Use "collections.ChainMap" instead of "typing.ChainMap"
from typing_extensions import DefaultDict # Y022 Use "collections.defaultdict" instead of "typing_extensions.DefaultDict"
from typing_extensions import ChainMap # Y022 Use "collections.ChainMap" instead of "typing_extensions.ChainMap"
from typing_extensions import AsyncContextManager # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing_extensions.AsyncContextManager"

# BAD IMPORTS (Y023 code)
from typing_extensions import ClassVar # Y023 Use "typing.ClassVar" instead of "typing_extensions.ClassVar"
from typing_extensions import Awaitable # Y023 Use "collections.abc.Awaitable" or "typing.Awaitable" instead of "typing_extensions.Awaitable"
from typing_extensions import ContextManager # Y023 Use "contextlib.AbstractContextManager" or "typing.ContextManager" instead of "typing_extensions.ContextManager"

# BAD IMPORTS: OTHER
from collections import namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
from collections.abc import Set # Y025 Use "from collections.abc import Set as AbstractSet" to avoid confusion with "builtins.set"

# GOOD ATTRIBUTE ACCESS
foo: typing.SupportsIndex
bar: typing_extensions.Final[int]
baz: collections.abc.Sized
blah: collections.deque[int]

# BAD ATTRIBUTE ACCESS (Y022 code)
a: typing.Dict[str, int] # Y022 Use "builtins.dict" instead of "typing.Dict"
b: typing.Counter[float] # Y022 Use "collections.Counter" instead of "typing.Counter"
c: typing.AsyncContextManager[None] # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing.AsyncContextManager"
d: typing.ChainMap[int, str] # Y022 Use "collections.ChainMap" instead of "typing.ChainMap"
e: typing_extensions.DefaultDict[bytes, bytes] # Y022 Use "collections.defaultdict" instead of "typing_extensions.DefaultDict"
f: typing_extensions.ChainMap[str, str] # Y022 Use "collections.ChainMap" instead of "typing_extensions.ChainMap"
g: typing_extensions.AsyncContextManager[Any] # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing_extensions.AsyncContextManager"

# BAD ATTRIBUTE ACCESS (Y023 code)
class Foo:
attribute: typing_extensions.ClassVar[int] # Y023 Use "typing.ClassVar" instead of "typing_extensions.ClassVar"


h: typing_extensions.Awaitable[float] # Y023 Use "collections.abc.Awaitable" or "typing.Awaitable" instead of "typing_extensions.Awaitable"
i: typing_extensions.ContextManager[None] # Y023 Use "contextlib.AbstractContextManager" or "typing.ContextManager" instead of "typing_extensions.ContextManager"

# BAD ATTRIBUTE ACCESS: OTHER
j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
4 changes: 2 additions & 2 deletions tests/vanilla_flake8_not_clean_forward_refs.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# flags: --no-pyi-aware-file-checker
from typing import List, Union
from typing import Union


ManyStr = List[EitherStr] # F821 undefined name 'EitherStr'
ManyStr = list[EitherStr] # F821 undefined name 'EitherStr'
EitherStr = Union[str, bytes]


Expand Down