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 all 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.
* Y025: 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
128 changes: 128 additions & 0 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,59 @@ class TypeVarInfo(NamedTuple):
name: str


# OrderedDict is omitted from this blacklist:
# -- 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_Y022_IMPORTS = {
# typing aliases for collections
"typing.Counter": "collections.Counter",
"typing.Deque": "collections.deque",
"typing.DefaultDict": "collections.defaultdict",
"typing.ChainMap": "collections.ChainMap",
# typing aliases for builtins
"typing.Dict": "builtins.dict",
"typing.FrozenSet": "builtins.frozenset",
"typing.List": "builtins.list",
"typing.Set": "builtins.set",
"typing.Tuple": "builtins.tuple",
"typing.Type": "builtins.type",
# One typing alias for contextlib
"typing.AsyncContextManager": "contextlib.AbstractAsyncContextManager",
# typing_extensions aliases for collections
"typing_extensions.Counter": "collections.Counter",
"typing_extensions.Deque": "collections.deque",
"typing_extensions.DefaultDict": "collections.defaultdict",
"typing_extensions.ChainMap": "collections.ChainMap",
# One typing_extensions alias for a builtin
"typing_extensions.Type": "builtins.type",
# one typing_extensions alias for contextlib
"typing_extensions.AsyncContextManager": "contextlib.AbstractAsyncContextManager",
}

# typing_extensions.ContextManager is omitted from this collection - special-cased
_BAD_Y023_IMPORTS = frozenset(
{
# collections.abc aliases
"Awaitable",
"Coroutine",
"AsyncIterable",
"AsyncIterator",
"AsyncGenerator",
# typing aliases
"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 +240,73 @@ def in_class(self) -> bool:
"""Determine whether we are inside a `class` statement"""
return bool(self._class_nesting)

def _check_import_or_attribute(
self, node: ast.Attribute | ast.ImportFrom, module_name: str, object_name: str
) -> None:
fullname = f"{module_name}.{object_name}"

# Y022 errors
if fullname in _BAD_Y022_IMPORTS:
error_message = Y022.format(
good_cls_name=f'"{_BAD_Y022_IMPORTS[fullname]}"',
bad_cls_alias=fullname,
)

# Y023 errors
elif module_name == "typing_extensions":
if object_name in _BAD_Y023_IMPORTS:
error_message = Y023.format(
good_cls_name=f'"typing.{object_name}"',
bad_cls_alias=f"typing_extensions.{object_name}",
)
elif object_name == "ContextManager":
suggested_syntax = (
'"contextlib.AbstractContextManager" '
'(or "typing.ContextManager" in Python 2-compatible code)'
)
error_message = Y023.format(
good_cls_name=suggested_syntax,
bad_cls_alias="typing_extensions.ContextManager",
)
else:
return

# Y024 errors
elif fullname == "collections.namedtuple":
error_message = Y024

else:
return

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

self._check_import_or_attribute(
node=node, module_name=thing.id, object_name=node.attr
)

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

if module_name is None:
return

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

for obj in imported_objects:
self._check_import_or_attribute(
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 +382,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 +874,13 @@ 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"'
)
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
173 changes: 173 additions & 0 deletions tests/imports.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 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 Type # Y022 Use "builtins.type" instead of "typing_extensions.Type"
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 "typing.Awaitable" instead of "typing_extensions.Awaitable"
from typing_extensions import ContextManager # Y023 Use "contextlib.AbstractContextManager" (or "typing.ContextManager" in Python 2-compatible code) 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

@typing_extensions.final
def bar(arg: collections.abc.Sized) -> typing_extensions.Literal[True]:
...


class Fish:
blah: collections.deque[int]

def method(self, arg: typing.SupportsInt = ...) -> None:
...


# BAD ATTRIBUTE ACCESS (Y022 code)
a: typing.Dict[str, int] # Y022 Use "builtins.dict" instead of "typing.Dict"

def func1() -> typing.Counter[float]: # Y022 Use "collections.Counter" instead of "typing.Counter"
...


def func2(c: typing.AsyncContextManager[None]) -> None: # Y022 Use "contextlib.AbstractAsyncContextManager" instead of "typing.AsyncContextManager"
...


def func3(d: typing.ChainMap[int, str] = ...) -> None: # Y022 Use "collections.ChainMap" instead of "typing.ChainMap"
...


class Spam:
def meth1() -> typing_extensions.DefaultDict[bytes, bytes]: # Y022 Use "collections.defaultdict" instead of "typing_extensions.DefaultDict"
...

def meth2(self, f: typing_extensions.ChainMap[str, str]) -> None: # Y022 Use "collections.ChainMap" instead of "typing_extensions.ChainMap"
...

def meth3(self, g: typing_extensions.AsyncContextManager[Any] = ...) -> None: # 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 "typing.Awaitable" instead of "typing_extensions.Awaitable"
i: typing_extensions.ContextManager[None] # Y023 Use "contextlib.AbstractContextManager" (or "typing.ContextManager" in Python 2-compatible code) 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