diff --git a/jedi/imports.py b/jedi/imports.py index b0d6c5a7..f0e5341f 100644 --- a/jedi/imports.py +++ b/jedi/imports.py @@ -254,7 +254,11 @@ class ImportPath(pr.Base): if self.file_path: sys_path_mod = list(self.sys_path_with_modifications()) - sys_path_mod.insert(0, self.file_path) + module = self.import_stmt.get_parent_until() + if not module.has_explicit_absolute_import: + # If the module explicitly asks for absolute imports, + # there's probably a bogus local one. + sys_path_mod.insert(0, self.file_path) else: sys_path_mod = list(modules.get_sys_path()) diff --git a/jedi/parsing_representation.py b/jedi/parsing_representation.py index c7421d35..e51132f0 100644 --- a/jedi/parsing_representation.py +++ b/jedi/parsing_representation.py @@ -308,18 +308,17 @@ class Scope(Simple, IsScope): class Module(IsScope): - """ For isinstance checks. fast_parser.Module also inherits from this. """ - pass + """ + For isinstance checks. fast_parser.Module also inherits from this. + """ class SubModule(Scope, Module): - """ The top scope, which is always a module. Depending on the underlying parser this may be a full module or just a part of a module. """ - def __init__(self, path, start_pos=(1, 0), top_module=None): """ Initialize :class:`SubModule`. @@ -378,6 +377,22 @@ class SubModule(Scope, Module): def is_builtin(self): return not (self.path is None or self.path.endswith('.py')) + @property + def has_explicit_absolute_import(self): + """ + Checks if imports in this module are explicitly absolute, i.e. there + is a ``__future__`` import. + """ + for imp in self.imports: + if imp.from_ns is None or imp.namespace is None: + continue + + namespace, feature = imp.from_ns.names[0], imp.namespace.names[0] + if namespace == "__future__" and feature == "absolute_import": + return True + + return False + class Class(Scope): """ diff --git a/pytest.ini b/pytest.ini index b393f082..ab58a87e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,7 +2,7 @@ addopts = --doctest-modules # Ignore broken files in blackbox test directories -norecursedirs = .* docs completion refactor +norecursedirs = .* docs completion refactor absolute_import # Activate `clean_jedi_cache` fixture for all tests. This should be # fine as long as we are using `clean_jedi_cache` as a session scoped diff --git a/test/absolute_import/local_module.py b/test/absolute_import/local_module.py new file mode 100644 index 00000000..d256fd67 --- /dev/null +++ b/test/absolute_import/local_module.py @@ -0,0 +1,14 @@ +""" +This is a module that imports the *standard library* unittest, +despite there being a local "unittest" module. It specifies that it +wants the stdlib one with the ``absolute_import`` __future__ import. + +The twisted equivalent of this module is ``twisted.trial._synctest``. +""" +from __future__ import absolute_import + +import unittest # this is stdlib unittest, but jedi gets the local one + + +class Assertions(unittest.TestCase): + pass diff --git a/test/absolute_import/unittest.py b/test/absolute_import/unittest.py new file mode 100644 index 00000000..eee1e937 --- /dev/null +++ b/test/absolute_import/unittest.py @@ -0,0 +1,14 @@ +""" +This is a module that shadows a builtin (intentionally). + +It imports a local module, which in turn imports stdlib unittest (the +name shadowed by this module). If that is properly resolved, there's +no problem. However, if jedi doesn't understand absolute_imports, it +will get this module again, causing infinite recursion. +""" +from local_module import Assertions + + +class TestCase(Assertions): + def test(self): + self.assertT diff --git a/test/test_absolute_import.py b/test/test_absolute_import.py new file mode 100644 index 00000000..1fe9aa9d --- /dev/null +++ b/test/test_absolute_import.py @@ -0,0 +1,39 @@ +import jedi +from jedi.parsing import Parser +from . import base + + +def test_explicit_absolute_imports(): + """ + Detect modules with ``from __future__ import absolute_import``. + """ + parser = Parser("from __future__ import absolute_import", "test.py") + assert parser.module.has_explicit_absolute_import + + +def test_no_explicit_absolute_imports(): + """ + Detect modules without ``from __future__ import absolute_import``. + """ + parser = Parser("1", "test.py") + assert not parser.module.has_explicit_absolute_import + + +def test_dont_break_imports_without_namespaces(): + """ + The code checking for ``from __future__ import absolute_import`` shouldn't + assume that all imports have non-``None`` namespaces. + """ + src = "from __future__ import absolute_import\nimport xyzzy" + parser = Parser(src, "test.py") + assert parser.module.has_explicit_absolute_import + + +@base.cwd_at("test/absolute_import") +def test_can_complete_when_shadowing(): + filename = "unittest.py" + with open(filename) as f: + lines = f.readlines() + src = "".join(lines) + script = jedi.Script(src, len(lines), len(lines[1]), filename) + assert script.completions()