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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- #853 Implement patchedast handlers TypeVar
- #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf)
- #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan)
- #861 Recognize pattern captures for rename and reject unsupported inline operations (@yangfan-yf-yf)

# Release 1.14.0

Expand Down
5 changes: 4 additions & 1 deletion rope/base/oi/soi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

import rope.base.builtins # Use full qualification for clarity.
from rope.base import arguments, evaluate, pynames, pyobjects, utils
from rope.base import arguments, ast, evaluate, pynames, pyobjects, utils
from rope.base.oi.type_hinting.factory import get_type_hinting_factory

_ignore_inferred = utils.ignore_exception(pyobjects.IsBeingInferredError)
Expand Down Expand Up @@ -141,6 +141,9 @@ def _parameter_objects(pyobject):

@_ignore_inferred
def _infer_assignment(assignment, pymodule):
assign_node = assignment.type_hint or assignment.ast_node
if isinstance(assign_node, ast.pattern):
return pyobjects.get_unknown()
result = _follow_pyname(assignment, pymodule)
if result is None:
return None
Expand Down
16 changes: 16 additions & 0 deletions rope/base/pyobjectsdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,22 @@ def _AnnAssign(self, node):
def _AugAssign(self, node):
pass

def _add_match_capture(self, node, name):
if name is not None:
assignment = pynamesdef.AssignmentValue(node)
self._assigned(name, assignment)

def _MatchAs(self, node):
self._add_match_capture(node, node.name)
self.generic_visit(node)

def _MatchStar(self, node):
self._add_match_capture(node, node.name)

def _MatchMapping(self, node):
self._add_match_capture(node, node.rest)
self.generic_visit(node)

def _For(self, node):
self._update_evaluated(node.target, node.iter, ".__iter__().next()")
for child in node.body + node.orelse:
Expand Down
8 changes: 8 additions & 0 deletions rope/refactor/inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import rope.base.builtins # Use fully qualified names for clarity.
from rope.base import (
ast,
codeanalyze,
evaluate,
exceptions,
Expand Down Expand Up @@ -245,6 +246,13 @@ def __init__(self, *args, **kwds):
self._init_imports()

def _check_exceptional_conditions(self):
if any(
isinstance(assignment.ast_node, ast.pattern)
for assignment in self.pyname.assignments
):
raise exceptions.RefactoringError(
"Pattern matching captures cannot be inlined."
)
if len(self.pyname.assignments) != 1:
raise exceptions.RefactoringError(
"Local variable should be assigned once for inlining."
Expand Down
13 changes: 12 additions & 1 deletion ropetest/objectinfertest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from textwrap import dedent

import rope.base.builtins # Use fully-qualified names for clarity.
from rope.base import libutils
from rope.base import libutils, pyobjects
from ropetest import testutils


Expand Down Expand Up @@ -324,6 +324,17 @@ class C(object):
a_var = mod["a_var"].get_object()
self.assertEqual(c_class, a_var.get_type())

@testutils.only_for_versions_higher("3.10")
def test_match_capture_is_unknown(self):
code = dedent("""\
captured = 1
match value:
case [1, *_] as captured:
pass
""")
scope = libutils.get_string_scope(self.project, code)
self.assertEqual(pyobjects.get_unknown(), scope["captured"].get_object())

def test_basic_list_comprehensions(self):
code = dedent("""\
class C(object):
Expand Down
19 changes: 19 additions & 0 deletions ropetest/pyscopestest.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ def test_inline_assignment(self):
["a_var", "values"],
)

@testutils.only_for_versions_higher("3.10")
def test_match_capture_names(self):
code = dedent("""\
def func(value):
match value:
case [_, *rest] as whole:
pass
case {"key": item, **remaining}:
pass
""")
scope = libutils.get_string_scope(self.project, code)
function_scope = scope.get_scopes()[0]
defined_names = set(function_scope.get_names())

self.assertLessEqual(
{"item", "remaining", "rest", "whole"}, defined_names
)
self.assertNotIn("_", defined_names)

@testutils.only_for_versions_higher("3.8")
def test_inline_assignment_in_comprehensions(self):
code = dedent("""\
Expand Down
16 changes: 16 additions & 0 deletions ropetest/refactor/inlinetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ def test_on_unknown_vars(self):
with self.assertRaises(rope.base.exceptions.RefactoringError):
self._inline(code, code.index("another_var") + 1)

@testutils.only_for_versions_higher("3.10")
def test_pattern_capture_cannot_be_inlined(self):
code = dedent("""\
old_name = 0
match value:
case old_name:
print(old_name)
print(old_name)
""")
capture_offset = code.index("old_name", code.index("case")) + 1
with self.assertRaisesRegex(
rope.base.exceptions.RefactoringError,
"Pattern matching captures cannot be inlined",
):
self._inline(code, capture_offset)

def test_attribute_inlining(self):
code = dedent("""\
class A(object):
Expand Down
59 changes: 58 additions & 1 deletion ropetest/refactor/renametest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import sys
import unittest
from textwrap import dedent
from rope.base import exceptions

import rope.base.codeanalyze
import rope.refactor.occurrences
from rope.base import exceptions
from rope.refactor import rename
from rope.refactor.rename import Rename
from ropetest import testutils
Expand Down Expand Up @@ -242,6 +242,63 @@ def test_renaming_inline_assignment(self):
refactored,
)

@testutils.only_for_versions_higher("3.10")
def test_renaming_match_as_capture(self):
code = dedent("""\
match value:
case [1, *_] as old_name:
print(old_name)
""")
refactored = self._local_rename(
code, code.rindex("old_name") + 1, "new_name"
)
self.assertEqual(
dedent("""\
match value:
case [1, *_] as new_name:
print(new_name)
"""),
refactored,
)

@testutils.only_for_versions_higher("3.10")
def test_renaming_match_star_capture(self):
code = dedent("""\
match value:
case [1, *old_name] if old_name:
print(old_name)
""")
refactored = self._local_rename(
code, code.index("old_name") + 1, "new_name"
)
self.assertEqual(
dedent("""\
match value:
case [1, *new_name] if new_name:
print(new_name)
"""),
refactored,
)

@testutils.only_for_versions_higher("3.10")
def test_renaming_match_mapping_rest_capture(self):
code = dedent("""\
match value:
case {"key": item, **old_name}:
print(old_name)
""")
refactored = self._local_rename(
code, code.index("old_name") + 1, "new_name"
)
self.assertEqual(
dedent("""\
match value:
case {"key": item, **new_name}:
print(new_name)
"""),
refactored,
)

def test_renaming_arguments_for_normal_args_changing_calls(self):
code = dedent("""\
def a_func(p1=None, p2=None):
Expand Down
Loading