From 5f19237a3ee44a08c7d0bc8a1f47c2339e75b8fb Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 27 Jul 2023 03:06:27 +0200 Subject: [PATCH] Fix renaming of namespace packages, fixes #1779 --- jedi/api/classes.py | 3 +-- jedi/api/refactoring/__init__.py | 28 ++++++++++++++++----- test/completion/on_import.py | 2 +- test/test_api/test_refactoring.py | 42 +++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index ee741c33..7054788e 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -105,8 +105,7 @@ class BaseName: # Compiled modules should not return a module path even if they # have one. path: Optional[Path] = self._get_module_context().py__file__() - if path is not None: - return path + return path return None diff --git a/jedi/api/refactoring/__init__.py b/jedi/api/refactoring/__init__.py index 9e08cc21..7983e233 100644 --- a/jedi/api/refactoring/__init__.py +++ b/jedi/api/refactoring/__init__.py @@ -5,6 +5,7 @@ from typing import Dict, Iterable, Tuple from parso import split_lines from jedi.api.exceptions import RefactoringError +from jedi.inference.value.namespace import ImplicitNSName EXPRESSION_PARTS = ( 'or_test and_test not_test comparison ' @@ -102,7 +103,12 @@ class Refactoring: to_path=calculate_to_path(path), module_node=next(iter(map_)).get_root_node(), 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]]: @@ -116,7 +122,7 @@ class Refactoring: project_path = self._inference_state.project.path for from_, to in self.get_renames(): 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()) @@ -146,13 +152,17 @@ def rename(inference_state, definitions, new_name): raise RefactoringError("There is no name under the cursor") 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 - if d.type == 'module' and tree_name is None: - p = None if d.module_path is None else Path(d.module_path) + if d.type == 'module' and tree_name is None and d.module_path is not None: + p = Path(d.module_path) 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: - # 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: fmap = file_tree_name_map.setdefault(d.module_path, {}) 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". """ 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 diff --git a/test/completion/on_import.py b/test/completion/on_import.py index 91524df8..e7279b71 100644 --- a/test/completion/on_import.py +++ b/test/completion/on_import.py @@ -76,7 +76,7 @@ from import_tree.pkg.mod1 import not_existant, #? 22 ['mod1', 'base'] from import_tree.pkg. import mod1 #? 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'] from import_tree.p import pkg diff --git a/test/test_api/test_refactoring.py b/test/test_api/test_refactoring.py index 13b74a40..d2b54ff9 100644 --- a/test/test_api/test_refactoring.py +++ b/test/test_api/test_refactoring.py @@ -1,4 +1,5 @@ import os +import shutil from textwrap import dedent from pathlib import Path import platform @@ -6,6 +7,7 @@ import platform import pytest import jedi +from test.helpers import get_example_dir @pytest.fixture() @@ -52,6 +54,46 @@ def test_rename_mod(Script, 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): refactoring = Script('foo', path=None).rename(new_name='bar') with pytest.raises(jedi.RefactoringError, match='on a Script with path=None'):