From ffed4b5311efce9306088f98e8e5d419a27d26d5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:26:21 +0200 Subject: [PATCH 1/6] Use match statement in checkers (1) --- pylint/checkers/match_statements_checker.py | 4 +- pylint/extensions/_check_docs_utils.py | 35 +++++++------- pylint/extensions/code_style.py | 13 +++--- pylint/extensions/for_any_all.py | 13 +++--- pylint/extensions/private_import.py | 52 ++++++++++----------- pyproject.toml | 5 ++ 6 files changed, 65 insertions(+), 57 deletions(-) diff --git a/pylint/checkers/match_statements_checker.py b/pylint/checkers/match_statements_checker.py index 18fd8a726a..d2139f66d4 100644 --- a/pylint/checkers/match_statements_checker.py +++ b/pylint/checkers/match_statements_checker.py @@ -37,13 +37,13 @@ def visit_match(self, node: nodes.Match) -> None: """ for idx, case in enumerate(node.cases): match case.pattern: - case nodes.MatchAs(pattern=None, name=nodes.AssignName()) if ( + case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=n)) if ( idx < len(node.cases) - 1 ): self.add_message( "bare-name-capture-pattern", node=case.pattern, - args=case.pattern.name.name, + args=n, confidence=HIGH, ) diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index 9b4b3e0db6..a6c378b476 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -133,23 +133,24 @@ def possible_exc_types(node: nodes.NodeNG) -> set[nodes.ClassDef]: except astroid.InferenceError: pass else: - target = _get_raise_target(node) - if isinstance(target, nodes.ClassDef): - exceptions = [target] - elif isinstance(target, nodes.FunctionDef): - for ret in target.nodes_of_class(nodes.Return): - if ret.value is None: - continue - if ret.frame() != target: - # return from inner function - ignore it - continue - - val = utils.safe_infer(ret.value) - if val and utils.inherit_from_std_ex(val): - if isinstance(val, nodes.ClassDef): - exceptions.append(val) - elif isinstance(val, astroid.Instance): - exceptions.append(val.getattr("__class__")[0]) + match target := _get_raise_target(node): + case nodes.ClassDef(): + exceptions = [target] + case nodes.FunctionDef(): + for ret in target.nodes_of_class(nodes.Return): + if ret.value is None: + continue + if ret.frame() != target: + # return from inner function - ignore it + continue + + val = utils.safe_infer(ret.value) + if val and utils.inherit_from_std_ex(val): + match val: + case nodes.ClassDef(): + exceptions.append(val) + case astroid.Instance(): + exceptions.append(val.getattr("__class__")[0]) try: return { diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index d187cd7c5c..51fb3c89ad 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -309,12 +309,13 @@ def _check_ignore_assignment_expr_suggestion( if isinstance(node.test, nodes.Compare): next_if_node: nodes.If | None = None next_sibling = node.next_sibling() - if len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If): - # elif block - next_if_node = node.orelse[0] - elif isinstance(next_sibling, nodes.If): - # separate if block - next_if_node = next_sibling + match (node.orelse, next_sibling): + case [[nodes.If() as next_if_node], _]: + # elif block + pass + case [_, nodes.If() as next_if_node]: + # separate if block + pass if ( # pylint: disable=too-many-boolean-expressions next_if_node is not None diff --git a/pylint/extensions/for_any_all.py b/pylint/extensions/for_any_all.py index 2369a595dc..44644e6c19 100644 --- a/pylint/extensions/for_any_all.py +++ b/pylint/extensions/for_any_all.py @@ -147,12 +147,13 @@ def _build_suggested_string(node: nodes.For, final_return_bool: bool) -> str: loop_iter = node.iter.as_string() test_node = next(node.body[0].get_children()) - if isinstance(test_node, nodes.UnaryOp) and test_node.op == "not": - # The condition is negated. Advance the node to the operand and modify the suggestion - test_node = test_node.operand - suggested_function = "all" if final_return_bool else "not all" - else: - suggested_function = "not any" if final_return_bool else "any" + match test_node: + case nodes.UnaryOp(op="not"): + # The condition is negated. Advance the node to the operand and modify the suggestion + test_node = test_node.operand + suggested_function = "all" if final_return_bool else "not all" + case _: + suggested_function = "not any" if final_return_bool else "any" test = test_node.as_string() return f"{suggested_function}({test} for {loop_var} in {loop_iter})" diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index 962bfe1f1c..24488835fb 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -147,14 +147,15 @@ def _populate_type_annotations( if isinstance(usage_node, nodes.AssignName) and isinstance( usage_node.parent, (nodes.AnnAssign, nodes.Assign) ): - assign_parent = usage_node.parent - if isinstance(assign_parent, nodes.AnnAssign): - name_assignments.append(assign_parent) - private_name = self._populate_type_annotations_annotation( - usage_node.parent.annotation, all_used_type_annotations - ) - elif isinstance(assign_parent, nodes.Assign): - name_assignments.append(assign_parent) + match assign_parent := usage_node.parent: + case nodes.AnnAssign(): + name_assignments.append(assign_parent) + private_name = self._populate_type_annotations_annotation( + assign_parent.annotation, + all_used_type_annotations, + ) + case nodes.Assign(): + name_assignments.append(assign_parent) if isinstance(usage_node, nodes.FunctionDef): self._populate_type_annotations_function( @@ -194,24 +195,23 @@ def _populate_type_annotations_annotation( """Handles the possibility of an annotation either being a Name, i.e. just type, or a Subscript e.g. `Optional[type]` or an Attribute, e.g. `pylint.lint.linter`. """ - if isinstance(node, nodes.Name) and node.name not in all_used_type_annotations: - all_used_type_annotations[node.name] = True - return node.name # type: ignore[no-any-return] - if isinstance(node, nodes.Subscript): # e.g. Optional[List[str]] - # slice is the next nested type - self._populate_type_annotations_annotation( - node.slice, all_used_type_annotations - ) - # value is the current type name: could be a Name or Attribute - return self._populate_type_annotations_annotation( - node.value, all_used_type_annotations - ) - if isinstance(node, nodes.Attribute): - # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level - # up, could be another attribute - return self._populate_type_annotations_annotation( - node.expr, all_used_type_annotations - ) + match node: + case nodes.Name(name=n) if n not in all_used_type_annotations: + all_used_type_annotations[n] = True + return n # type: ignore[no-any-return] + case nodes.Subscript(slice=s, value=v): + # slice is the next nested type + self._populate_type_annotations_annotation(s, all_used_type_annotations) + # value is the current type name: could be a Name or Attribute + return self._populate_type_annotations_annotation( + v, all_used_type_annotations + ) + case nodes.Attribute(expr=e): + # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level + # up, could be another attribute + return self._populate_type_annotations_annotation( + e, all_used_type_annotations + ) return None @staticmethod diff --git a/pyproject.toml b/pyproject.toml index ed32d13599..d36a6f93c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,6 +247,11 @@ scripts_are_modules = true warn_unused_ignores = true show_error_codes = true enable_error_code = "ignore-without-code" +disable_error_code = [ + # TODO: remove once match false-positives are fixed, probably in v1.18 + # https://github.com/python/mypy/pull/19708 + "has-type", +] strict = true # TODO: Remove this once pytest has annotations disallow_untyped_decorators = false From ed93da506d0b4d606377f961b2197bdbb380f6d4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:11:15 +0200 Subject: [PATCH 2/6] Revert mypy disable --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d36a6f93c2..ed32d13599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,11 +247,6 @@ scripts_are_modules = true warn_unused_ignores = true show_error_codes = true enable_error_code = "ignore-without-code" -disable_error_code = [ - # TODO: remove once match false-positives are fixed, probably in v1.18 - # https://github.com/python/mypy/pull/19708 - "has-type", -] strict = true # TODO: Remove this once pytest has annotations disallow_untyped_decorators = false From 3b460ee7e0f7611ce09597b012975964b02d715d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:16:18 +0200 Subject: [PATCH 3/6] Revert questionable tuple match --- pylint/extensions/code_style.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 51fb3c89ad..d187cd7c5c 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -309,13 +309,12 @@ def _check_ignore_assignment_expr_suggestion( if isinstance(node.test, nodes.Compare): next_if_node: nodes.If | None = None next_sibling = node.next_sibling() - match (node.orelse, next_sibling): - case [[nodes.If() as next_if_node], _]: - # elif block - pass - case [_, nodes.If() as next_if_node]: - # separate if block - pass + if len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If): + # elif block + next_if_node = node.orelse[0] + elif isinstance(next_sibling, nodes.If): + # separate if block + next_if_node = next_sibling if ( # pylint: disable=too-many-boolean-expressions next_if_node is not None From b7938b18848cfb2d9840d82e6f27f0fe4bc479f7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:17:53 +0200 Subject: [PATCH 4/6] Use better variable names --- pylint/checkers/match_statements_checker.py | 4 ++-- pylint/extensions/private_import.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pylint/checkers/match_statements_checker.py b/pylint/checkers/match_statements_checker.py index d2139f66d4..99132fa964 100644 --- a/pylint/checkers/match_statements_checker.py +++ b/pylint/checkers/match_statements_checker.py @@ -37,13 +37,13 @@ def visit_match(self, node: nodes.Match) -> None: """ for idx, case in enumerate(node.cases): match case.pattern: - case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=n)) if ( + case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=name)) if ( idx < len(node.cases) - 1 ): self.add_message( "bare-name-capture-pattern", node=case.pattern, - args=n, + args=name, confidence=HIGH, ) diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index 24488835fb..ccedc7c01f 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -196,21 +196,21 @@ def _populate_type_annotations_annotation( or a Subscript e.g. `Optional[type]` or an Attribute, e.g. `pylint.lint.linter`. """ match node: - case nodes.Name(name=n) if n not in all_used_type_annotations: - all_used_type_annotations[n] = True - return n # type: ignore[no-any-return] - case nodes.Subscript(slice=s, value=v): + case nodes.Name(name=name) if name not in all_used_type_annotations: + all_used_type_annotations[name] = True + return name # type: ignore[no-any-return] + case nodes.Subscript(slice=s, value=value): # slice is the next nested type self._populate_type_annotations_annotation(s, all_used_type_annotations) # value is the current type name: could be a Name or Attribute return self._populate_type_annotations_annotation( - v, all_used_type_annotations + value, all_used_type_annotations ) - case nodes.Attribute(expr=e): + case nodes.Attribute(expr=expr): # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level # up, could be another attribute return self._populate_type_annotations_annotation( - e, all_used_type_annotations + expr, all_used_type_annotations ) return None From e7344577bcb712758c71ad0dd1fbbd0ddbcb8e77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:02:41 +0200 Subject: [PATCH 5/6] Remove unnecessary variable binding --- pylint/extensions/private_import.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index ccedc7c01f..c0ef6a5ae5 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -196,21 +196,23 @@ def _populate_type_annotations_annotation( or a Subscript e.g. `Optional[type]` or an Attribute, e.g. `pylint.lint.linter`. """ match node: - case nodes.Name(name=name) if name not in all_used_type_annotations: - all_used_type_annotations[name] = True - return name # type: ignore[no-any-return] - case nodes.Subscript(slice=s, value=value): + case nodes.Name() if node.name not in all_used_type_annotations: + all_used_type_annotations[node.name] = True + return node.name # type: ignore[no-any-return] + case nodes.Subscript(): # slice is the next nested type - self._populate_type_annotations_annotation(s, all_used_type_annotations) + self._populate_type_annotations_annotation( + node.slice, all_used_type_annotations + ) # value is the current type name: could be a Name or Attribute return self._populate_type_annotations_annotation( - value, all_used_type_annotations + node.value, all_used_type_annotations ) - case nodes.Attribute(expr=expr): + case nodes.Attribute(): # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level # up, could be another attribute return self._populate_type_annotations_annotation( - expr, all_used_type_annotations + node.expr, all_used_type_annotations ) return None From b3d16540ec245760134be638ecdfc4d7e40391af Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:03:59 +0200 Subject: [PATCH 6/6] Add back code comment --- pylint/extensions/private_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index c0ef6a5ae5..4d535aae3a 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -199,7 +199,7 @@ def _populate_type_annotations_annotation( case nodes.Name() if node.name not in all_used_type_annotations: all_used_type_annotations[node.name] = True return node.name # type: ignore[no-any-return] - case nodes.Subscript(): + case nodes.Subscript(): # e.g. Optional[List[str]] # slice is the next nested type self._populate_type_annotations_annotation( node.slice, all_used_type_annotations