Skip to content

Commit

Permalink
Reduce metaclass-related false positives from Y034 (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored Nov 2, 2023
1 parent 2f5b26e commit 0ccf764
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"""
Expand Down
28 changes: 27 additions & 1 deletion tests/classdefs.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,6 +15,7 @@ from collections.abc import (
Iterable,
Iterator,
)
from enum import EnumMeta
from typing import Any, Generic, TypeVar, overload

import typing_extensions
Expand Down Expand Up @@ -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]: ...
Expand Down

0 comments on commit 0ccf764

Please sign in to comment.