diff --git a/CHANGELOG.md b/CHANGELOG.md index 650b8e61..3229833b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ Other changes: * Y038 now flags `from typing_extensions import AbstractSet` as well as `from typing import AbstractSet`. * Y022 and Y037 now flag more imports from `typing_extensions`. +* Y034 now attempts to avoid flagging methods inside classes that inherit from + `builtins.type`, `abc.ABCMeta` and/or `enum.EnumMeta`. Classes that have one + or more of these as bases are metaclasses, and PEP 673 + [forbids the use of `typing(_extensions).Self`](https://peps.python.org/pep-0673/#valid-locations-for-self) + for metaclasses. While reliably determining whether a class is a metaclass in + all cases would be impossible for flake8-pyi, the new heuristics should + reduce the number of false positives from this check. ## 23.10.0 diff --git a/pyi.py b/pyi.py index eff7ccbd..cccdc31f 100644 --- a/pyi.py +++ b/pyi.py @@ -479,6 +479,11 @@ def _has_bad_hardcoded_returns( method: ast.FunctionDef | ast.AsyncFunctionDef, *, classdef: ast.ClassDef ) -> bool: """Return `True` if `function` should be rewritten with `typing_extensions.Self`.""" + # PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses. + # Do our best to avoid false positives here: + if _is_metaclass(classdef): + return False + # Much too complex for our purposes to worry # about overloaded functions or abstractmethods if any( @@ -818,6 +823,29 @@ def _is_enum_class(node: ast.ClassDef) -> bool: return any(_is_enum_base(base) for base in node.bases) +_COMMON_METACLASSES = { + "type": "builtins", + "ABCMeta": "abc", + "EnumMeta": "enum", + "EnumType": "enum", +} + + +def _is_metaclass_base(node: ast.expr) -> bool: + if isinstance(node, ast.Name): + return node.id in _COMMON_METACLASSES + return ( + isinstance(node, ast.Attribute) + and node.attr in _COMMON_METACLASSES + and _is_name(node.value, _COMMON_METACLASSES[node.attr]) + ) + + +def _is_metaclass(node: ast.ClassDef) -> bool: + """Best-effort attempt to determine if a class is a metaclass or not.""" + return any(_is_metaclass_base(base) for base in node.bases) + + @dataclass class NestingCounter: """Class to help the PyiVisitor keep track of internal state""" diff --git a/tests/classdefs.pyi b/tests/classdefs.pyi index 2cc44632..452d0de3 100644 --- a/tests/classdefs.pyi +++ b/tests/classdefs.pyi @@ -3,8 +3,9 @@ import abc import builtins import collections.abc +import enum import typing -from abc import abstractmethod +from abc import ABCMeta, abstractmethod from collections.abc import ( AsyncGenerator, AsyncIterable, @@ -14,6 +15,7 @@ from collections.abc import ( Iterable, Iterator, ) +from enum import EnumMeta from typing import Any, Generic, TypeVar, overload import typing_extensions @@ -126,6 +128,30 @@ class AsyncIteratorReturningSimpleAsyncGenerator2: class AsyncIteratorReturningComplexAsyncGenerator: def __aiter__(self) -> AsyncGenerator[str, int]: ... +class MetaclassInWhichSelfCannotBeUsed(type): + def __new__(cls) -> MetaclassInWhichSelfCannotBeUsed: ... + def __enter__(self) -> MetaclassInWhichSelfCannotBeUsed: ... + async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed: ... + def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed) -> MetaclassInWhichSelfCannotBeUsed: ... + +class MetaclassInWhichSelfCannotBeUsed2(EnumMeta): + def __new__(cls) -> MetaclassInWhichSelfCannotBeUsed2: ... + def __enter__(self) -> MetaclassInWhichSelfCannotBeUsed2: ... + async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed2: ... + def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed2) -> MetaclassInWhichSelfCannotBeUsed2: ... + +class MetaclassInWhichSelfCannotBeUsed3(enum.EnumType): + def __new__(cls) -> MetaclassInWhichSelfCannotBeUsed3: ... + def __enter__(self) -> MetaclassInWhichSelfCannotBeUsed3: ... + async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed3: ... + def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed3) -> MetaclassInWhichSelfCannotBeUsed3: ... + +class MetaclassInWhichSelfCannotBeUsed4(ABCMeta): + def __new__(cls) -> MetaclassInWhichSelfCannotBeUsed4: ... + def __enter__(self) -> MetaclassInWhichSelfCannotBeUsed4: ... + async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed4: ... + def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed4) -> MetaclassInWhichSelfCannotBeUsed4: ... + class Abstract(Iterator[str]): @abstractmethod def __iter__(self) -> Iterator[str]: ...