diff --git a/CHANGELOG.md b/CHANGELOG.md index 1650aa77a..69c06d28e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +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) +- #845 Support PEP 695 type parameters on class definitions and type aliases in patchedast (@marlon-costa-dc) # Release 1.14.0 diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 2a52fc924..2ea194d16 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -327,6 +327,7 @@ def _ClassDef(self, node): for decorator in node.decorator_list: children.extend(("@", decorator)) children.extend(["class", node.name]) + self._add_type_params(node, children) if node.bases: children.append("(") children.extend(self._child_nodes(node.bases, ",")) @@ -493,9 +494,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) - type_params = getattr(node, "type_params", []) - if type_params: - children.extend(["[", *self._child_nodes(type_params, ","), "]"]) + self._add_type_params(node, children) children.extend(["(", node.args, ")"]) children.append(":") children.extend(node.body) @@ -872,9 +871,17 @@ def _MatchMapping(self, node): self._handle(node, children) def _TypeAlias(self, node): - children = ["type", node.name, node.value] + children = ["type", node.name] + self._add_type_params(node, children) + children.append(node.value) self._handle(node, children) + def _add_type_params(self, node, children): + """Append the PEP 695 ``[T, ...]`` clause of a def, class or type alias.""" + type_params = getattr(node, "type_params", []) + if type_params: + children.extend(["[", *self._child_nodes(type_params, ","), "]"]) + def _TypeVar(self, node): children = [node.name] if node.bound: diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index c014f6ed1..6998b24f7 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -326,6 +326,83 @@ 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 = dedent("""\ + type Alias[T] = list[T] + """) + 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): + # TP occurs only in the type parameter list, so its region can only + # come from the "[TP]" clause rendered between the name and "(". + source = dedent("""\ + def f[TP](x): + return x + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + 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 = dedent("""\ + class C[TP]: + pass + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + 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 = dedent("""\ + match x: + case [1, *rest]: + pass + """) + 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") + def test_handling_match_or_and_singleton(self): + source = dedent("""\ + match x: + case 1 | None: + pass + """) + 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'''