Skip to content

Commit a044940

Browse files
authored
Enforce PEP-570 syntax in stubs (#461)
1 parent 832be02 commit a044940

File tree

5 files changed

+110
-3
lines changed

5 files changed

+110
-3
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
New error codes:
6+
* Y063: Use [PEP 570 syntax](https://peps.python.org/pep-0570/) to mark
7+
positional-only arguments, rather than
8+
[the older Python 3.7-compatible syntax](https://peps.python.org/pep-0484/#positional-only-arguments)
9+
described in PEP 484.
10+
311
## 24.1.0
412

513
New error codes:

ERRORCODES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ The following warnings are currently emitted by default:
7676
| Y060 | Redundant inheritance from `Generic[]`. For example, `class Foo(Iterable[_T], Generic[_T]): ...` can be written more simply as `class Foo(Iterable[_T]): ...`.<br><br>To avoid false-positive errors, and to avoid complexity in the implementation, this check is deliberately conservative: it only flags classes where all subscripted bases have identical code inside their subscript slices. | Style
7777
| Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. Note that this warning is not emitted if Y062 is emitted for the same `Literal[]` slice. For example, `Literal[None, None, True, True]` only causes Y062 to be emitted. | Style
7878
| Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. | Redundant code
79+
| Y063 | Use [PEP 570 syntax](https://peps.python.org/pep-0570/) (e.g. `def foo(x: int, /) -> None: ...`) to denote positional-only arguments, rather than [the older Python 3.7-compatible syntax described in PEP 484](https://peps.python.org/pep-0484/#positional-only-arguments) (`def foo(__x: int) -> None: ...`, etc.). | Style
7980

8081
## Warnings disabled by default
8182

pyi.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,6 +2089,34 @@ def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> N
20892089
return_annotation=return_annotation,
20902090
)
20912091

2092+
@staticmethod
2093+
def _is_positional_pre_570_argname(name: str) -> bool:
2094+
# https://peps.python.org/pep-0484/#positional-only-arguments
2095+
return name.startswith("__") and len(name) >= 3 and not name.endswith("__")
2096+
2097+
def _check_pep570_syntax_used_where_applicable(
2098+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
2099+
) -> None:
2100+
if node.args.posonlyargs:
2101+
return
2102+
pos_or_kw_args = node.args.args
2103+
try:
2104+
first_param = pos_or_kw_args[0]
2105+
except IndexError:
2106+
return
2107+
if self.enclosing_class_ctx is None or any(
2108+
isinstance(decorator, ast.Name) and decorator.id == "staticmethod"
2109+
for decorator in node.decorator_list
2110+
):
2111+
uses_old_syntax = self._is_positional_pre_570_argname(first_param.arg)
2112+
else:
2113+
uses_old_syntax = self._is_positional_pre_570_argname(first_param.arg) or (
2114+
len(pos_or_kw_args) >= 2
2115+
and self._is_positional_pre_570_argname(pos_or_kw_args[1].arg)
2116+
)
2117+
if uses_old_syntax:
2118+
self.error(node, Y063)
2119+
20922120
def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
20932121
with self.in_function.enabled():
20942122
self.generic_visit(node)
@@ -2110,6 +2138,7 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
21102138
):
21112139
self.error(statement, Y010)
21122140

2141+
self._check_pep570_syntax_used_where_applicable(node)
21132142
if self.enclosing_class_ctx is not None:
21142143
self.check_self_typevars(node)
21152144

@@ -2336,6 +2365,7 @@ def parse_options(options: argparse.Namespace) -> None:
23362365
)
23372366
Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"'
23382367
Y062 = 'Y062 Duplicate "Literal[]" member "{}"'
2368+
Y063 = "Y063 Use PEP-570 syntax to indicate positional-only arguments"
23392369
Y090 = (
23402370
'Y090 "{original}" means '
23412371
'"a tuple of length 1, in which the sole element is of type {typ!r}". '

tests/exit_methods.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class GoodTwo:
2121
async def __aexit__(self, typ: Type[BaseException] | None, *args: object) -> bool: ...
2222

2323
class GoodThree:
24-
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
24+
def __exit__(self, typ: typing.Type[BaseException] | None, /, exc: BaseException | None, *args: object) -> None: ...
2525
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...
2626

2727
class GoodFour:
@@ -41,7 +41,7 @@ class GoodSix:
4141
def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ...
4242

4343
class GoodSeven:
44-
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
44+
def __exit__(self, typ: typing.Type[BaseException] | None, /, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
4545
def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
4646

4747

@@ -56,8 +56,12 @@ class BadTwo:
5656

5757
class BadThree:
5858
def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # Y036 Badly defined __exit__ method: The first arg in an __exit__ method should be annotated with "type[BaseException] | None" or "object", not "type[BaseException]"
59-
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # Y036 Badly defined __aexit__ method: The second arg in an __aexit__ method should be annotated with "BaseException | None" or "object", not "BaseException" # Y036 Badly defined __aexit__ method: The third arg in an __aexit__ method should be annotated with "types.TracebackType | None" or "object", not "TracebackType"
59+
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException, tb: TracebackType, /) -> bool | None: ... # Y036 Badly defined __aexit__ method: The second arg in an __aexit__ method should be annotated with "BaseException | None" or "object", not "BaseException" # Y036 Badly defined __aexit__ method: The third arg in an __aexit__ method should be annotated with "types.TracebackType | None" or "object", not "TracebackType"
6060

6161
class BadFour:
6262
def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # Y036 Badly defined __exit__ method: Star-args in an __exit__ method should be annotated with "object", not "list[str]" # Y036 Badly defined __exit__ method: The first arg in an __exit__ method should be annotated with "type[BaseException] | None" or "object", not "BaseException | None"
6363
def __aexit__(self, *args: Any) -> Awaitable[None]: ... # Y036 Badly defined __aexit__ method: Star-args in an __aexit__ method should be annotated with "object", not "Any"
64+
65+
class ThisExistsToTestInteractionBetweenY036AndY063:
66+
def __exit__(self, __typ, exc, tb, weird_extra_arg) -> None: ... # Y036 Badly defined __exit__ method: All arguments after the first 4 in an __exit__ method must have a default value # Y063 Use PEP-570 syntax to indicate positional-only arguments
67+
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # Y036 Badly defined __aexit__ method: The second arg in an __aexit__ method should be annotated with "BaseException | None" or "object", not "BaseException" # Y036 Badly defined __aexit__ method: The third arg in an __aexit__ method should be annotated with "types.TracebackType | None" or "object", not "TracebackType" # Y063 Use PEP-570 syntax to indicate positional-only arguments

tests/pep570.pyi

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# See https://peps.python.org/pep-0484/#positional-only-arguments
2+
# for the full details on which arguments using the older syntax should/shouldn't
3+
# be considered positional-only arguments by type checkers.
4+
from typing import Self
5+
6+
def bad(__x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
7+
def also_bad(__x: int, __y: str) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
8+
def still_bad(__x_: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
9+
10+
def no_args() -> None: ...
11+
def okay(__x__: int) -> None: ...
12+
# The first argument isn't positional-only, so logically the second can't be either:
13+
def also_okay(x: int, __y: str) -> None: ...
14+
def fine(x: bytes, /) -> None: ...
15+
def no_idea_why_youd_do_this(__x: int, /, __y: str) -> None: ...
16+
def cool(_x__: int) -> None: ...
17+
def also_cool(x__: int) -> None: ...
18+
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ...
19+
def _(_: int) -> None: ...
20+
21+
class Foo:
22+
def bad(__self) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
23+
@staticmethod
24+
def bad2(__self) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
25+
def bad3(__self, __x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
26+
def still_bad(self, __x_: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
27+
@staticmethod
28+
def this_is_bad_too(__x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
29+
@classmethod
30+
def not_good(cls, __foo: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
31+
32+
# The first non-self argument isn't positional-only, so logically the second can't be either:
33+
def okay1(self, x: int, __y: int) -> None: ...
34+
# Same here:
35+
@staticmethod
36+
def okay2(x: int, __y_: int) -> None: ...
37+
@staticmethod
38+
def no_args() -> int: ...
39+
def okay3(__self__, __x__: int, __y: str) -> None: ...
40+
def okay4(self, /) -> None: ...
41+
def okay5(self, x: int, /) -> None: ...
42+
def okay6(__self, /) -> None: ...
43+
def cool(_self__: int) -> None: ...
44+
def also_cool(self__: int) -> None: ...
45+
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ...
46+
def _(_: int) -> None: ...
47+
@classmethod
48+
def fine(cls, foo: int, /) -> None: ...
49+
50+
class Metaclass(type):
51+
@classmethod
52+
def __new__(mcls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
53+
54+
class Metaclass2(type):
55+
@classmethod
56+
def __new__(metacls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
57+
58+
class GoodMetaclass(type):
59+
@classmethod
60+
def __new__(mcls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ...
61+
62+
class GoodMetaclass2(type):
63+
@classmethod
64+
def __new__(metacls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ...

0 commit comments

Comments
 (0)