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

Fix bug where Y019 was not emitted on methods that use PEP-695 TypeVars #402

Merged
merged 1 commit into from
Jun 19, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Features:
* Support Python 3.12
* Support [PEP 695](https://peps.python.org/pep-0695/) syntax for declaring
type aliases
* Correctly emit Y019 errors for PEP-695 methods that are generic around a `TypeVar`
instead of returning `typing_extensions.Self`
* Introduce Y057: Do not use `typing.ByteString` or `collections.abc.ByteString`. These
types have unclear semantics, and are deprecated; use `typing_extensions.Buffer` or
a union such as `bytes | bytearray | memoryview` instead. See
Expand Down
25 changes: 23 additions & 2 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1788,12 +1788,31 @@ def _Y019_error(
) -> None:
cleaned_method = deepcopy(node)
cleaned_method.decorator_list.clear()
if sys.version_info >= (3, 12):
cleaned_method.type_params = [
param
for param in cleaned_method.type_params
if not (isinstance(param, ast.TypeVar) and param.name == typevar_name)
]
non_kw_only_args = cleaned_method.args.posonlyargs + cleaned_method.args.args
non_kw_only_args[0].annotation = None
new_syntax = _unparse_func_node(cleaned_method)
new_syntax = re.sub(rf"\b{typevar_name}\b", "Self", new_syntax)
self.error(node, Y019.format(typevar_name=typevar_name, new_syntax=new_syntax))

@staticmethod
def _is_likely_private_typevar(
method: ast.FunctionDef | ast.AsyncFunctionDef, tvar_name: str
) -> bool:
if tvar_name.startswith("_"):
return True
if sys.version_info < (3, 12):
return False
return any( # type: ignore[unreachable]
isinstance(param, ast.TypeVar) and param.name == tvar_name
for param in method.type_params
)

def _check_instance_method_for_bad_typevars(
self,
*,
Expand All @@ -1809,7 +1828,7 @@ def _check_instance_method_for_bad_typevars(

arg1_annotation_name = first_arg_annotation.id

if arg1_annotation_name.startswith("_"):
if self._is_likely_private_typevar(method, arg1_annotation_name):
self._Y019_error(method, arg1_annotation_name)

def _check_class_method_for_bad_typevars(
Expand All @@ -1833,7 +1852,9 @@ def _check_class_method_for_bad_typevars(
if not _is_name(first_arg_annotation.value, "type"):
return

if cls_typevar == return_annotation.id and cls_typevar.startswith("_"):
if cls_typevar == return_annotation.id and self._is_likely_private_typevar(
method, cls_typevar
):
self._Y019_error(method, cls_typevar)

def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
Expand Down
66 changes: 62 additions & 4 deletions tests/pep695_py312.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,68 @@
# Temporary workaround until pyflakes supports PEP 695:
# flags: --extend-ignore=F821

import typing
from collections.abc import Iterator
from typing import Any, NamedTuple, NoReturn, Protocol, Self, TypedDict

type lowercase_alias = str | int # Y042 Type aliases should use the CamelCase naming convention
type _LooksLikeATypeVarT = str | int # Y043 Bad name for a type alias (the "T" suffix implies a TypeVar)
type _Unused = str | int # Y047 Type alias "_Unused" is not used
# the F821 here is a pyflakes false positive;
# we can get rid of it when there's a pyflakes release that supports PEP 695
type _List[T] = list[T] # F821 undefined name 'T'
type _List[T] = list[T]

y: _List[int]

x: _LooksLikeATypeVarT

class GenericPEP695Class[T]:
def __init__(self, x: int) -> None:
self.x = x # Y010 Function body must contain only "..."
def __new__(cls, *args: Any, **kwargs: Any) -> GenericPEP695Class: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "GenericPEP695Class.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
def __repr__(self) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
def __eq__(self, other: Any) -> bool: ... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
def method(self) -> T: ...
... # Y013 Non-empty class body must not contain "..."
pass # Y012 Class body must not contain "pass"
def __exit__(self, *args: Any) -> None: ... # Y036 Badly defined __exit__ method: Star-args in an __exit__ method should be annotated with "object", not "Any"
async def __aexit__(self) -> None: ... # Y036 Badly defined __aexit__ method: If there are no star-args, there should be at least 3 non-keyword-only args in an __aexit__ method (excluding "self")
def never_call_me(self, arg: NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations

class GenericPEP695InheritingFromObject[T](object): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
x: T

class GenericPEP695InheritingFromIterator[T](Iterator[T]):
def __iter__(self) -> Iterator[T]: ... # Y034 "__iter__" methods in classes like "GenericPEP695InheritingFromIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "GenericPEP695InheritingFromIterator.__iter__", e.g. "def __iter__(self) -> Self: ..."

class PEP695BadBody[T]:
pass # Y009 Empty body should contain "...", not "pass"

class PEP695Docstring[T]:
"""Docstring""" # Y021 Docstrings should not be included in stubs
... # Y013 Non-empty class body must not contain "..."

class PEP695BadDunderNew[T]:
def __new__[S](cls: type[S], *args: Any, **kwargs: Any) -> S: ... # Y019 Use "typing_extensions.Self" instead of "S", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
def generic_instance_method[S](self: S) -> S: ... # Y019 Use "typing_extensions.Self" instead of "S", e.g. "def generic_instance_method(self) -> Self: ..."
Comment on lines +43 to +44
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y019 is not emitted on these two methods on main


class PEP695GoodDunderNew[T]:
def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...

class GenericNamedTuple[T](NamedTuple):
foo: T

class GenericTypedDict[T](TypedDict):
foo: T

class GenericTypingDotNamedTuple(typing.NamedTuple):
foo: T

class GenericTypingDotTypedDict(typing.TypedDict):
foo: T

type NoDuplicatesInThisUnion = GenericPEP695Class[str] | GenericPEP695Class[int]
type ThisHasDuplicates = GenericPEP695Class[str] | GenericPEP695Class[str] # Y016 Duplicate union member "GenericPEP695Class[str]"

class _UnusedPEP695Protocol[T](Protocol): # Y046 Protocol "_UnusedPEP695Protocol" is not used
x: T

class _UnusedPEP695TypedDict[T](TypedDict): # Y049 TypedDict "_UnusedPEP695TypedDict" is not used
x: T