From 1914d1083673bb10994288b3055df021cf4d5169 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 8 Mar 2019 14:25:54 +0100 Subject: [PATCH] Fix relative imports outside of the proper paths --- jedi/api/__init__.py | 13 ++++++----- jedi/evaluate/imports.py | 28 +++++++++++------------ test/test_evaluate/test_imports.py | 36 ++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 82598a24..542c1401 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -82,7 +82,8 @@ class Script(object): :type sys_path: Environment """ def __init__(self, source=None, line=None, column=None, path=None, - encoding='utf-8', sys_path=None, environment=None): + encoding='utf-8', sys_path=None, environment=None, + _project=None): self._orig_path = path # An empty path (also empty string) should always result in no path. self.path = os.path.abspath(path) if path else None @@ -98,10 +99,12 @@ class Script(object): if sys_path is not None and not is_py3: sys_path = list(map(force_unicode, sys_path)) - # Load the Python grammar of the current interpreter. - project = get_default_project( - os.path.dirname(self.path)if path else os.getcwd() - ) + project = _project + if project is None: + # Load the Python grammar of the current interpreter. + project = get_default_project( + os.path.dirname(self.path)if path else os.getcwd() + ) # TODO deprecate and remove sys_path from the Script API. if sys_path is not None: project._sys_path = sys_path diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 0253d5ba..e6dbd419 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -211,7 +211,7 @@ def _level_to_base_import_path(project_path, directory, level): # import path for it. while True: if d == project_path: - return level_import_paths, None + return level_import_paths, d dir_name = os.path.basename(d) if dir_name: level_import_paths.insert(0, dir_name) @@ -280,18 +280,19 @@ class Importer(object): base_import_path, base_directory = _level_to_base_import_path( self._evaluator.project._path, directory, level, ) + if base_directory is None: + # Everything is lost, the relative import does point + # somewhere out of the filesystem. + self._inference_possible = False + else: + self._fixed_sys_path = [base_directory] + if base_import_path is None: if import_path: _add_error( module_context, import_path[0], message='Attempted relative import beyond top-level package.' ) - if base_directory is None: - # Everything is lost, the relative import does point - # somewhere out of the filesystem. - self._inference_possible = False - else: - self._fixed_sys_path = [base_directory] else: import_path = base_import_path + import_path self.import_path = import_path @@ -375,6 +376,9 @@ class Importer(object): :param only_modules: Indicates wheter it's possible to import a definition that is not defined in a module. """ + if not self._inference_possible: + return [] + names = [] if self.import_path: # flask @@ -416,15 +420,11 @@ class Importer(object): for filter in context.get_filters(search_global=False): names += filter.values() else: - # Empty import path=completion after import if self.level: - if self.file_path is not None: - path = self.file_path - for i in range(self.level): - path = os.path.dirname(path) - raise 1 - names += self._get_module_names([path]) + # We only get here if the level cannot be properly calculated. + names += self._get_module_names(self._fixed_sys_path) else: + # This is just the list of global imports. names += self._get_module_names() return names diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 1c0537b7..44941287 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -280,20 +280,38 @@ def test_get_modules_containing_name(evaluator, path, goal): @pytest.mark.parametrize('empty_sys_path', (False, True)) def test_relative_imports_with_multiple_similar_directories(Script, path, empty_sys_path): dir = get_example_dir('issue1209') + if empty_sys_path: + project = Project(dir, sys_path=(), smart_sys_path=False) + else: + project = Project(dir) script = Script( "from . ", path=os.path.join(dir, path), + _project=project, ) - # TODO pass this project to the script as a param once that's possible. - if empty_sys_path: - script._evaluator.project = Project(dir, smart_sys_path=False) - else: - script._evaluator.project = Project(dir) name, import_ = script.completions() assert import_.name == 'import' assert name.name == 'api_test1' +def test_relative_imports_x(Script): + dir = get_example_dir('issue1209') + project = Project(dir, sys_path=[], smart_sys_path=False) + script = Script( + "from ...", + path=os.path.join(dir, 'api/whatever/test_this.py'), + _project=project, + ) + assert [c.name for c in script.completions()] == ['api', 'import', 'whatever'] + + script = Script( + "from " + '.' * 100, + path=os.path.join(dir, 'api/whatever/test_this.py'), + _project=project, + ) + assert [c.name for c in script.completions()] == ['import'] + + @cwd_at('test/examples/issue1209/api/whatever/') def test_relative_imports_without_path(Script): script = Script("from . ") @@ -322,12 +340,12 @@ def test_relative_import_out_of_file_system(Script): @pytest.mark.parametrize( 'level, directory, project_path, result', [ - (1, '/a/b/c', '/a', (['b', 'c'], None)), - (2, '/a/b/c', '/a', (['b'], None)), - (3, '/a/b/c', '/a', ([], None)), + (1, '/a/b/c', '/a', (['b', 'c'], '/a')), + (2, '/a/b/c', '/a', (['b'], '/a')), + (3, '/a/b/c', '/a', ([], '/a')), (4, '/a/b/c', '/a', (None, '/')), (5, '/a/b/c', '/a', (None, None)), - (1, '/', '/', ([], None)), + (1, '/', '/', ([], '/')), (2, '/', '/', (None, None)), ] )