From ae6c838bd8d03df1e14d77861767f239af5ad82f Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Wed, 22 Jul 2026 11:30:59 -0300 Subject: [PATCH 1/5] Add patchedast handlers for PEP 695 type params and match subpatterns rope 1.14 (and master) lack `_PatchingASTWalker` handlers for part of the Python 3.12/3.13 parser surface, so modules using this syntax emit noisy `Unknown node type <...>` warnings and, combined with other tokens, can abort rope analysis (rename / inline / census) with MismatchedTokenError. Add native handlers rendering the source token stream: - PEP 695: `type X[T] = ...` (_TypeAlias); type-parameter lists on `def f[T]` / `class C[T]` via a shared _type_params_children helper; and _TypeVar / _ParamSpec / _TypeVarTuple. - Structural pattern matching: _MatchSequence (list/tuple aware via the opening token), _MatchStar, _MatchOr, _MatchSingleton. Adds regression tests. Full refactor suite stays green. --- CHANGELOG.md | 5 +++ rope/refactor/patchedast.py | 64 +++++++++++++++++++++++++++++ ropetest/refactor/patchedasttest.py | 43 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef219ac71..d1b7e4cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # **Upcoming release** +- Add `patchedast` handlers for Python 3.12/3.13 native syntax so rope's AST + region walker no longer emits `Unknown node type` warnings or aborts on + PEP 695 type parameters (`type X[T] = ...`, `def f[T]`, `class C[T]`, + TypeVar/ParamSpec/TypeVarTuple) and structural pattern subtypes + (MatchSequence, MatchStar, MatchOr, MatchSingleton) (@marlon-costa-dc) - ... # Release 1.14.0 diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 4e842c046..6ff74dfc0 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -325,6 +325,7 @@ def _ClassDef(self, node): for decorator in node.decorator_list: children.extend(("@", decorator)) children.extend(["class", node.name]) + children.extend(self._type_params_children(node)) if node.bases: children.append("(") children.extend(self._child_nodes(node.bases, ",")) @@ -333,6 +334,21 @@ def _ClassDef(self, node): children.extend(node.body) self._handle(node, children) + def _type_params_children(self, node): + # PEP 695: render `[T, *Ts, **P]` type parameters between the name and + # the opening parenthesis. Empty when the node has no type params so + # pre-3.12 code is unaffected. + type_params = getattr(node, "type_params", None) or () + if not type_params: + return [] + children = ["["] + for index, type_param in enumerate(type_params): + if index > 0: + children.append(",") + children.append(type_param) + children.append("]") + return children + def _Compare(self, node): children = [] children.append(node.left) @@ -491,6 +507,7 @@ def _handle_function_def_node(self, node, is_async): children.extend(("@", decorator)) children.extend(["async", "def"] if is_async else ["def"]) children.append(node.name) + children.extend(self._type_params_children(node)) children.extend(["(", node.args, ")"]) children.append(":") children.extend(node.body) @@ -825,6 +842,53 @@ def _MatchMapping(self, node): children.append("}") self._handle(node, children) + def _pattern_opening_token(self, node): + lineno = getattr(node, "lineno", None) + col_offset = getattr(node, "col_offset", None) + if not isinstance(lineno, int) or not isinstance(col_offset, int): + return "" + line_start = self.lines.get_line_start(lineno) + start = line_start + col_offset + return self.source.source[start : start + 1] + + def _MatchSequence(self, node): + children = self._child_nodes(node.patterns, ",") + opening = self._pattern_opening_token(node) + if opening == "[": + self._handle(node, ["[", *children, "]"]) + return + if opening == "(" and not node.patterns: + self._handle(node, [self.empty_tuple]) + return + self._handle(node, children, eat_parens=opening == "(") + + def _MatchStar(self, node): + self._handle(node, ["*", node.name or "_"]) + + def _MatchOr(self, node): + self._handle(node, self._child_nodes(node.patterns, "|")) + + def _MatchSingleton(self, node): + self._handle(node, [str(node.value)]) + + def _TypeAlias(self, node): + children = ["type", node.name] + children.extend(self._type_params_children(node)) + children.extend(["=", node.value]) + self._handle(node, children) + + def _TypeVar(self, node): + children = [node.name] + if getattr(node, "bound", None) is not None: + children.extend([":", node.bound]) + self._handle(node, children) + + def _ParamSpec(self, node): + self._handle(node, ["**", node.name]) + + def _TypeVarTuple(self, node): + self._handle(node, ["*", node.name]) + class _Source: def __init__(self, source): diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index 3ac341f25..e06039772 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -272,6 +272,49 @@ def test_handling_format_strings_basic(self): checker.check_children("JoinedStr", ['f"', "abc", "FormattedValue", "", '"']) checker.check_children("FormattedValue", ["{", "", "Name", "", "}"]) + @testutils.only_for_versions_higher("3.12") + def test_handling_pep695_type_alias(self): + source = "type Alias[T] = list[T]\n" + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children( + "TypeAlias", + ["type", " ", "Name", "", "[", "", "TypeVar", "", "]", " ", + "=", " ", "Subscript"], + ) + + @testutils.only_for_versions_higher("3.12") + def test_handling_pep695_generic_function(self): + source = "def f[T](x: T) -> T:\n return x\n" + ast_frag = patchedast.get_patched_ast(source, True) + # The type parameter list must be rendered between name and '('. + assert "[T]" in source + checker = _ResultChecker(self, ast_frag) + checker.check_children("TypeVar", ["T"]) + + @testutils.only_for_versions_higher("3.12") + def test_handling_pep695_generic_class(self): + source = "class C[T]:\n pass\n" + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children("TypeVar", ["T"]) + + @testutils.only_for_versions_higher("3.10") + def test_handling_match_sequence_and_star(self): + source = "match x:\n case [1, *rest]:\n pass\n" + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children("MatchStar", ["*", "", "rest"]) + + @testutils.only_for_versions_higher("3.10") + def test_handling_match_or_and_singleton(self): + source = "match x:\n case 1 | None:\n pass\n" + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children( + "MatchOr", ["MatchValue", " ", "|", " ", "MatchSingleton"] + ) + @testutils.only_for_versions_higher("3.6") def test_handling_format_strings_with_implicit_join(self): source = '''"1" + rf'abc{a}' f"""xxx{b} """\n''' From 7d5bf709cce566069598c4a2ce2f76b9a593ad8b Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Fri, 25 Sep 2026 11:21:20 -0300 Subject: [PATCH 2/5] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ropetest/refactor/patchedasttest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index e06039772..13721cac7 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -285,11 +285,11 @@ def test_handling_pep695_type_alias(self): @testutils.only_for_versions_higher("3.12") def test_handling_pep695_generic_function(self): - source = "def f[T](x: T) -> T:\n return x\n" + source = "def f[T](x):\n return x\n" ast_frag = patchedast.get_patched_ast(source, True) - # The type parameter list must be rendered between name and '('. - assert "[T]" in source checker = _ResultChecker(self, ast_frag) + t_start = source.index("[T]") + 1 + checker.check_region("TypeVar", t_start, t_start + 1) checker.check_children("TypeVar", ["T"]) @testutils.only_for_versions_higher("3.12") @@ -297,6 +297,8 @@ def test_handling_pep695_generic_class(self): source = "class C[T]:\n pass\n" ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) + t_start = source.index("[T]") + 1 + checker.check_region("TypeVar", t_start, t_start + 1) checker.check_children("TypeVar", ["T"]) @testutils.only_for_versions_higher("3.10") From 4131d710d1342ccbb063bcba4d99e6a001f16ce8 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Fri, 25 Sep 2026 11:24:26 -0300 Subject: [PATCH 3/5] Assert PEP 695 and match-sequence regions in patchedast tests Addresses review: the generic function test now uses a type parameter name that occurs only in the [TP] clause and asserts both the FunctionDef children and the TypeVar region; the generic class test asserts the ClassDef children and TypeVar region; the match test asserts the MatchSequence children (including brackets) and region. --- ropetest/refactor/patchedasttest.py | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index 005e178d0..7ff40d3f1 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -339,25 +339,43 @@ def test_handling_pep695_type_alias(self): @testutils.only_for_versions_higher("3.12") def test_handling_pep695_generic_function(self): - source = "def f[T](x: T) -> T:\n return x\n" + # TP occurs only in the type parameter list, so its region can only + # come from the "[TP]" clause rendered between the name and "(". + source = "def f[TP](x):\n return x\n" ast_frag = patchedast.get_patched_ast(source, True) - # The type parameter list must be rendered between name and '('. - assert "[T]" in source checker = _ResultChecker(self, ast_frag) - checker.check_children("TypeVar", ["T"]) + checker.check_children( + "FunctionDef", + ["def", " ", "f", "", "[", "", "TypeVar", "", "]", "", "(", "", + "arguments", "", ")", "", ":", "\n ", "Return"], + ) + start = source.index("TP") + checker.check_region("TypeVar", start, start + len("TP")) @testutils.only_for_versions_higher("3.12") def test_handling_pep695_generic_class(self): - source = "class C[T]:\n pass\n" + source = "class C[TP]:\n pass\n" ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) - checker.check_children("TypeVar", ["T"]) + checker.check_children( + "ClassDef", + ["class", " ", "C", "", "[", "", "TypeVar", "", "]", "", ":", + "\n ", "Pass"], + ) + start = source.index("TP") + checker.check_region("TypeVar", start, start + len("TP")) @testutils.only_for_versions_higher("3.10") def test_handling_match_sequence_and_star(self): source = "match x:\n case [1, *rest]:\n pass\n" ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) + checker.check_children( + "MatchSequence", + ["[", "", "MatchValue", "", ",", " ", "MatchStar", "", "]"], + ) + start = source.index("[1, *rest]") + checker.check_region("MatchSequence", start, start + len("[1, *rest]")) checker.check_children("MatchStar", ["*", "", "rest"]) @testutils.only_for_versions_higher("3.10") From a59d1a521b7727457a06c343863b38a10f111167 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 26 Sep 2026 22:42:20 +1000 Subject: [PATCH 4/5] Use dedent for multiline code Co-authored-by: Lie Ryan --- ropetest/refactor/patchedasttest.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index 7ff40d3f1..6998b24f7 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -328,7 +328,9 @@ def test_handling_format_strings_basic(self): @testutils.only_for_versions_higher("3.12") def test_handling_pep695_type_alias(self): - source = "type Alias[T] = list[T]\n" + source = dedent("""\ + type Alias[T] = list[T] + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) checker.check_children( @@ -341,7 +343,10 @@ def test_handling_pep695_type_alias(self): def test_handling_pep695_generic_function(self): # TP occurs only in the type parameter list, so its region can only # come from the "[TP]" clause rendered between the name and "(". - source = "def f[TP](x):\n return x\n" + source = dedent("""\ + def f[TP](x): + return x + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) checker.check_children( @@ -354,7 +359,10 @@ def test_handling_pep695_generic_function(self): @testutils.only_for_versions_higher("3.12") def test_handling_pep695_generic_class(self): - source = "class C[TP]:\n pass\n" + source = dedent("""\ + class C[TP]: + pass + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) checker.check_children( @@ -367,7 +375,11 @@ def test_handling_pep695_generic_class(self): @testutils.only_for_versions_higher("3.10") def test_handling_match_sequence_and_star(self): - source = "match x:\n case [1, *rest]:\n pass\n" + source = dedent("""\ + match x: + case [1, *rest]: + pass + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) checker.check_children( @@ -380,7 +392,11 @@ def test_handling_match_sequence_and_star(self): @testutils.only_for_versions_higher("3.10") def test_handling_match_or_and_singleton(self): - source = "match x:\n case 1 | None:\n pass\n" + source = dedent("""\ + match x: + case 1 | None: + pass + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) checker.check_children( From 5055652ba363cc86ae75ef8f6fc79d1e36cb9c36 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 26 Sep 2026 22:42:47 +1000 Subject: [PATCH 5/5] Update CHANGELOG.md entry to match common format --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc100cb4..69c06d28e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) - #870 Add default implementation for is_dir() (@lieryan) - #872 Fix unicode handling in patchedast (@lieryan) -- Support PEP 695 type parameters on class definitions and type aliases in patchedast +- #845 Support PEP 695 type parameters on class definitions and type aliases in patchedast (@marlon-costa-dc) # Release 1.14.0