From 4b276bae87a3170672f7ddb3e00f5851fe24d562 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 21 Jul 2018 00:16:10 +0200 Subject: [PATCH] The import resolution for namespace packages was wrong With this change we can now include all parents of the script, which will make relative imports always work. Now the whole meta_path is scanned and not just importlib's PathFinder. Fixes #1183. --- jedi/_compatibility.py | 36 ++++++++++++------- jedi/api/project.py | 7 +--- jedi/evaluate/imports.py | 14 +++++--- .../namespace_package_relative_import/rel1.py | 1 + .../namespace_package_relative_import/rel2.py | 1 + test/test_evaluate/test_namespace_package.py | 19 ++++++++++ 6 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 test/examples/namespace_package_relative_import/rel1.py create mode 100644 test/examples/namespace_package_relative_import/rel2.py diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 6f42ddcd..b5dde6a4 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -34,24 +34,36 @@ class DummyFile(object): del self.loader -def find_module_py34(string, path=None, full_name=None): +def find_module_py34(string, path=None, full_name=None, is_global_search=True): spec = None loader = None - spec = importlib.machinery.PathFinder.find_spec(string, path) - if spec is not None: - # We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs - if not spec.has_location: - full_name = string if not path else full_name - implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path) - return None, implicit_ns_info, False + for finder in sys.meta_path: + if is_global_search and finder != importlib.machinery.PathFinder: + p = None + else: + p = path + try: + find_spec = finder.find_spec + except AttributeError: + # These are old-school clases that still have a different API, just + # ignore those. + continue + + spec = find_spec(string, p) + if spec is not None: + loader = spec.loader + if loader is None and not spec.has_location: + # This is a namespace package. + full_name = string if not path else full_name + implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path) + return None, implicit_ns_info, False + break - # we have found the tail end of the dotted path - loader = spec.loader return find_module_py33(string, path, loader) -def find_module_py33(string, path=None, loader=None, full_name=None): +def find_module_py33(string, path=None, loader=None, full_name=None, is_global_search=True): loader = loader or importlib.machinery.PathFinder.find_module(string, path) if loader is None and path is None: # Fallback to find builtins @@ -104,7 +116,7 @@ def find_module_py33(string, path=None, loader=None, full_name=None): return module_file, module_path, is_package -def find_module_pre_py33(string, path=None, full_name=None): +def find_module_pre_py33(string, path=None, full_name=None, is_global_search=True): # This import is here, because in other places it will raise a # DeprecationWarning. import imp diff --git a/jedi/api/project.py b/jedi/api/project.py index 3f2bc6a3..eed8f3f9 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -108,12 +108,7 @@ class Project(object): if evaluator.script_path is not None: suffixed += discover_buildout_paths(evaluator, evaluator.script_path) - traversed = [] - for parent in traverse_parents(evaluator.script_path): - traversed.append(parent) - if parent == self._path: - # Don't go futher than the project path. - break + traversed = list(traverse_parents(evaluator.script_path)) # AFAIK some libraries have imports like `foo.foo.bar`, which # leads to the conclusion to by default prefer longer paths diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index df2f0fcc..99791dc9 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -21,6 +21,7 @@ from jedi._compatibility import (FileNotFoundError, ImplicitNSInfo, force_unicode, unicode) from jedi import debug from jedi import settings +from jedi.common.utils import traverse_parents from jedi.parser_utils import get_cached_code_lines from jedi.evaluate import sys_path from jedi.evaluate import helpers @@ -264,8 +265,11 @@ class Importer(object): ) def sys_path_with_modifications(self): - sys_path_mod = self._evaluator.get_sys_path() \ - + sys_path.check_sys_path_modifications(self.module_context) + + sys_path_mod = ( + self._evaluator.get_sys_path() + + sys_path.check_sys_path_modifications(self.module_context) + ) if self.import_path and self.file_path is not None \ and self._evaluator.environment.version_info.major == 2: @@ -350,7 +354,8 @@ class Importer(object): code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( string=import_parts[-1], path=path, - full_name=module_name + full_name=module_name, + is_global_search=False, ) if module_path is not None: break @@ -358,13 +363,14 @@ class Importer(object): _add_error(self.module_context, import_path[-1]) return NO_CONTEXTS else: - debug.dbg('search_module %s in %s', import_parts[-1], self.file_path) + debug.dbg('global search_module %s in %s', import_parts[-1], self.file_path) # Override the sys.path. It works only good that way. # Injecting the path directly into `find_module` did not work. code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( string=import_parts[-1], full_name=module_name, sys_path=sys_path, + is_global_search=True, ) if module_path is None: # The module is not a package. diff --git a/test/examples/namespace_package_relative_import/rel1.py b/test/examples/namespace_package_relative_import/rel1.py new file mode 100644 index 00000000..79b35a58 --- /dev/null +++ b/test/examples/namespace_package_relative_import/rel1.py @@ -0,0 +1 @@ +from .rel2 import name diff --git a/test/examples/namespace_package_relative_import/rel2.py b/test/examples/namespace_package_relative_import/rel2.py new file mode 100644 index 00000000..14a0ee4a --- /dev/null +++ b/test/examples/namespace_package_relative_import/rel2.py @@ -0,0 +1 @@ +name = 1 diff --git a/test/test_evaluate/test_namespace_package.py b/test/test_evaluate/test_namespace_package.py index 4500c297..30f3b924 100644 --- a/test/test_evaluate/test_namespace_package.py +++ b/test/test_evaluate/test_namespace_package.py @@ -1,6 +1,9 @@ from os.path import dirname, join import pytest +import py + +from ..helpers import get_example_dir SYS_PATH = [join(dirname(__file__), d) @@ -72,3 +75,19 @@ def test_nested_namespace_package(Script): result = script.goto_definitions() assert len(result) == 1 + + +def test_relative_import(Script, tmpdir): + """ + Attempt a relative import in a very simple namespace package. + """ + directory = get_example_dir('namespace_package_relative_import') + # Need to copy the content in a directory where there's no __init__.py. + py.path.local(directory).copy(tmpdir) + file_path = join(tmpdir.strpath, "rel1.py") + script = Script(path=file_path, line=1) + d, = script.goto_definitions() + assert d.name == 'int' + d, = script.goto_assignments() + assert d.name == 'name' + assert d.module_name == 'rel2'