Fix renaming of namespace packages, fixes #1779

This commit is contained in:
Dave Halter
2023-07-27 03:06:27 +02:00
parent f2444b4be5
commit 5f19237a3e
4 changed files with 66 additions and 9 deletions

View File

@@ -105,8 +105,7 @@ class BaseName:
# Compiled modules should not return a module path even if they # Compiled modules should not return a module path even if they
# have one. # have one.
path: Optional[Path] = self._get_module_context().py__file__() path: Optional[Path] = self._get_module_context().py__file__()
if path is not None: return path
return path
return None return None

View File

@@ -5,6 +5,7 @@ from typing import Dict, Iterable, Tuple
from parso import split_lines from parso import split_lines
from jedi.api.exceptions import RefactoringError from jedi.api.exceptions import RefactoringError
from jedi.inference.value.namespace import ImplicitNSName
EXPRESSION_PARTS = ( EXPRESSION_PARTS = (
'or_test and_test not_test comparison ' 'or_test and_test not_test comparison '
@@ -102,7 +103,12 @@ class Refactoring:
to_path=calculate_to_path(path), to_path=calculate_to_path(path),
module_node=next(iter(map_)).get_root_node(), module_node=next(iter(map_)).get_root_node(),
node_to_str_map=map_ node_to_str_map=map_
) for path, map_ in sorted(self._file_to_node_changes.items()) )
# We need to use `or`, because the path can be None
for path, map_ in sorted(
self._file_to_node_changes.items(),
key=lambda x: x[0] or Path("")
)
} }
def get_renames(self) -> Iterable[Tuple[Path, Path]]: def get_renames(self) -> Iterable[Tuple[Path, Path]]:
@@ -116,7 +122,7 @@ class Refactoring:
project_path = self._inference_state.project.path project_path = self._inference_state.project.path
for from_, to in self.get_renames(): for from_, to in self.get_renames():
text += 'rename from %s\nrename to %s\n' \ text += 'rename from %s\nrename to %s\n' \
% (from_.relative_to(project_path), to.relative_to(project_path)) % (_try_relative_to(from_, project_path), _try_relative_to(to, project_path))
return text + ''.join(f.get_diff() for f in self.get_changed_files().values()) return text + ''.join(f.get_diff() for f in self.get_changed_files().values())
@@ -146,13 +152,17 @@ def rename(inference_state, definitions, new_name):
raise RefactoringError("There is no name under the cursor") raise RefactoringError("There is no name under the cursor")
for d in definitions: for d in definitions:
# This private access is ok in a way. It's not public to
# protect Jedi users from seeing it.
tree_name = d._name.tree_name tree_name = d._name.tree_name
if d.type == 'module' and tree_name is None: if d.type == 'module' and tree_name is None and d.module_path is not None:
p = None if d.module_path is None else Path(d.module_path) p = Path(d.module_path)
file_renames.add(_calculate_rename(p, new_name)) file_renames.add(_calculate_rename(p, new_name))
elif isinstance(d._name, ImplicitNSName):
#file_renames.add(_calculate_rename(p, new_name))
for p in d._name._value.py__path__():
file_renames.add(_calculate_rename(Path(p), new_name))
else: else:
# This private access is ok in a way. It's not public to
# protect Jedi users from seeing it.
if tree_name is not None: if tree_name is not None:
fmap = file_tree_name_map.setdefault(d.module_path, {}) fmap = file_tree_name_map.setdefault(d.module_path, {})
fmap[tree_name] = tree_name.prefix + new_name fmap[tree_name] = tree_name.prefix + new_name
@@ -246,3 +256,9 @@ def _remove_indent_of_prefix(prefix):
Removes the last indentation of a prefix, e.g. " \n \n " becomes " \n \n". Removes the last indentation of a prefix, e.g. " \n \n " becomes " \n \n".
""" """
return ''.join(split_lines(prefix, keepends=True)[:-1]) return ''.join(split_lines(prefix, keepends=True)[:-1])
def _try_relative_to(path: Path, base: Path) -> Path:
try:
return path.relative_to(base)
except ValueError:
return path

View File

@@ -76,7 +76,7 @@ from import_tree.pkg.mod1 import not_existant,
#? 22 ['mod1', 'base'] #? 22 ['mod1', 'base']
from import_tree.pkg. import mod1 from import_tree.pkg. import mod1
#? 17 ['mod1', 'mod2', 'random', 'pkg', 'references', 'rename1', 'rename2', 'classes', 'globals', 'recurse_class1', 'recurse_class2', 'invisible_pkg', 'flow_import'] #? 17 ['mod1', 'mod2', 'random', 'pkg', 'references', 'rename1', 'rename2', 'classes', 'globals', 'recurse_class1', 'recurse_class2', 'invisible_pkg', 'flow_import']
from import_tree. import pkg from import_tree. import new_pkg
#? 18 ['pkg'] #? 18 ['pkg']
from import_tree.p import pkg from import_tree.p import pkg

View File

@@ -1,4 +1,5 @@
import os import os
import shutil
from textwrap import dedent from textwrap import dedent
from pathlib import Path from pathlib import Path
import platform import platform
@@ -6,6 +7,7 @@ import platform
import pytest import pytest
import jedi import jedi
from test.helpers import get_example_dir
@pytest.fixture() @pytest.fixture()
@@ -52,6 +54,46 @@ def test_rename_mod(Script, dir_with_content):
''').format(dir=dir_with_content) ''').format(dir=dir_with_content)
def test_namespace_package(Script, tmpdir, skip_pre_python38):
origin = get_example_dir('implicit_namespace_package')
shutil.copytree(origin, tmpdir.strpath, dirs_exist_ok=True)
sys_path = [
os.path.join(tmpdir.strpath, 'ns1'),
os.path.join(tmpdir.strpath, 'ns2')
]
script_path = os.path.join(tmpdir.strpath, 'script.py')
script = Script(
'import pkg\n',
path=script_path,
project=jedi.Project(os.path.join(tmpdir.strpath, 'does-not-exist'), sys_path=sys_path),
)
refactoring = script.rename(line=1, new_name='new_pkg')
refactoring.apply()
old1 = os.path.join(sys_path[0], "pkg")
new1 = os.path.join(sys_path[0], "new_pkg")
old2 = os.path.join(sys_path[1], "pkg")
new2 = os.path.join(sys_path[1], "new_pkg")
assert not os.path.exists(old1)
assert os.path.exists(new1)
assert not os.path.exists(old2)
assert os.path.exists(new2)
changed, = iter(refactoring.get_changed_files().values())
assert changed.get_new_code() == "import new_pkg\n"
assert refactoring.get_diff() == dedent(f'''\
rename from {old1}
rename to {new1}
rename from {old2}
rename to {new2}
--- {script_path}
+++ {script_path}
@@ -1,2 +1,2 @@
-import pkg
+import new_pkg
''').format(dir=dir_with_content)
def test_rename_none_path(Script): def test_rename_none_path(Script):
refactoring = Script('foo', path=None).rename(new_name='bar') refactoring = Script('foo', path=None).rename(new_name='bar')
with pytest.raises(jedi.RefactoringError, match='on a Script with path=None'): with pytest.raises(jedi.RefactoringError, match='on a Script with path=None'):