Skip to content

Commit

Permalink
Enforce PEP-570 syntax in stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jan 5, 2024
1 parent a113980 commit cc7f42b
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

New error codes:
* Y062: Disallow duplicate elements inside `Literal[]` slices.
* Y063: Use [PEP 570 syntax](https://peps.python.org/pep-0570/) to mark
positional-only arguments, rather than
[the older Python 3.7-compatible syntax](https://peps.python.org/pep-0484/#positional-only-arguments)
described in PEP 484.

Other features:
* Support flake8>=7.0.0
Expand Down
1 change: 1 addition & 0 deletions ERRORCODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ The following warnings are currently emitted by default:
| 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
| 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
| Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. | Redundant code
| 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

## Warnings disabled by default

Expand Down
48 changes: 40 additions & 8 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2054,7 +2054,11 @@ def _check_class_method_for_bad_typevars(
):
self._Y019_error(method, cls_typevar)

def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
def check_self_typevars(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef,
decorator_names: AbstractSet[str],
) -> None:
pos_or_keyword_args = node.args.posonlyargs + node.args.args

if not pos_or_keyword_args:
Expand All @@ -2068,12 +2072,6 @@ def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> N
if not isinstance(first_arg_annotation, (ast.Name, ast.Subscript)):
return

decorator_names = {
decorator.id
for decorator in node.decorator_list
if isinstance(decorator, ast.Name)
}

if "classmethod" in decorator_names or node.name == "__new__":
self._check_class_method_for_bad_typevars(
method=node,
Expand All @@ -2089,6 +2087,33 @@ def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> N
return_annotation=return_annotation,
)

@staticmethod
def _is_positional_pre_570_argname(name: str) -> bool:
# https://peps.python.org/pep-0484/#positional-only-arguments
return name.startswith("__") and len(name) >= 3 and not name.endswith("__")

def _check_pep570_syntax_used_where_applicable(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef,
decorator_names: AbstractSet[str],
) -> None:
if node.args.posonlyargs:
return
pos_or_kw_args = node.args.args
try:
first_param = pos_or_kw_args[0]
except IndexError:
return
if self.enclosing_class_ctx is None or "staticmethod" in decorator_names:
uses_old_syntax = self._is_positional_pre_570_argname(first_param.arg)
else:
uses_old_syntax = self._is_positional_pre_570_argname(first_param.arg) or (
len(pos_or_kw_args) >= 2
and self._is_positional_pre_570_argname(pos_or_kw_args[1].arg)
)
if uses_old_syntax:
self.error(node, Y063)

def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
with self.in_function.enabled():
self.generic_visit(node)
Expand All @@ -2110,8 +2135,14 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
):
self.error(statement, Y010)

decorator_names = {
decorator.id
for decorator in node.decorator_list
if isinstance(decorator, ast.Name)
}
self._check_pep570_syntax_used_where_applicable(node, decorator_names)
if self.enclosing_class_ctx is not None:
self.check_self_typevars(node)
self.check_self_typevars(node, decorator_names)

def visit_arg(self, node: ast.arg) -> None:
if _is_NoReturn(node.annotation):
Expand Down Expand Up @@ -2336,6 +2367,7 @@ def parse_options(options: argparse.Namespace) -> None:
)
Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"'
Y062 = 'Y062 Duplicate "Literal[]" member "{}"'
Y063 = "Y063 Use PEP-570 syntax to indicate positional-only arguments"
Y090 = (
'Y090 "{original}" means '
'"a tuple of length 1, in which the sole element is of type {typ!r}". '
Expand Down
6 changes: 3 additions & 3 deletions tests/exit_methods.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class GoodTwo:
async def __aexit__(self, typ: Type[BaseException] | None, *args: object) -> bool: ...

class GoodThree:
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...

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

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


Expand All @@ -56,7 +56,7 @@ class BadTwo:

class BadThree:
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]"
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"
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

class BadFour:
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"
Expand Down
34 changes: 34 additions & 0 deletions tests/pep570.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://peps.python.org/pep-0484/#positional-only-arguments
# for the full details on which arguments using the older syntax should/shouldn't
# be considered positional-only arguments by type checkers.

def no_args() -> None: ...

def bad(__x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
def also_bad(__x: int, __y: str) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
def still_bad(__x_: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments

def okay(__x__: int) -> None: ...
# The first argument isn't positional-only, so logically the second can't be either:
def also_okay(x: int, __y: str) -> None: ...
def fine(x: bytes, /) -> None: ...
def no_idea_why_youd_do_this(__x: int, /, __y: str) -> None: ...

class Foo:
def bad(__self) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
@staticmethod
def bad2(__self) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
def bad3(__self, __x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
def still_bad(self, __x_: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
@staticmethod
def this_is_bad_too(__x: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments

# The first non-self argument isn't positional-only, so logically the second can't be either:
def okay1(self, x: int, __y: int) -> None: ...
# Same here:
@staticmethod
def okay2(x: int, __y_: int) -> None: ...
def okay3(__self__, __x__: int, __y: str) -> None: ...
def okay4(self, /) -> None: ...
def okay5(self, x: int, /) -> None: ...
def okay6(__self, /) -> None: ...

0 comments on commit cc7f42b

Please sign in to comment.