Skip to content
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

Reduce metaclass-related false positives from Y034 #436

Merged
merged 4 commits into from
Nov 2, 2023
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
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