Skip to content

Commit 9a0e847

Browse files
committed
Bugfixes
1 parent b9e70c8 commit 9a0e847

File tree

3 files changed

+102
-9
lines changed

3 files changed

+102
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Behaviour changes:
1111
* Expand Y035 to cover `__match_args__` inside class definitions, as well as `__all__`
1212
in the global scope.
1313

14+
Bugfixes:
15+
* Improve Y026 check (regarding `typing.TypeAlias`) to reduce false-positive errors
16+
emitted when the plugin encountered variable aliases in a stub file.
17+
1418
## 22.3.0
1519

1620
Bugfixes:

pyi.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
import argparse
55
import ast
6+
import builtins
7+
import collections.abc
68
import logging
79
import optparse
810
import re
911
import sys
12+
import typing
1013
from collections import Counter
1114
from collections.abc import Container, Iterable, Iterator, Sequence
1215
from contextlib import contextmanager
@@ -16,6 +19,7 @@
1619
from itertools import chain
1720
from keyword import iskeyword
1821
from pathlib import Path
22+
from types import ModuleType
1923
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
2024

2125
from flake8 import checker # type: ignore[import]
@@ -548,6 +552,16 @@ def _is_assignment_which_must_have_a_value(
548552
)
549553

550554

555+
def _is_probably_class_from_module(var: str, *, module: ModuleType) -> bool:
556+
"""
557+
>>> _is_probably_class_from_module("AbstractSet", module=typing)
558+
True
559+
>>> _is_probably_class_from_module("int", module=builtins)
560+
True
561+
"""
562+
return isinstance(getattr(module, var, object()), (type, type(typing.List)))
563+
564+
551565
@dataclass
552566
class NestingCounter:
553567
"""Class to help the PyiVisitor keep track of internal state"""
@@ -574,6 +588,11 @@ def __init__(self, filename: Path | None = None) -> None:
574588
self.errors: list[Error] = []
575589
# Mapping of all private TypeVars/ParamSpecs/TypeVarTuples to the nodes where they're defined
576590
self.typevarlike_defs: dict[TypeVarInfo, ast.Assign] = {}
591+
# Mapping of all assignments in the file that could be type aliases
592+
# (This excludes assignments to function calls and ellipses, etc.)
593+
self.maybe_typealias_assignments: dict[str, ast.Assign] = {}
594+
# Set of all names and attributes that are used as annotations in the file
595+
self.all_annotations: set[str] = set()
577596
# Mapping of each name in the file to the no. of occurrences
578597
self.all_name_occurrences: Counter[str] = Counter()
579598
self.string_literals_allowed = NestingCounter()
@@ -743,12 +762,54 @@ def visit_Assign(self, node: ast.Assign) -> None:
743762
):
744763
return self._Y015_error(node)
745764

746-
# We avoid triggering Y026 for calls and = ... because there are various
747-
# unusual cases where assignment to the result of a call is legitimate
748-
# in stubs.
749-
if not is_special_assignment and not isinstance(
750-
assignment, (ast.Ellipsis, ast.Call)
751-
):
765+
if not is_special_assignment:
766+
self._check_for_type_aliases(node, target_name, assignment)
767+
768+
def _check_for_type_aliases(
769+
self, node: ast.Assign, target_name: str, assignment: ast.expr
770+
) -> None:
771+
"""
772+
Check for assignments that look like they could be type aliases,
773+
but aren't annotated with `typing(_extensions).TypeAlias`.
774+
775+
We avoid triggering Y026 for calls and = ... because there are various
776+
unusual cases where assignment to the result of a call is legitimate
777+
in stubs (`T = TypeVar("T")`, `List = _Alias()`, etc.).
778+
779+
Most assignments to names in builtins.py or typing.py will be type aliases,
780+
so special-case those. For other `ast.Attribute` and `ast.Name` nodes,
781+
avoid triggering Y026 now, as they might be variable aliases rather than
782+
type aliases.
783+
"""
784+
if isinstance(assignment, (ast.Ellipsis, ast.Call)):
785+
return
786+
787+
special_cased_modules = {builtins, typing, collections.abc}
788+
789+
if isinstance(assignment, ast.Attribute):
790+
if isinstance(assignment.value, ast.Name):
791+
module_name, assignment_name = assignment.value.id, assignment.attr
792+
for module in special_cased_modules:
793+
if module_name == module.__name__:
794+
if assignment_name in dir(
795+
module
796+
) and _is_probably_class_from_module(
797+
assignment_name, module=module
798+
):
799+
self.error(node, Y026)
800+
break
801+
else:
802+
self.maybe_typealias_assignments[target_name] = node
803+
elif isinstance(assignment, ast.Name):
804+
assignment_name = assignment.id
805+
for module in special_cased_modules:
806+
if assignment_name in dir(module):
807+
if _is_probably_class_from_module(assignment_name, module=module):
808+
self.error(node, Y026)
809+
break
810+
else:
811+
self.maybe_typealias_assignments[target_name] = node
812+
else:
752813
self.error(node, Y026)
753814

754815
def visit_Name(self, node: ast.Name) -> None:
@@ -792,7 +853,9 @@ def visit_Expr(self, node: ast.Expr) -> None:
792853
self.generic_visit(node)
793854

794855
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
795-
if _is_Final(node.annotation):
856+
annotation = node.annotation
857+
self.all_annotations.add(unparse(annotation))
858+
if _is_Final(annotation):
796859
with self.string_literals_allowed.enabled():
797860
self.generic_visit(node)
798861
return
@@ -808,7 +871,7 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
808871
self.error(node, Y035.format(var=target_name))
809872
return
810873
self.generic_visit(node)
811-
if _is_TypeAlias(node.annotation):
874+
if _is_TypeAlias(annotation):
812875
return
813876
if node.value and not isinstance(node.value, ast.Ellipsis):
814877
self._Y015_error(node)
@@ -1380,6 +1443,9 @@ def run(self, tree: ast.AST) -> Iterable[Error]:
13801443
def_node,
13811444
Y018.format(typevarlike_cls=cls_name, typevar_name=typevar_name),
13821445
)
1446+
for annotation in self.all_annotations:
1447+
if annotation in self.maybe_typealias_assignments:
1448+
self.error(self.maybe_typealias_assignments[annotation], Y026)
13831449
yield from self.errors
13841450

13851451

tests/aliases.pyi

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
from collections.abc import Mapping
2+
import builtins
3+
import collections
14
import typing
2-
from typing import ParamSpec as _ParamSpec, TypeAlias, TypedDict, _Alias
5+
from typing import ParamSpec as _ParamSpec, TypeAlias, TypedDict, Union, _Alias # Y037 Use PEP 604 union types instead of typing.Union (e.g. "int | str" instead of "Union[int, str]").
36

47
import typing_extensions
58

9+
T = builtins.str # Y026 Use typing_extensions.TypeAlias for type aliases
10+
U = typing.AbstractSet # Y026 Use typing_extensions.TypeAlias for type aliases
11+
V = Mapping # Y026 Use typing_extensions.TypeAlias for type aliases
612
X = int # Y026 Use typing_extensions.TypeAlias for type aliases
13+
Y = int | str # Y026 Use typing_extensions.TypeAlias for type aliases
14+
Z = Union[str, bytes] # Y026 Use typing_extensions.TypeAlias for type aliases
15+
716
X: TypeAlias = int
817
Y: typing.TypeAlias = int
918
Z: typing_extensions.TypeAlias = int
@@ -15,3 +24,17 @@ _P = _ParamSpec("_P")
1524
List = _Alias()
1625

1726
TD = TypedDict("TD", {"in": bool})
27+
28+
def foo() -> None: ...
29+
alias_for_foo_but_not_type_alias = foo
30+
31+
alias_for_function_from_builtins = dir
32+
33+
class Foo:
34+
def baz(self) -> None: ...
35+
36+
f: Foo = ...
37+
baz = f.baz
38+
39+
typealias_for_deque = collections.deque # Y026 Use typing_extensions.TypeAlias for type aliases
40+
uses_typealias_for_deque_in_annotation: typealias_for_deque

0 commit comments

Comments
 (0)