From 2b894c707bc334570ad5cc7e4e06f6b9d286d701 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 31 Oct 2023 10:53:41 +0000 Subject: [PATCH 1/4] Reduce metaclass-related false positives from Y034 --- CHANGELOG.md | 7 +++++++ pyi.py | 23 +++++++++++++++++++++++ tests/classdefs.pyi | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+) 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..4b419993 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,24 @@ 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"} + + +def _is_metaclass(node: ast.ClassDef) -> bool: + """Best-effort attempt to determine if a class is a metaclass or not.""" + for base in node.bases: + if isinstance(base, ast.Name): + if base.id in _COMMON_METACLASSES: + return True + elif isinstance(base, ast.Attribute): + if base.attr in _COMMON_METACLASSES and _is_name( + base.value, _COMMON_METACLASSES[base.attr] + ): + return True + else: + return False + + @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..7991c36e 100644 --- a/tests/classdefs.pyi +++ b/tests/classdefs.pyi @@ -14,6 +14,7 @@ from collections.abc import ( Iterable, Iterator, ) +from enum import EnumMeta from typing import Any, Generic, TypeVar, overload import typing_extensions @@ -126,6 +127,24 @@ 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(abc.ABCMeta): + def __new__(cls) -> MetaclassInWhichSelfCannotBeUsed3: ... + def __enter__(self) -> MetaclassInWhichSelfCannotBeUsed3: ... + async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed3: ... + def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed3) -> MetaclassInWhichSelfCannotBeUsed3: ... + class Abstract(Iterator[str]): @abstractmethod def __iter__(self) -> Iterator[str]: ... From 763ec9e64cc7a2f480af472eac5e114ac3b081da Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 31 Oct 2023 11:35:01 +0000 Subject: [PATCH 2/4] Improve readability --- pyi.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyi.py b/pyi.py index 4b419993..f78c4518 100644 --- a/pyi.py +++ b/pyi.py @@ -826,19 +826,19 @@ def _is_enum_class(node: ast.ClassDef) -> bool: _COMMON_METACLASSES = {"type": "builtins", "ABCMeta": "abc", "EnumMeta": "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 base.attr in _COMMON_METACLASSES + and _is_name(base.value, _COMMON_METACLASSES[base.attr]) + ) + + def _is_metaclass(node: ast.ClassDef) -> bool: """Best-effort attempt to determine if a class is a metaclass or not.""" - for base in node.bases: - if isinstance(base, ast.Name): - if base.id in _COMMON_METACLASSES: - return True - elif isinstance(base, ast.Attribute): - if base.attr in _COMMON_METACLASSES and _is_name( - base.value, _COMMON_METACLASSES[base.attr] - ): - return True - else: - return False + return any(_is_metaclass_base(base) for base in node.bases) @dataclass From 959d52c3f20efccaa73465d7115d4e34a20af977 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 31 Oct 2023 11:36:30 +0000 Subject: [PATCH 3/4] . --- pyi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyi.py b/pyi.py index f78c4518..0dea1f99 100644 --- a/pyi.py +++ b/pyi.py @@ -831,8 +831,8 @@ def _is_metaclass_base(node: ast.expr) -> bool: return node.id in _COMMON_METACLASSES return ( isinstance(node, ast.Attribute) - and base.attr in _COMMON_METACLASSES - and _is_name(base.value, _COMMON_METACLASSES[base.attr]) + and node.attr in _COMMON_METACLASSES + and _is_name(node.value, _COMMON_METACLASSES[node.attr]) ) From 6f76d33191387aee440f3e1d871fc0d58aeacbad Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 1 Nov 2023 18:52:57 +0000 Subject: [PATCH 4/4] Do `EnumType` as well. --- pyi.py | 7 ++++++- tests/classdefs.pyi | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pyi.py b/pyi.py index 0dea1f99..cccdc31f 100644 --- a/pyi.py +++ b/pyi.py @@ -823,7 +823,12 @@ 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"} +_COMMON_METACLASSES = { + "type": "builtins", + "ABCMeta": "abc", + "EnumMeta": "enum", + "EnumType": "enum", +} def _is_metaclass_base(node: ast.expr) -> bool: diff --git a/tests/classdefs.pyi b/tests/classdefs.pyi index 7991c36e..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, @@ -139,12 +140,18 @@ class MetaclassInWhichSelfCannotBeUsed2(EnumMeta): async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed2: ... def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed2) -> MetaclassInWhichSelfCannotBeUsed2: ... -class MetaclassInWhichSelfCannotBeUsed3(abc.ABCMeta): +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]: ...