Skip to content

Commit

Permalink
Fix bug where Y019 was not emitted on methods that use PEP-695 TypeVa…
Browse files Browse the repository at this point in the history
…rs (#402)

And add a lot more tests that use PEP-695 syntax. Closes #391.
  • Loading branch information
AlexWaygood authored Jun 19, 2023
1 parent 7a10c90 commit e52d86d
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 6 deletions.
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: ..."

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

0 comments on commit e52d86d

Please sign in to comment.