Skip to content

Commit a528bc0

Browse files
JelleZijlstraAlexWaygoodhauntsaninja
authored
Add Y091: Protocol method arg should not be pos-or-kw (#442)
Fixes #441 Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Shantanu <[email protected]>
1 parent 06a2682 commit a528bc0

File tree

4 files changed

+54
-9
lines changed

4 files changed

+54
-9
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
New error codes:
6+
* Introduce Y091: Protocol method parameters should not be positional-or-keyword.
7+
38
## 24.9.0
49

510
Bugfixes

ERRORCODES.md

+1
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,4 @@ recommend only using `--extend-select`, never `--select`.
9595
| Code | Description | Code category
9696
|------|-------------|---------------
9797
| <a id="Y090" href="#Y090">Y090</a> | `tuple[int]` means "a tuple of length 1, in which the sole element is of type `int`". Consider using `tuple[int, ...]` instead, which means "a tuple of arbitrary (possibly 0) length, in which all elements are of type `int`". | Correctness
98+
| <a id="Y091" href="#Y091">Y091</a> | Protocol methods should not have positional-or-keyword parameters. Usually, a positional-only parameter is better.

pyi.py

+34-9
Original file line numberDiff line numberDiff line change
@@ -1742,7 +1742,9 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
17421742
self.generic_visit(node)
17431743
self._check_class_bases(node.bases)
17441744
self.enclosing_class_ctx = old_context
1745+
self.check_class_pass_and_ellipsis(node)
17451746

1747+
def check_class_pass_and_ellipsis(self, node: ast.ClassDef) -> None:
17461748
# empty class body should contain "..." not "pass"
17471749
if len(node.body) == 1:
17481750
statement = node.body[0]
@@ -2086,7 +2088,11 @@ def _check_class_method_for_bad_typevars(
20862088
):
20872089
self._Y019_error(method, cls_typevar)
20882090

2089-
def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
2091+
def check_self_typevars(
2092+
self,
2093+
node: ast.FunctionDef | ast.AsyncFunctionDef,
2094+
decorator_names: Container[str],
2095+
) -> None:
20902096
pos_or_keyword_args = node.args.posonlyargs + node.args.args
20912097

20922098
if not pos_or_keyword_args:
@@ -2100,12 +2106,6 @@ def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> N
21002106
if not isinstance(first_arg_annotation, (ast.Name, ast.Subscript)):
21012107
return
21022108

2103-
decorator_names = {
2104-
decorator.id
2105-
for decorator in node.decorator_list
2106-
if isinstance(decorator, ast.Name)
2107-
}
2108-
21092109
if "classmethod" in decorator_names or node.name == "__new__":
21102110
self._check_class_method_for_bad_typevars(
21112111
method=node,
@@ -2121,6 +2121,20 @@ def check_self_typevars(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> N
21212121
return_annotation=return_annotation,
21222122
)
21232123

2124+
def check_protocol_param_kinds(
2125+
self,
2126+
node: ast.FunctionDef | ast.AsyncFunctionDef,
2127+
decorator_names: Container[str],
2128+
) -> None:
2129+
if "staticmethod" in decorator_names:
2130+
relevant_params = node.args.args
2131+
else:
2132+
relevant_params = node.args.args[1:] # exclude "self"
2133+
for pos_or_kw in relevant_params:
2134+
if pos_or_kw.arg.startswith("__"):
2135+
continue
2136+
self.error(pos_or_kw, Y091.format(arg=pos_or_kw.arg, method=node.name))
2137+
21242138
@staticmethod
21252139
def _is_positional_pre_570_argname(name: str) -> bool:
21262140
# https://peps.python.org/pep-0484/#positional-only-arguments
@@ -2175,7 +2189,14 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
21752189

21762190
self._check_pep570_syntax_used_where_applicable(node)
21772191
if self.enclosing_class_ctx is not None:
2178-
self.check_self_typevars(node)
2192+
decorator_names = {
2193+
decorator.id
2194+
for decorator in node.decorator_list
2195+
if isinstance(decorator, ast.Name)
2196+
}
2197+
self.check_self_typevars(node, decorator_names)
2198+
if self.enclosing_class_ctx.is_protocol_class:
2199+
self.check_protocol_param_kinds(node, decorator_names)
21792200

21802201
def visit_arg(self, node: ast.arg) -> None:
21812202
if _is_NoReturn(node.annotation):
@@ -2413,5 +2434,9 @@ def parse_options(options: argparse.Namespace) -> None:
24132434
'"a tuple of length 1, in which the sole element is of type {typ!r}". '
24142435
'Perhaps you meant "{new}"?'
24152436
)
2437+
Y091 = (
2438+
'Y091 Argument "{arg}" to protocol method "{method}" should probably not be positional-or-keyword. '
2439+
"Make it positional-only, since usually you don't want to mandate a specific argument name"
2440+
)
24162441

2417-
DISABLED_BY_DEFAULT = ["Y090"]
2442+
DISABLED_BY_DEFAULT = ["Y090", "Y091"]

tests/protocol_arg.pyi

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# flags: --extend-select=Y091
2+
from typing import Protocol
3+
4+
class P(Protocol):
5+
def method1(self, arg: int) -> None: ... # Y091 Argument "arg" to protocol method "method1" should probably not be positional-or-keyword. Make it positional-only, since usually you don't want to mandate a specific argument name
6+
def method2(self, arg: str, /) -> None: ...
7+
def method3(self, *, arg: str) -> None: ...
8+
def method4(self, arg: int, /) -> None: ...
9+
def method5(self, arg: int, /, *, foo: str) -> None: ...
10+
# Ensure Y091 recognizes this as pos-only for the benefit of users still
11+
# using the old syntax.
12+
def method6(self, __arg: int) -> None: ... # Y063 Use PEP-570 syntax to indicate positional-only arguments
13+
@staticmethod
14+
def smethod1(arg: int) -> None: ... # Y091 Argument "arg" to protocol method "smethod1" should probably not be positional-or-keyword. Make it positional-only, since usually you don't want to mandate a specific argument name

0 commit comments

Comments
 (0)