From 78e87d0ab8f1aecff66bd73ae0e2b396772985e7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 20 Oct 2020 01:00:22 +0200 Subject: [PATCH 1/5] Relative imports should work even if they are not within the project --- jedi/inference/imports.py | 33 ++++++++++++++++++++++++++++- test/test_api/test_usages.py | 2 +- test/test_inference/test_imports.py | 21 ++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index 5ae5819fa..d360f7873 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -245,7 +245,38 @@ 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 imorts 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. + 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..82aed0fdc 100644 --- a/test/test_api/test_usages.py +++ b/test/test_api/test_usages.py @@ -3,7 +3,7 @@ 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] + 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 f1eb3f575..36016b160 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 @@ -468,3 +469,23 @@ def test_relative_import_star(Script): script = Script(source, path='export.py') 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 From bf5610342826523a3b46e1d296ce58346f496e1e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 21 Oct 2020 22:32:24 +0200 Subject: [PATCH 2/5] Update jedi/inference/imports.py Co-authored-by: Peter Law --- jedi/inference/imports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index d360f7873..890735970 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -252,7 +252,7 @@ def follow(self): # 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 imorts in other places are + # usually not a problem, except if imports in other places are # using the same names. Example: # # foo/ < #1 From 5f2f4af851baa7d48f67462b206dd39271787d4d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 21 Oct 2020 22:32:33 +0200 Subject: [PATCH 3/5] Update test/test_inference/test_imports.py Co-authored-by: Peter Law --- test/test_inference/test_imports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index 36016b160..bbc337acc 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -475,7 +475,7 @@ def test_relative_import_star(Script): 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 + # 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) From 43ff2833f393a87fc39dcc58987c06fd568301a8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 23 Oct 2020 16:29:51 +0200 Subject: [PATCH 4/5] Make a test more reliable --- test/test_api/test_usages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_api/test_usages.py b/test/test_api/test_usages.py index 82aed0fdc..ed789c660 100644 --- a/test/test_api/test_usages.py +++ b/test/test_api/test_usages.py @@ -1,8 +1,10 @@ import pytest +from ..helpers import test_dir + def test_import_references(Script): - s = Script("from .. import foo", path="foo.py") + s = Script("from .. import foo", path=test_dir.joinpath("foo.py")) assert [usage.line for usage in s.get_references()] == [1] From ce0ed4b8aef6ef9abeffcd5810c8c6f493dddc47 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 10 Dec 2020 16:57:09 +0100 Subject: [PATCH 5/5] Improve a comment --- jedi/inference/imports.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index 890735970..8309473fc 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -267,6 +267,10 @@ def follow(self): # `__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(