Skip to content

Commit d2c5127

Browse files
authored
Merge pull request #863 from python-rope/lieryan-match-case-corner-cases
Handle parentheses around MatchSequence more correctly
2 parents 5ad2c6a + 2d52f55 commit d2c5127

5 files changed

Lines changed: 182 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- #852 Implement patchedast handlers for TypeAlias
66
- #853 Implement patchedast handlers TypeVar
77
- #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf)
8-
- #819 supports MatchOr, MatchSequence, MatchStar (@jheld)
8+
- #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan)
99

1010
# Release 1.14.0
1111

rope/base/codeanalyze.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ def get_line_start(self, lineno):
7070
def get_line_end(self, lineno):
7171
return self.starts[lineno] - 1
7272

73+
def __getitem__(self, subscript):
74+
start_offset = self._calculate_offset(subscript.start)
75+
stop_offset = self._calculate_offset(subscript.stop)
76+
return self.code[start_offset:stop_offset]
77+
78+
def _calculate_offset(self, coord: tuple[int, int]) -> int:
79+
lineno, col_offset = coord
80+
lineno = self._clamp(0, self.length(), lineno)
81+
col_offset = self._clamp(
82+
0,
83+
self.get_line_end(lineno) - self.get_line_start(lineno),
84+
col_offset,
85+
)
86+
87+
return self.get_line_start(lineno) + col_offset
88+
89+
def _clamp(self, min_value, max_value, value):
90+
return max(min_value, min(max_value, value))
91+
7392

7493
class ArrayLinesAdapter:
7594
def __init__(self, lines):

rope/refactor/patchedast.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,9 +804,43 @@ def _match_case(self, node):
804804
self._handle(node, children)
805805

806806
def _MatchSequence(self, node):
807-
children = ["[", *self._child_nodes(node.patterns, ","), "]"]
807+
if node.patterns:
808+
opening_paren, closing_paren = self._get_surrounding_parens(node)
809+
810+
children = [
811+
*opening_paren,
812+
*self._child_nodes(node.patterns, ","),
813+
*closing_paren,
814+
]
815+
else:
816+
node_start = (node.lineno, node.col_offset)
817+
node_end = (node.end_lineno, node.end_col_offset)
818+
children = [self.lines[node_start:node_end]]
808819
self._handle(node, children)
809820

821+
def _get_surrounding_parens(self, node: ast.MatchSequence):
822+
node_start = (node.lineno, node.col_offset)
823+
first_pattern_start = (node.patterns[0].lineno, node.patterns[0].col_offset)
824+
opening_paren = self.lines[node_start:first_pattern_start].strip()
825+
if opening_paren not in ["[", "(", ""]:
826+
warnings.warn(
827+
f"Unexpected character in MatchSequence's opening_paren <{opening_paren}>; please report!",
828+
RuntimeWarning,
829+
)
830+
831+
last_pattern_end = (
832+
node.patterns[-1].end_lineno,
833+
node.patterns[-1].end_col_offset,
834+
)
835+
node_end = (node.end_lineno, node.end_col_offset)
836+
closing_paren = self.lines[last_pattern_end:node_end].strip()
837+
if closing_paren not in ["]", ")", ""]:
838+
warnings.warn(
839+
f"Unexpected character in MatchSequence's closing_paren <{closing_paren}>; please report!",
840+
RuntimeWarning,
841+
)
842+
return opening_paren, closing_paren
843+
810844
def _MatchStar(self, node):
811845
self._handle(node, ["*", node.name or "_"])
812846

ropetest/codeanalyzetest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ def test_source_lines_last_line_with_no_new_line(self):
3838
to_lines = SourceLinesAdapter("line1")
3939
self.assertEqual(1, to_lines.get_line_number(5))
4040

41+
def test_source_lines_getitem_range(self):
42+
to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n")
43+
self.assertEqual('ne2\nli', to_lines[(2, 2):(3, 2)])
44+
45+
def test_source_lines_getitem_start_lineno_out_of_range(self):
46+
to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n")
47+
self.assertEqual("", to_lines[(100, 2):(3, 2)])
48+
49+
def test_source_lines_getitem_start_col_offset_out_of_range(self):
50+
to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n")
51+
self.assertEqual('\nli', to_lines[(2, 100):(3, 2)])
52+
53+
def test_source_lines_getitem_end_lineno_out_of_range(self):
54+
to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n")
55+
self.assertEqual("ne2\nline3\nline4\n", to_lines[(2, 2):(100, 2)])
56+
57+
def test_source_lines_getitem_end_col_offset_out_of_range(self):
58+
to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n")
59+
self.assertEqual('ne2\nline3', to_lines[(2, 2):(3, 100)])
60+
4161

4262
class WordRangeFinderTest(unittest.TestCase):
4363
def _find_primary(self, code, offset):

ropetest/refactor/patchedasttest.py

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,85 +1426,168 @@ def test_match_node_with_wildcard(self):
14261426

14271427
@testutils.only_for_versions_higher("3.10")
14281428
def test_match_node_with_match_or(self):
1429-
source = dedent(
1430-
"""\
1429+
source = dedent("""\
14311430
match x:
14321431
case 'v'|'z':
14331432
print(x)
1434-
"""
1435-
)
1433+
""")
14361434
ast_frag = patchedast.get_patched_ast(source, True)
14371435
checker = _ResultChecker(self, ast_frag)
14381436
self.assert_single_case_match_block(checker, "MatchOr")
14391437
checker.check_children("MatchOr", ["MatchValue", "", "|", "", "MatchValue"])
14401438

14411439
@testutils.only_for_versions_higher("3.10")
14421440
def test_match_node_with_match_singleton_true(self):
1443-
source = dedent(
1444-
"""\
1441+
source = dedent("""\
14451442
match x:
14461443
case True:
14471444
print(x)
1448-
"""
1449-
)
1445+
""")
14501446
ast_frag = patchedast.get_patched_ast(source, True)
14511447
checker = _ResultChecker(self, ast_frag)
14521448
self.assert_single_case_match_block(checker, "MatchSingleton")
14531449
checker.check_children("MatchSingleton", ["True"])
14541450

14551451
@testutils.only_for_versions_higher("3.10")
14561452
def test_match_node_with_match_singleton_none(self):
1457-
source = dedent(
1458-
"""\
1453+
source = dedent("""\
14591454
match x:
14601455
case None:
14611456
print(x)
1462-
"""
1463-
)
1457+
""")
14641458
ast_frag = patchedast.get_patched_ast(source, True)
14651459
checker = _ResultChecker(self, ast_frag)
14661460
self.assert_single_case_match_block(checker, "MatchSingleton")
14671461
checker.check_children("MatchSingleton", ["None"])
14681462

14691463
@testutils.only_for_versions_higher("3.10")
14701464
def test_match_node_with_match_sequence_with_star_wildcard(self):
1471-
source = dedent(
1472-
"""\
1465+
source = dedent("""\
14731466
match x:
14741467
case [*_]:
14751468
print(x)
1476-
"""
1477-
)
1469+
""")
14781470
ast_frag = patchedast.get_patched_ast(source, True)
14791471
checker = _ResultChecker(self, ast_frag)
14801472
self.assert_single_case_match_block(checker, "MatchSequence")
14811473
checker.check_children("MatchSequence", ["[", "", "MatchStar", "", "]"])
14821474

14831475
@testutils.only_for_versions_higher("3.10")
14841476
def test_match_node_with_match_sequence_with_tail_capture(self):
1485-
source = dedent(
1486-
"""\
1477+
source = dedent("""\
14871478
match x:
14881479
case [1, 2, *rest]:
14891480
print(rest)
1490-
"""
1491-
)
1481+
""")
14921482
ast_frag = patchedast.get_patched_ast(source, True)
14931483
checker = _ResultChecker(self, ast_frag)
14941484
self.assert_single_case_match_block(checker, "MatchSequence")
14951485
checker.check_children("MatchSequence", [
14961486
"[", "", "MatchValue", "", ",", " ", "MatchValue", "", ",", " ", "MatchStar", "", "]",
14971487
])
14981488

1489+
@testutils.only_for_versions_higher("3.10")
1490+
def test_match_node_with_match_sequence_with_no_parens(self):
1491+
source = dedent("""\
1492+
match x:
1493+
case 1, 2:
1494+
print(rest)
1495+
""")
1496+
ast_frag = patchedast.get_patched_ast(source, True)
1497+
checker = _ResultChecker(self, ast_frag)
1498+
self.assert_single_case_match_block(checker, "MatchSequence")
1499+
checker.check_children("MatchSequence", [
1500+
"MatchValue", "", ",", " ", "MatchValue",
1501+
])
1502+
1503+
@testutils.only_for_versions_higher("3.10")
1504+
def test_match_node_with_match_sequence_with_square_parens(self):
1505+
source = dedent("""\
1506+
match x:
1507+
case [1, 2]:
1508+
print(rest)
1509+
""")
1510+
ast_frag = patchedast.get_patched_ast(source, True)
1511+
checker = _ResultChecker(self, ast_frag)
1512+
self.assert_single_case_match_block(checker, "MatchSequence")
1513+
checker.check_children("MatchSequence", [
1514+
"[", "", "MatchValue", "", ",", " ", "MatchValue", "", "]",
1515+
])
1516+
1517+
@testutils.only_for_versions_higher("3.10")
1518+
def test_match_node_with_match_sequence_with_round_parens(self):
1519+
source = dedent("""\
1520+
match x:
1521+
case (1, 2):
1522+
print(rest)
1523+
""")
1524+
ast_frag = patchedast.get_patched_ast(source, True)
1525+
checker = _ResultChecker(self, ast_frag)
1526+
self.assert_single_case_match_block(checker, "MatchSequence")
1527+
checker.check_children("MatchSequence", [
1528+
"(", "", "MatchValue", "", ",", " ", "MatchValue", "", ")",
1529+
])
1530+
1531+
@testutils.only_for_versions_higher("3.10")
1532+
def test_match_node_with_match_sequence_with_spaces_around_parens(self):
1533+
source = dedent("""\
1534+
match x:
1535+
case ( 1, 2
1536+
):
1537+
print(rest)
1538+
""")
1539+
ast_frag = patchedast.get_patched_ast(source, True)
1540+
checker = _ResultChecker(self, ast_frag)
1541+
self.assert_single_case_match_block(checker, "MatchSequence")
1542+
checker.check_children("MatchSequence", [
1543+
"(", " ", "MatchValue", "", ",", " ", "MatchValue", "\n", ")",
1544+
])
1545+
1546+
@testutils.only_for_versions_higher("3.10")
1547+
def test_match_node_with_match_sequence_with_internal_parens(self):
1548+
source = dedent("""\
1549+
match x:
1550+
case [1], [2]:
1551+
print(rest)
1552+
""")
1553+
ast_frag = patchedast.get_patched_ast(source, True)
1554+
checker = _ResultChecker(self, ast_frag)
1555+
self.assert_single_case_match_block(checker, "MatchSequence")
1556+
checker.check_children("MatchSequence", [
1557+
"MatchSequence", "", ",", " ", "MatchSequence"
1558+
])
1559+
1560+
@testutils.only_for_versions_higher("3.10")
1561+
def test_match_node_with_match_sequence_empty_round_parens(self):
1562+
source = dedent("""\
1563+
match x:
1564+
case ( ):
1565+
print(x)
1566+
""")
1567+
ast_frag = patchedast.get_patched_ast(source, True)
1568+
checker = _ResultChecker(self, ast_frag)
1569+
self.assert_single_case_match_block(checker, "MatchSequence")
1570+
checker.check_children("MatchSequence", ["( )"])
1571+
1572+
@testutils.only_for_versions_higher("3.10")
1573+
def test_match_node_with_match_sequence_empty_square_parens(self):
1574+
source = dedent("""\
1575+
match x:
1576+
case []:
1577+
print(x)
1578+
""")
1579+
ast_frag = patchedast.get_patched_ast(source, True)
1580+
checker = _ResultChecker(self, ast_frag)
1581+
self.assert_single_case_match_block(checker, "MatchSequence")
1582+
checker.check_children("MatchSequence", ["[]"])
1583+
14991584
@testutils.only_for_versions_higher("3.10")
15001585
def test_match_node_with_match_sequence_with_star_and_value(self):
1501-
source = dedent(
1502-
"""\
1586+
source = dedent("""\
15031587
match x:
15041588
case [*_, "something"]:
15051589
print(x)
1506-
"""
1507-
)
1590+
""")
15081591
ast_frag = patchedast.get_patched_ast(source, True)
15091592
checker = _ResultChecker(self, ast_frag)
15101593
self.assert_single_case_match_block(checker, "MatchSequence")

0 commit comments

Comments
 (0)