diff --git a/CHANGELOG.md b/CHANGELOG.md index ba328bd9a..83f35bc1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - #119, #928 Fix IndexError in rename and find occurrences when a `from` import is the last thing in a module without a trailing newline (@mcepl) - #921 Patch the else suite of try statements that also have a finally (@marlon-costa-dc) - #937 Preserve non-ASCII argument expressions and default values when changing function signatures (@Jinyong-Li) +- #935 Rename variables declared `nonlocal` in inner functions together with the enclosing function's variable (@ryanchou1994) # Release 1.15.1 diff --git a/rope/base/pyobjectsdef.py b/rope/base/pyobjectsdef.py index 95bd15691..05577ac52 100644 --- a/rope/base/pyobjectsdef.py +++ b/rope/base/pyobjectsdef.py @@ -568,6 +568,22 @@ def _Global(self, node): pyname = pynamesdef.AssignedName(node.lineno) self.names[name] = pyname + def _Nonlocal(self, node): + for name in node.names: + pyname = self._lookup_enclosing_function(name) + if pyname is not None: + self.names[name] = pyname + + def _lookup_enclosing_function(self, name): + enclosing = self.owner_object.parent + while isinstance(enclosing, (PyFunction, PyClass)): + if isinstance(enclosing, PyFunction): + names = enclosing.get_scope().get_names() + if name in names: + return names[name] + enclosing = enclosing.parent + return None + class _ComprehensionVisitor(_ScopeVisitor): def _comprehension(self, node): diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index a92883693..1445793a2 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -1108,6 +1108,50 @@ def a_func(): refactored, ) + _nonlocal_code = dedent("""\ + var_x = "global" + def outer(): + def inner1(): + nonlocal var_x + var_x = 1 + def inner2(): + nonlocal var_x + var_x = 2 + def inner3(): + print(var_x) + def inner4(): + var_x = 4 + var_x = None + return var_x + """) + + _nonlocal_expected = dedent("""\ + var_x = "global" + def outer(): + def inner1(): + nonlocal new_var + new_var = 1 + def inner2(): + nonlocal new_var + new_var = 2 + def inner3(): + print(new_var) + def inner4(): + var_x = 4 + new_var = None + return new_var + """) + + def test_renaming_nonlocal_variables(self): + code = self._nonlocal_code + refactored = self._local_rename(code, code.index("var_x", code.index("nonlocal")), "new_var") + self.assertEqual(self._nonlocal_expected, refactored) + + def test_renaming_nonlocal_variables_from_enclosing_function(self): + code = self._nonlocal_code + refactored = self._local_rename(code, code.index("var_x = None"), "new_var") + self.assertEqual(self._nonlocal_expected, refactored) + def test_renaming_when_unsure(self): code = dedent("""\ class C(object):