Skip to content

Commit 19e0f3e

Browse files
authored
Add additional checks for match class patterns (#10587)
1 parent b0dd52e commit 19e0f3e

File tree

14 files changed

+195
-9
lines changed

14 files changed

+195
-9
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Book:
2+
__match_args__ = ("title", "year")
3+
4+
def __init__(self, title, year):
5+
self.title = title
6+
self.year = year
7+
8+
9+
def func(item: Book):
10+
match item:
11+
case Book(title=str(title)): # [match-class-bind-self]
12+
...
13+
case Book(year=int(year)): # [match-class-bind-self]
14+
...
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Book:
2+
__match_args__ = ("title", "year")
3+
4+
def __init__(self, title, year):
5+
self.title = title
6+
self.year = year
7+
8+
9+
def func(item: Book):
10+
match item:
11+
case Book(title=str() as title):
12+
...
13+
case Book(year=int() as year):
14+
...
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `Python documentation <https://docs.python.org/3/reference/compound_stmts.html#class-patterns>`_
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Book:
2+
__match_args__ = ("title", "year")
3+
4+
def __init__(self, title, year):
5+
self.title = title
6+
self.year = year
7+
8+
9+
def func(item: Book):
10+
match item:
11+
case Book("abc", 2000): # [match-class-positional-attributes]
12+
...
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Book:
2+
__match_args__ = ("title", "year")
3+
4+
def __init__(self, title, year):
5+
self.title = title
6+
self.year = year
7+
8+
9+
def func(item: Book):
10+
match item:
11+
case Book(title="abc", year=2000):
12+
...
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `Python documentation <https://docs.python.org/3/reference/compound_stmts.html#class-patterns>`_

doc/user_guide/checkers/features.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,12 @@ Match Statements checker Messages
694694
:multiple-class-sub-patterns (E1904): *Multiple sub-patterns for attribute %s*
695695
Emitted when there is more than one sub-pattern for a specific attribute in
696696
a class pattern.
697+
:match-class-bind-self (R1905): *Use '%s() as %s' instead*
698+
Match class patterns are faster if the name binding happens for the whole
699+
pattern and any lookup for `__match_args__` can be avoided.
700+
:match-class-positional-attributes (R1906): *Use keyword attributes instead of positional ones (%s)*
701+
Keyword attributes are more explicit and slightly faster since CPython can
702+
skip the `__match_args__` lookup.
697703

698704

699705
Method Args checker

doc/user_guide/messages/messages_overview.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@ All messages in the refactor category:
518518
refactor/inconsistent-return-statements
519519
refactor/literal-comparison
520520
refactor/magic-value-comparison
521+
refactor/match-class-bind-self
522+
refactor/match-class-positional-attributes
521523
refactor/no-classmethod-decorator
522524
refactor/no-else-break
523525
refactor/no-else-continue
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Add additional checks for suboptimal uses of class patterns in :keyword:`match`.
2+
3+
* :ref:`match-class-bind-self` is emitted if a name is bound to ``self`` instead of
4+
using an ``as`` pattern.
5+
* :ref:`match-class-positional-attributes` is emitted if a class pattern has positional
6+
attributes when keywords could be used.
7+
8+
Refs #10586

pylint/checkers/match_statements_checker.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919
from pylint.lint import PyLinter
2020

2121

22+
# List of builtin classes which match self
23+
# https://docs.python.org/3/reference/compound_stmts.html#class-patterns
24+
MATCH_CLASS_SELF_NAMES = {
25+
"builtins.bool",
26+
"builtins.bytearray",
27+
"builtins.bytes",
28+
"builtins.dict",
29+
"builtins.float",
30+
"builtins.frozenset",
31+
"builtins.int",
32+
"builtins.list",
33+
"builtins.set",
34+
"builtins.str",
35+
"builtins.tuple",
36+
}
37+
38+
2239
class MatchStatementChecker(BaseChecker):
2340
name = "match_statements"
2441
msgs = {
@@ -46,6 +63,19 @@ class MatchStatementChecker(BaseChecker):
4663
"Emitted when there is more than one sub-pattern for a specific "
4764
"attribute in a class pattern.",
4865
),
66+
"R1905": (
67+
"Use '%s() as %s' instead",
68+
"match-class-bind-self",
69+
"Match class patterns are faster if the name binding happens "
70+
"for the whole pattern and any lookup for `__match_args__` "
71+
"can be avoided.",
72+
),
73+
"R1906": (
74+
"Use keyword attributes instead of positional ones (%s)",
75+
"match-class-positional-attributes",
76+
"Keyword attributes are more explicit and slightly faster "
77+
"since CPython can skip the `__match_args__` lookup.",
78+
),
4979
}
5080

5181
@only_required_for_messages("invalid-match-args-definition")
@@ -86,6 +116,26 @@ def visit_match(self, node: nodes.Match) -> None:
86116
confidence=HIGH,
87117
)
88118

119+
@only_required_for_messages("match-class-bind-self")
120+
def visit_matchas(self, node: nodes.MatchAs) -> None:
121+
match node:
122+
case nodes.MatchAs(
123+
parent=nodes.MatchClass(cls=nodes.Name() as cls_name, patterns=[_]),
124+
name=nodes.AssignName(name=name),
125+
pattern=None,
126+
):
127+
inferred = safe_infer(cls_name)
128+
if (
129+
isinstance(inferred, nodes.ClassDef)
130+
and inferred.qname() in MATCH_CLASS_SELF_NAMES
131+
):
132+
self.add_message(
133+
"match-class-bind-self",
134+
node=node,
135+
args=(cls_name.name, name),
136+
confidence=HIGH,
137+
)
138+
89139
@staticmethod
90140
def get_match_args_for_class(node: nodes.NodeNG) -> list[str] | None:
91141
"""Infer __match_args__ from class name."""
@@ -95,6 +145,8 @@ def get_match_args_for_class(node: nodes.NodeNG) -> list[str] | None:
95145
try:
96146
match_args = inferred.getattr("__match_args__")
97147
except astroid.exceptions.NotFoundError:
148+
if inferred.qname() in MATCH_CLASS_SELF_NAMES:
149+
return ["<self>"]
98150
return None
99151

100152
match match_args:
@@ -124,6 +176,7 @@ def check_duplicate_sub_patterns(
124176
attrs.add(name)
125177

126178
@only_required_for_messages(
179+
"match-class-positional-attributes",
127180
"multiple-class-sub-patterns",
128181
"too-many-positional-sub-patterns",
129182
)
@@ -144,6 +197,22 @@ def visit_matchclass(self, node: nodes.MatchClass) -> None:
144197
)
145198
return
146199

200+
inferred = safe_infer(node.cls)
201+
if not (
202+
isinstance(inferred, nodes.ClassDef)
203+
and (
204+
inferred.qname() in MATCH_CLASS_SELF_NAMES
205+
or "tuple" in inferred.basenames
206+
)
207+
):
208+
attributes = [f"'{attr}'" for attr in match_args[: len(node.patterns)]]
209+
self.add_message(
210+
"match-class-positional-attributes",
211+
node=node,
212+
args=(", ".join(attributes),),
213+
confidence=INFERENCE,
214+
)
215+
147216
for i in range(len(node.patterns)):
148217
name = match_args[i]
149218
self.check_duplicate_sub_patterns(name, node, attrs=attrs, dups=dups)

0 commit comments

Comments
 (0)