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.
This commit is contained in:
Dave Halter
2018-07-21 00:16:10 +02:00
parent ad5170a37a
commit 4b276bae87
6 changed files with 56 additions and 22 deletions

View File

@@ -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)
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:
# We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs
if not spec.has_location:
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

View File

@@ -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

View File

@@ -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_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.

View File

@@ -0,0 +1 @@
from .rel2 import name

View File

@@ -0,0 +1 @@
name = 1

View File

@@ -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'