diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e7311ec..92aca3eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # **Upcoming release** +- Honor native module and namespace precedence when resolving imports: regular packages and modules win over namespace-package directories, and namespace portions are combined across source folders - #895 patchedast Starred and keyword now consumes their syntactically expected and ** (@lieryan) - #896 patchedast cleanup and refactoring (@lieryan) diff --git a/rope/base/project.py b/rope/base/project.py index ea19722ac..f01abf94f 100644 --- a/rope/base/project.py +++ b/rope/base/project.py @@ -6,7 +6,7 @@ import sys import warnings from contextlib import ExitStack -from typing import Optional +from typing import Optional, Union import rope.base.fscommands # Use full qualification for clarity. import rope.base.resourceobserver as resourceobserver @@ -142,24 +142,19 @@ def get_relative_module(self, name, folder, level): raise ModuleNotFoundError("Module %s not found" % name) return self.pycore.resource_to_pyobject(module) - def find_module(self, modname, folder=None) -> Optional[File]: + def find_module(self, modname, folder=None) -> Optional[Union[File, Folder]]: """Returns a resource corresponding to the given module returns None if it can not be found """ - for src in self.get_source_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - for src in self.get_python_path_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - if folder is not None: - module = _find_module_in_folder(folder, modname) - if module is not None: - return module - return None + + def folders(): + yield from self.get_source_folders() + yield from self.get_python_path_folders() + if folder is not None: + yield folder + + return _find_module_in_folders(folders(), modname) def find_relative_module(self, modname, folder, level): for i in range(level - 1): @@ -167,7 +162,7 @@ def find_relative_module(self, modname, folder, level): if modname == "": return folder else: - return _find_module_in_folder(folder, modname) + return _find_module_in_folders([folder], modname) def is_ignored(self, resource): return False @@ -437,22 +432,31 @@ def _realpath(path): return os.path.realpath(os.path.abspath(os.path.expanduser(path))) -def _find_module_in_folder(folder, modname): - module = folder - packages = modname.split(".") - for pkg in packages[:-1]: - if module.is_folder() and module.has_child(pkg): - module = module.get_child(pkg) +def _find_module_in_folders(folders, modname): + for name in modname.split("."): + namespaces = [] + for folder in folders: + if not folder.is_folder(): + continue + package = folder.get_child(name) if folder.has_child(name) else None + if package is not None and package.is_folder(): + if ( + package.has_child("__init__.py") + and not package.get_child("__init__.py").is_folder() + ): + module = package + folders = [package] + break + namespaces.append(package) + if folder.has_child(name + ".py"): + source = folder.get_child(name + ".py") + if not source.is_folder(): + module = source + folders = [] + break else: - return None - if module.is_folder(): - if ( - module.has_child(packages[-1]) - and module.get_child(packages[-1]).is_folder() - ): - return module.get_child(packages[-1]) - elif ( - module.has_child(packages[-1] + ".py") - and not module.get_child(packages[-1] + ".py").is_folder() - ): - return module.get_child(packages[-1] + ".py") + if not namespaces: + return None + module = namespaces[0] + folders = namespaces + return module diff --git a/ropetest/pycoretest.py b/ropetest/pycoretest.py index 5ccd80d2a..0fa6e8314 100644 --- a/ropetest/pycoretest.py +++ b/ropetest/pycoretest.py @@ -4,6 +4,7 @@ from rope.base import exceptions, libutils from rope.base.builtins import BuiltinClass, File +from rope.base.project import Project from rope.base.pycore import _TextChangeDetector from rope.base.pynamesdef import AssignedName from rope.base.pyobjects import AbstractFunction, get_base_type @@ -330,6 +331,81 @@ def test_find_module_when_module_and_package_with_the_same_name(self): found_module = self.project.find_module("sample") self.assertEqual(samplepkg, found_module) + def test_find_module_prefers_module_to_namespace_directory(self): + self.project.root.create_folder("sample") + samplemod = testutils.create_module(self.project, "sample") + self.assertEqual(samplemod, self.project.find_module("sample")) + + def test_find_module_retains_namespace_directory(self): + namespace = self.project.root.create_folder("sample") + self.project.close() + self.project = Project(self.project.address, source_folders=[""]) + self.assertEqual(namespace, self.project.find_module("sample")) + + def test_find_nested_module_does_not_search_shadowed_namespace(self): + namespace = self.project.root.create_folder("sample") + testutils.create_module(self.project, "child", namespace) + testutils.create_module(self.project, "sample") + self.assertIsNone(self.project.find_module("sample.child")) + + def test_find_module_defers_namespace_to_later_source_module(self): + first = self.project.root.create_folder("first") + second = self.project.root.create_folder("second") + first.create_folder("sample") + samplemod = testutils.create_module(self.project, "sample", second) + self.project.close() + self.project = Project(self.project.address, source_folders=["first", "second"]) + self.assertEqual(samplemod, self.project.find_module("sample")) + + def test_find_module_defers_namespace_to_later_regular_package(self): + first = self.project.root.create_folder("first") + second = self.project.root.create_folder("second") + namespace = first.create_folder("sample") + testutils.create_module(self.project, "child", namespace) + package = testutils.create_package(self.project, "sample", second) + self.project.close() + self.project = Project(self.project.address, source_folders=["first", "second"]) + self.assertEqual(package, self.project.find_module("sample")) + self.assertIsNone(self.project.find_module("sample.child")) + + def test_find_nested_module_searches_namespace_portions(self): + first = self.project.root.create_folder("first") + second = self.project.root.create_folder("second") + first.create_folder("sample") + namespace = second.create_folder("sample") + samplemod = testutils.create_module(self.project, "child", namespace) + self.project.close() + self.project = Project(self.project.address, source_folders=["first", "second"]) + self.assertEqual(samplemod, self.project.find_module("sample.child")) + + def test_relative_import_prefers_config_module_to_namespace_directory(self): + package = testutils.create_package(self.project, "sample") + package.create_folder("config") + config = testutils.create_module(self.project, "config", package) + config.write("class FlextCliConfig:\n pass\n") + consumer = testutils.create_module(self.project, "consumer", package) + consumer.write("from .config import FlextCliConfig\n") + expected = self.project.get_pymodule(config)["FlextCliConfig"].get_object() + actual = self.project.get_pymodule(consumer)["FlextCliConfig"].get_object() + self.assertEqual(expected, actual) + + def test_relative_import_keeps_regular_package_precedence(self): + package = testutils.create_package(self.project, "sample") + testutils.create_module(self.project, "config", package) + config = testutils.create_package(self.project, "config", package) + config.get_child("__init__.py").write("class Configuration:\n pass\n") + consumer = testutils.create_module(self.project, "consumer", package) + consumer.write("from .config import Configuration\n") + expected = self.project.get_pymodule(config)["Configuration"].get_object() + actual = self.project.get_pymodule(consumer)["Configuration"].get_object() + self.assertEqual(expected, actual) + + def test_relative_module_does_not_search_unrelated_source_roots(self): + package = testutils.create_package(self.project, "sample") + testutils.create_module(self.project, "config") + with self.assertRaises(exceptions.ModuleNotFoundError): + self.project.get_relative_module("config", package, 1) + def test_source_folders_preference(self): testutils.create_package(self.project, "pkg1") testutils.create_package(self.project, "pkg1.src2")