diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index f464b08b8..d2b42e438 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -245,7 +245,42 @@ def _sys_path_with_modifications(self, is_completion): ) def follow(self): - if not self.import_path or not self._infer_possible: + if not self.import_path: + if self._fixed_sys_path: + # This is a bit of a special case, that maybe should be + # revisited. If the project path is wrong or the user uses + # relative imports the wrong way, we might end up here, where + # the `fixed_sys_path == project.path` in that case we kind of + # use the project.path.parent directory as our path. This is + # usually not a problem, except if imports in other places are + # using the same names. Example: + # + # foo/ < #1 + # - setup.py + # - foo/ < #2 + # - __init__.py + # - foo.py < #3 + # + # If the top foo is our project folder and somebody uses + # `from . import foo` in `setup.py`, it will resolve to foo #2, + # which means that the import for foo.foo is cached as + # `__init__.py` (#2) and not as `foo.py` (#3). This is usually + # not an issue, because this case is probably pretty rare, but + # might be an issue for some people. + # + # However for most normal cases where we work with different + # file names, this code path hits where we basically change the + # project path to an ancestor of project path. + from jedi.inference.value.namespace import ImplicitNamespaceValue + import_path = (os.path.basename(self._fixed_sys_path[0]),) + ns = ImplicitNamespaceValue( + self._inference_state, + string_names=import_path, + paths=self._fixed_sys_path, + ) + return ValueSet({ns}) + return NO_VALUES + if not self._infer_possible: return NO_VALUES # Check caches first diff --git a/test/test_api/test_usages.py b/test/test_api/test_usages.py index e38683331..ed789c660 100644 --- a/test/test_api/test_usages.py +++ b/test/test_api/test_usages.py @@ -1,9 +1,11 @@ import pytest +from ..helpers import test_dir + def test_import_references(Script): - s = Script("from .. import foo", path="foo.py") - assert [usage.line for usage in s.get_references(line=1, column=18)] == [1] + s = Script("from .. import foo", path=test_dir.joinpath("foo.py")) + assert [usage.line for usage in s.get_references()] == [1] def test_exclude_builtin_modules(Script): diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index 07ced3240..b783ac5f2 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -8,6 +8,7 @@ import pytest +import jedi from jedi.file_io import FileIO from jedi.inference import compiled from jedi.inference import imports @@ -473,6 +474,26 @@ def test_relative_import_star(Script): assert script.complete(3, len("furl.c")) +@pytest.mark.parametrize('with_init', [False, True]) +def test_relative_imports_without_path_and_setup_py( + Script, inference_state, environment, tmpdir, with_init): + # Contrary to other tests here we create a temporary folder that is not + # part of a folder with a setup.py that signifies + tmpdir.join('file1.py').write('do_foo = 1') + other_path = tmpdir.join('other_files') + other_path.join('file2.py').write('def do_nothing():\n pass', ensure=True) + if with_init: + other_path.join('__init__.py').write('') + + for name, code in [('file2', 'from . import file2'), + ('file1', 'from .. import file1')]: + for func in (jedi.Script.goto, jedi.Script.infer): + n, = func(Script(code, path=other_path.join('test1.py').strpath)) + assert n.name == name + assert n.type == 'module' + assert n.line == 1 + + def test_import_recursion(Script): path = get_example_dir('import-recursion', "cq_example.py") for c in Script(path=path).complete(3, 3):