diff --git a/CHANGELOG.md b/CHANGELOG.md index ef219ac7..d1b7e4ca 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 4e842c04..6ff74dfc 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 3ac341f2..e0603977 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'''