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'