Skip to content
Closed
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
@@ -1,5 +1,6 @@
# **Upcoming release**

- #402 Update literal `__all__` entries when renaming exported names.
- #895 patchedast Starred and keyword now consumes their syntactically expected and ** (@lieryan)
- #896 patchedast cleanup and refactoring (@lieryan)
- #905 Ignore `node_modules` directories by default when scanning project files and source folders. (@marcoeangeli)
Expand Down
33 changes: 33 additions & 0 deletions rope/refactor/rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from keyword import iskeyword

from rope.base import (
ast,
codeanalyze,
evaluate,
exceptions,
Expand Down Expand Up @@ -121,6 +122,10 @@ def unsure_func(value=unsure):
for file_ in resources:
job_set.started_job(file_.path)
new_content = rename_in_module(finder, new_name, resource=file_)
source = new_content if new_content is not None else file_.read()
new_exports = self._rename_in_all(file_, source, new_name)
if new_exports is not None:
new_content = new_exports
if new_content is not None:
changes.add_change(ChangeContents(file_, new_content))
job_set.finished_job()
Expand All @@ -130,6 +135,34 @@ def unsure_func(value=unsure):
self._rename_module(resource, new_name, changes)
return changes

def _rename_in_all(self, resource, source, new_name):
if "__all__" not in source:
return None
pymodule = self.project.get_pymodule(resource)
if not occurrences.same_pyname(
self.old_pyname, pymodule.get_attributes().get(self.old_name)
):
return None
lines = codeanalyze.ASTLinesAdapter(source)
collector = codeanalyze.ChangeCollector(source)
for node in ast.parse(source).body:
if isinstance(node, ast.Assign):
targets = node.targets
elif isinstance(node, ast.AnnAssign):
targets = [node.target]
else:
continue
if not any(
isinstance(target, ast.Name) and target.id == "__all__"
for target in targets
) or not isinstance(node.value, (ast.List, ast.Tuple)):
continue
for element in node.value.elts:
if isinstance(element, ast.Constant) and element.value == self.old_name:
start, end = lines[element]
collector.add_change(start, end, repr(new_name))
return collector.get_changed()

def validate_changes(
self,
new_name: str,
Expand Down
159 changes: 159 additions & 0 deletions ropetest/refactor/renametest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import unittest
from textwrap import dedent
from types import ModuleType
from unittest.mock import patch

import rope.base.codeanalyze
import rope.refactor.occurrences
Expand Down Expand Up @@ -53,6 +55,163 @@ def test_simple_global_variable_renaming(self):
refactored = self._local_rename("a_var = 20\n", 2, "new_var")
self.assertEqual("new_var = 20\n", refactored)

def test_renaming_exported_name_keeps_star_import_working(self):
mod1 = testutils.create_module(self.project, "mod1")
mod2 = testutils.create_module(self.project, "mod2")
code = dedent("""\
__all__ = ['old_name']
old_name = 1
""")
mod1.write(code)
mod2.write(dedent("""\
from mod1 import *
result = old_name
"""))
self._rename(mod1, code.rindex("old_name"), "new_name")
module = ModuleType("mod1")
exec(mod1.read(), module.__dict__)
namespace = {}
with patch.dict(sys.modules, {"mod1": module}):
exec(mod2.read(), namespace)
self.assertEqual(1, namespace["result"])
self.assertEqual(
dedent("""\
__all__ = ['new_name']
new_name = 1
"""),
mod1.read(),
)

def test_renaming_exported_function_and_class(self):
mod = testutils.create_module(self.project, "mod")
for declaration in ("def old_name(): pass", "class old_name: pass"):
with self.subTest(declaration=declaration):
code = dedent("""\
__all__ = ['old_name']
""") + declaration + "\n"
mod.write(code)
self._rename(mod, code.rindex("old_name"), "new_name")
self.assertEqual(code.replace("old_name", "new_name"), mod.read())

def test_renaming_reexported_name_from_importing_module(self):
mod1 = testutils.create_module(self.project, "mod1")
mod2 = testutils.create_module(self.project, "mod2")
mod1.write(dedent("""\
old_name = 1
"""))
code = dedent("""\
from mod1 import old_name
__all__ = ['old_name']
""")
mod2.write(code)
self._rename(mod2, code.index("old_name"), "new_name")
self.assertEqual(code.replace("old_name", "new_name"), mod2.read())
self.assertEqual("new_name = 1\n", mod1.read())

def test_renaming_exports_preserves_other_strings_and_scopes(self):
code = dedent("""\
old_name = 1
other = 'old_name'
__all__ = ['old_name', 'other', 'old_name_extra']
# old_name
def f():
__all__ = ['old_name']
class C:
__all__ = ['old_name']
""")
refactored = self._local_rename(code, 0, "new_name")
self.assertEqual(
dedent("""\
new_name = 1
other = 'old_name'
__all__ = ['new_name', 'other', 'old_name_extra']
# old_name
def f():
__all__ = ['old_name']
class C:
__all__ = ['old_name']
"""),
refactored,
)

def test_renaming_local_name_does_not_change_exports(self):
code = dedent("""\
__all__ = ['old_name']
old_name = 1
def f():
old_name = 2
return old_name
""")
refactored = self._local_rename(code, code.index("old_name = 2"), "new_name")
self.assertEqual(
dedent("""\
__all__ = ['old_name']
old_name = 1
def f():
new_name = 2
return new_name
"""),
refactored,
)

def test_renaming_exported_name_respects_resources(self):
mod1 = testutils.create_module(self.project, "mod1")
mod2 = testutils.create_module(self.project, "mod2")
mod1.write(dedent("""\
old_name = 1
"""))
code = dedent("""\
from mod1 import old_name
__all__ = ['old_name']
""")
mod2.write(code)
self._rename(mod1, 0, "new_name", resources=[mod1])
self.assertEqual(code, mod2.read())

def test_renaming_exports_ignores_unrelated_modules_and_aliases(self):
mod1 = testutils.create_module(self.project, "mod1")
mod2 = testutils.create_module(self.project, "mod2")
mod3 = testutils.create_module(self.project, "mod3")
mod1.write(dedent("""\
old_name = 1
"""))
code = dedent("""\
old_name = 2
__all__ = ['old_name']
""")
mod2.write(code)
mod3.write(dedent("""\
from mod1 import old_name as alias
__all__ = ['alias']
"""))
self._rename(mod1, 0, "new_name")
self.assertEqual(code, mod2.read())
self.assertEqual(
dedent("""\
from mod1 import new_name as alias
__all__ = ['alias']
"""),
mod3.read(),
)

def test_renaming_literal_exports_with_unicode_and_annotations(self):
mod = testutils.create_module(self.project, "mod")
for export in (
"__all__ = ('old_name',)",
"__all__: list[str] = ['old_name']",
"label = 'é'; __all__ = ['old_name']",
"__all__ = ['old_' 'name']",
"__all__ = ['old_\\x6eame']",
):
with self.subTest(export=export):
code = "old_name = 1\n" + export + "\n"
mod.write(code)
self._rename(mod, 0, "new_name")
namespace = {}
exec(mod.read(), namespace)
self.assertEqual(["new_name"], list(namespace["__all__"]))
self.assertEqual(1, namespace[namespace["__all__"][0]])

def test_variable_renaming_only_in_its_scope(self):
refactored = self._local_rename(
dedent("""\
Expand Down
Loading