Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions rope/base/codeanalyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ 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):
Expand Down
36 changes: 35 additions & 1 deletion rope/refactor/patchedast.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,9 +804,43 @@ def _match_case(self, node):
self._handle(node, children)

def _MatchSequence(self, node):
children = ["[", *self._child_nodes(node.patterns, ","), "]"]
if node.patterns:
opening_paren, closing_paren = self._get_surrounding_parens(node)

children = [
*opening_paren,
*self._child_nodes(node.patterns, ","),
*closing_paren,
]
else:
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 "_"])

Expand Down
20 changes: 20 additions & 0 deletions ropetest/codeanalyzetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
131 changes: 107 additions & 24 deletions ropetest/refactor/patchedasttest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,85 +1426,168 @@ 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")
checker.check_children("MatchOr", ["MatchValue", "", "|", "", "MatchValue"])

@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")
checker.check_children("MatchSingleton", ["True"])

@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")
checker.check_children("MatchSingleton", ["None"])

@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")
checker.check_children("MatchSequence", ["[", "", "MatchStar", "", "]"])

@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")
checker.check_children("MatchSequence", [
"[", "", "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_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("""\
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(
"""\
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")
Expand Down
Loading