From dd6b5614f1ccab84efba62d56f0b456e065b8da1 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 20 Aug 2026 23:46:23 +1000 Subject: [PATCH 1/7] Fix dedent formatting in patchedasttest.py --- ropetest/refactor/patchedasttest.py | 36 ++++++++++------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index da6c993a..c37ad448 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -1426,13 +1426,11 @@ def test_match_node_with_wildcard(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_or(self): - source = dedent( - """\ + source = dedent("""\ match x: case 'v'|'z': print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchOr") @@ -1440,13 +1438,11 @@ def test_match_node_with_match_or(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_singleton_true(self): - source = dedent( - """\ + source = dedent("""\ match x: case True: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSingleton") @@ -1454,13 +1450,11 @@ def test_match_node_with_match_singleton_true(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_singleton_none(self): - source = dedent( - """\ + source = dedent("""\ match x: case None: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSingleton") @@ -1468,13 +1462,11 @@ def test_match_node_with_match_singleton_none(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_star_wildcard(self): - source = dedent( - """\ + source = dedent("""\ match x: case [*_]: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence") @@ -1482,13 +1474,11 @@ def test_match_node_with_match_sequence_with_star_wildcard(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_tail_capture(self): - source = dedent( - """\ + source = dedent("""\ match x: case [1, 2, *rest]: print(rest) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence") @@ -1498,13 +1488,11 @@ def test_match_node_with_match_sequence_with_tail_capture(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_star_and_value(self): - source = dedent( - """\ + source = dedent("""\ match x: case [*_, "something"]: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence") From 935fe21cb0704f7db423d7a1f8b4671708afad29 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 01:04:12 +1000 Subject: [PATCH 2/7] Implement subscribing to SourceLinesAdapter with (lineno, col_offset) pair --- rope/base/codeanalyze.py | 15 +++++++++++++++ ropetest/codeanalyzetest.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index 992ff478..90ff379c 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -70,6 +70,21 @@ def get_line_start(self, lineno): def get_line_end(self, lineno): return self.starts[lineno] - 1 + def __getitem__(self, subscript): + start_offset = self._calculate_offset(subscript.start) + stop_offset = self._calculate_offset(subscript.stop) + return self.code[start_offset:stop_offset] + + def _calculate_offset(self, coord: tuple[int, int]) -> int: + lineno, col_offset = coord + lineno = self._clamp(0, self.length(), lineno) + col_offset = self._clamp(0, self.get_line_end(lineno) - self.get_line_start(lineno), col_offset) + + return self.get_line_start(lineno) + col_offset + + def _clamp(self, min_value, max_value, value): + return max(min_value, min(max_value, value)) + class ArrayLinesAdapter: def __init__(self, lines): diff --git a/ropetest/codeanalyzetest.py b/ropetest/codeanalyzetest.py index 88a4caf9..a73f607c 100644 --- a/ropetest/codeanalyzetest.py +++ b/ropetest/codeanalyzetest.py @@ -38,6 +38,26 @@ def test_source_lines_last_line_with_no_new_line(self): to_lines = SourceLinesAdapter("line1") self.assertEqual(1, to_lines.get_line_number(5)) + def test_source_lines_getitem_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('ne2\nli', to_lines[(2, 2):(3, 2)]) + + def test_source_lines_getitem_start_lineno_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual("", to_lines[(100, 2):(3, 2)]) + + def test_source_lines_getitem_start_col_offset_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('\nli', to_lines[(2, 100):(3, 2)]) + + def test_source_lines_getitem_end_lineno_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual("ne2\nline3\nline4\n", to_lines[(2, 2):(100, 2)]) + + def test_source_lines_getitem_end_col_offset_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('ne2\nline3', to_lines[(2, 2):(3, 100)]) + class WordRangeFinderTest(unittest.TestCase): def _find_primary(self, code, offset): From 50245fb6e59ca2ef813f311d47b946662b2aa20b Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 01:05:02 +1000 Subject: [PATCH 3/7] Handle the various different cases of parentheses around MatchSequence MatchSequence may use (), [], or no parentheses at all. --- rope/refactor/patchedast.py | 7 ++- ropetest/refactor/patchedasttest.py | 80 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 8ebfda39..d9edf700 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -804,7 +804,12 @@ def _match_case(self, node): self._handle(node, children) def _MatchSequence(self, node): - children = ["[", *self._child_nodes(node.patterns, ","), "]"] + if node.patterns: + opening_paren = self.lines[(node.lineno, node.col_offset):(node.patterns[0].lineno, node.patterns[0].col_offset)] + closing_paren = self.lines[(node.patterns[-1].end_lineno, node.patterns[-1].end_col_offset):(node.end_lineno, node.end_col_offset)] + children = [*opening_paren, *self._child_nodes(node.patterns, ","), *closing_paren] + else: + children = [self.lines[(node.lineno, node.col_offset):(node.end_lineno, node.end_col_offset)]] self._handle(node, children) def _MatchStar(self, node): diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index c37ad448..0bbe1225 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -1486,6 +1486,86 @@ def test_match_node_with_match_sequence_with_tail_capture(self): "[", "", "MatchValue", "", ",", " ", "MatchValue", "", ",", " ", "MatchStar", "", "]", ]) + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_no_parens(self): + source = dedent("""\ + match x: + case 1, 2: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "MatchValue", "", ",", " ", "MatchValue", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_square_parens(self): + source = dedent("""\ + match x: + case [1, 2]: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "[", "", "MatchValue", "", ",", " ", "MatchValue", "", "]", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_round_parens(self): + source = dedent("""\ + match x: + case (1, 2): + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "(", "", "MatchValue", "", ",", " ", "MatchValue", "", ")", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_internal_parens(self): + source = dedent("""\ + match x: + case [1], [2]: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "MatchSequence", "", ",", " ", "MatchSequence" + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_empty_round_parens(self): + source = dedent("""\ + match x: + case ( ): + print(x) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", ["( )"]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_empty_square_parens(self): + source = dedent("""\ + match x: + case []: + print(x) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", ["[]"]) + @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_star_and_value(self): source = dedent("""\ From 5de4db7572989de71ed33dcbc361892bc6dcb426 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 02:31:29 +1000 Subject: [PATCH 4/7] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e23bf1a3..6de2ac4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - #852 Implement patchedast handlers for TypeAlias - #853 Implement patchedast handlers TypeVar - #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf) -- #819 supports MatchOr, MatchSequence, MatchStar (@jheld) +- #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) # Release 1.14.0 From 2162483893a0c062f34af29d6b066218a569d61b Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 02:34:53 +1000 Subject: [PATCH 5/7] Black formatting --- rope/base/codeanalyze.py | 6 +++++- rope/refactor/patchedast.py | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index 90ff379c..25203cce 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -78,7 +78,11 @@ def __getitem__(self, subscript): def _calculate_offset(self, coord: tuple[int, int]) -> int: lineno, col_offset = coord lineno = self._clamp(0, self.length(), lineno) - col_offset = self._clamp(0, self.get_line_end(lineno) - self.get_line_start(lineno), col_offset) + col_offset = self._clamp( + 0, + self.get_line_end(lineno) - self.get_line_start(lineno), + col_offset, + ) return self.get_line_start(lineno) + col_offset diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index d9edf700..1e8fc81e 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -805,11 +805,32 @@ def _match_case(self, node): def _MatchSequence(self, node): if node.patterns: - opening_paren = self.lines[(node.lineno, node.col_offset):(node.patterns[0].lineno, node.patterns[0].col_offset)] - closing_paren = self.lines[(node.patterns[-1].end_lineno, node.patterns[-1].end_col_offset):(node.end_lineno, node.end_col_offset)] - children = [*opening_paren, *self._child_nodes(node.patterns, ","), *closing_paren] + opening_paren = self.lines[ + (node.lineno, node.col_offset) : ( + node.patterns[0].lineno, + node.patterns[0].col_offset, + ) + ] + closing_paren = self.lines[ + (node.patterns[-1].end_lineno, node.patterns[-1].end_col_offset) : ( + node.end_lineno, + node.end_col_offset, + ) + ] + children = [ + *opening_paren, + *self._child_nodes(node.patterns, ","), + *closing_paren, + ] else: - children = [self.lines[(node.lineno, node.col_offset):(node.end_lineno, node.end_col_offset)]] + children = [ + self.lines[ + (node.lineno, node.col_offset) : ( + node.end_lineno, + node.end_col_offset, + ) + ] + ] self._handle(node, children) def _MatchStar(self, node): From 3225d06ec9c8ec63571b9b047d15dd25e800da02 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 04:38:50 +1000 Subject: [PATCH 6/7] Strip spaces around parentheses of MatchSequence --- rope/refactor/patchedast.py | 4 ++-- ropetest/refactor/patchedasttest.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 1e8fc81e..3f0e43a7 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -810,13 +810,13 @@ def _MatchSequence(self, node): node.patterns[0].lineno, node.patterns[0].col_offset, ) - ] + ].strip() closing_paren = self.lines[ (node.patterns[-1].end_lineno, node.patterns[-1].end_col_offset) : ( node.end_lineno, node.end_col_offset, ) - ] + ].strip() children = [ *opening_paren, *self._child_nodes(node.patterns, ","), diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index 0bbe1225..67a521b7 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -1528,6 +1528,21 @@ def test_match_node_with_match_sequence_with_round_parens(self): "(", "", "MatchValue", "", ",", " ", "MatchValue", "", ")", ]) + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_spaces_around_parens(self): + source = dedent("""\ + match x: + case ( 1, 2 + ): + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "(", " ", "MatchValue", "", ",", " ", "MatchValue", "\n", ")", + ]) + @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_internal_parens(self): source = dedent("""\ From c8938c768fb833b3008d2a45d6776026c304e009 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 21 Aug 2026 05:03:26 +1000 Subject: [PATCH 7/7] Refactor _get_surrounding_parens() into separate function --- rope/refactor/patchedast.py | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 3f0e43a7..a10350ae 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -805,34 +805,42 @@ def _match_case(self, node): def _MatchSequence(self, node): if node.patterns: - opening_paren = self.lines[ - (node.lineno, node.col_offset) : ( - node.patterns[0].lineno, - node.patterns[0].col_offset, - ) - ].strip() - closing_paren = self.lines[ - (node.patterns[-1].end_lineno, node.patterns[-1].end_col_offset) : ( - node.end_lineno, - node.end_col_offset, - ) - ].strip() + opening_paren, closing_paren = self._get_surrounding_parens(node) + children = [ *opening_paren, *self._child_nodes(node.patterns, ","), *closing_paren, ] else: - children = [ - self.lines[ - (node.lineno, node.col_offset) : ( - node.end_lineno, - node.end_col_offset, - ) - ] - ] + node_start = (node.lineno, node.col_offset) + node_end = (node.end_lineno, node.end_col_offset) + children = [self.lines[node_start:node_end]] self._handle(node, children) + def _get_surrounding_parens(self, node: ast.MatchSequence): + node_start = (node.lineno, node.col_offset) + first_pattern_start = (node.patterns[0].lineno, node.patterns[0].col_offset) + opening_paren = self.lines[node_start:first_pattern_start].strip() + if opening_paren not in ["[", "(", ""]: + warnings.warn( + f"Unexpected character in MatchSequence's opening_paren <{opening_paren}>; please report!", + RuntimeWarning, + ) + + last_pattern_end = ( + node.patterns[-1].end_lineno, + node.patterns[-1].end_col_offset, + ) + node_end = (node.end_lineno, node.end_col_offset) + closing_paren = self.lines[last_pattern_end:node_end].strip() + if closing_paren not in ["]", ")", ""]: + warnings.warn( + f"Unexpected character in MatchSequence's closing_paren <{closing_paren}>; please report!", + RuntimeWarning, + ) + return opening_paren, closing_paren + def _MatchStar(self, node): self._handle(node, ["*", node.name or "_"])