diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 90939079..82598a24 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -32,7 +32,7 @@ from jedi.evaluate import imports from jedi.evaluate import usages from jedi.evaluate.arguments import try_iter_content from jedi.evaluate.helpers import get_module_names, evaluate_call_of_leaf -from jedi.evaluate.sys_path import calculate_dotted_path_from_sys_path +from jedi.evaluate.sys_path import transform_path_to_dotted from jedi.evaluate.filters import TreeNameDefinition, ParamName from jedi.evaluate.syntax_tree import tree_name_to_contexts from jedi.evaluate.context import ModuleContext @@ -108,7 +108,6 @@ class Script(object): self._evaluator = Evaluator( project, environment=environment, script_path=self.path ) - self._project = project debug.speed('init') self._module_node, source = self._evaluator.parse_and_get_code( code=source, @@ -149,10 +148,7 @@ class Script(object): def _get_module(self): names = ('__main__',) if self.path is not None: - import_names = calculate_dotted_path_from_sys_path( - self._evaluator.get_sys_path(), - self.path - ) + import_names = transform_path_to_dotted(self._evaluator.get_sys_path(), self.path) if import_names is not None: names = import_names diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index a9201318..a93eca36 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -522,7 +522,7 @@ def get_modules_containing_name(evaluator, modules, name): if base_names: import_names = base_names + (module_name,) else: - import_names = sys_path.calculate_dotted_path_from_sys_path(e_sys_path, path) + import_names = sys_path.transform_path_to_dotted(e_sys_path, path) module = _load_module( evaluator, path, code, diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index 22d98c37..e5ceed81 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -197,38 +197,40 @@ def _get_buildout_script_paths(search_path): continue -def calculate_dotted_path_from_sys_path(sys_path, module_path): +def transform_path_to_dotted(sys_path, module_path): """ - Returns the dotted path inside a sys.path as a list of names. + Returns the dotted path inside a sys.path as a list of names. e.g. - This function is supposed to be a backup plan in case there's no idea where - a file is lying. + >>> transform_path_to_dotted(["/foo"], '/foo/bar/baz.py') + ('bar', 'baz') + + Returns None if the path doesn't really resolve to anything. """ # First remove the suffix. for suffix in all_suffixes(): if module_path.endswith(suffix): module_path = module_path[:-len(suffix)] break - else: - # There should always be a suffix in a valid Python file on the path. - return None + # Once the suffix was removed we are using the files as we know them. This + # means that if someone uses an ending like .vim for a Python file, .vim + # will be part of the returned dotted part. - if module_path.endswith('__init__'): + if module_path.endswith(os.path.sep + '__init__'): + # -1 to remove the separator module_path = module_path[:-len('__init__') - 1] - if module_path.endswith(os.path.sep): - # The paths in sys.path may end with a slash. - module_path = module_path[:-1] - for p in sys_path: if module_path.startswith(p): rest = module_path[len(p):] + # On Windows a path can also use a slash. if rest.startswith(os.path.sep) or rest.startswith('/'): + # Remove a slash in cases it's still there. rest = rest[1:] + if rest: split = rest.split(os.path.sep) for string in split: - if not string or '.' in string: + if not string: return None return tuple(split) return None diff --git a/test/examples/issue1209/__init__.py b/test/examples/issue1209/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/issue1209/api/__init__.py b/test/examples/issue1209/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/issue1209/api/whatever/__init__.py b/test/examples/issue1209/api/whatever/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/issue1209/api/whatever/api_test1.py b/test/examples/issue1209/api/whatever/api_test1.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/issue1209/whatever/__init__.py b/test/examples/issue1209/whatever/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/issue1209/whatever/test.py b/test/examples/issue1209/whatever/test.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py index a0f4e4d5..328333d2 100644 --- a/test/test_api/test_project.py +++ b/test/test_api/test_project.py @@ -12,4 +12,4 @@ def test_django_default_project(Script): ) c, = script.completions() assert c.name == "SomeModel" - assert script._project._django is True + assert script._evaluator.project._django is True diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index f0bb8f13..a4fe9660 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -10,7 +10,8 @@ import pytest from jedi._compatibility import find_module_py33, find_module from jedi.evaluate import compiled from jedi.evaluate import imports -from ..helpers import cwd_at +from jedi.api.project import Project +from ..helpers import cwd_at, get_example_dir THIS_DIR = os.path.dirname(__file__) @@ -272,3 +273,18 @@ def test_get_modules_containing_name(evaluator, path, goal): ) assert input_module is module assert found_module.string_names == goal + + +@pytest.mark.parametrize( + 'path', ('api/whatever/test_this.py', 'api/whatever/file')) +def test_relative_imports_with_multiple_similar_directories(Script, path): + dir = get_example_dir('issue1209') + script = Script( + "from .", + path=os.path.join(dir, path) + ) + # TODO pass this project to the script as a param once that's possible. + script._evaluator.project = Project(dir) + name, import_ = script.completions() + assert import_.name == 'import' + assert name.name == 'api_test1' diff --git a/test/test_evaluate/test_sys_path.py b/test/test_evaluate/test_sys_path.py index e6c7d6f0..b457e7f7 100644 --- a/test/test_evaluate/test_sys_path.py +++ b/test/test_evaluate/test_sys_path.py @@ -63,17 +63,34 @@ def test_venv_and_pths(venv_path): assert not set(sys.path).intersection(ETALON) -@pytest.mark.parametrize(('sys_path_', 'path', 'expected'), [ - (['/foo'], '/foo/bar.py', ('bar',)), - (['/foo'], '/foo/bar/baz.py', ('bar', 'baz')), - (['/foo'], '/foo/bar/__init__.py', ('bar',)), - (['/foo'], '/foo/bar/baz/__init__.py', ('bar', 'baz')), +_s = ['/a', '/b', '/c/d/'] - (['/foo'], '/foo/bar.so', ('bar',)), - (['/foo'], '/foo/bar/__init__.so', ('bar',)), - (['/foo'], '/x/bar.py', None), - (['/foo'], '/foo/bar.xyz', None), -]) -def test_calculate_dotted_path_from_sys_path(path, sys_path_, expected): - assert sys_path.calculate_dotted_path_from_sys_path(sys_path_, path) == expected +@pytest.mark.parametrize( + 'sys_path_, module_path, result', [ + (_s, '/a/b', ('b',)), + (_s, '/a/b/c', ('b', 'c')), + (_s, '/a/b.py', ('b',)), + (_s, '/a/b/c.py', ('b', 'c')), + (_s, '/x/b.py', None), + (_s, '/c/d/x.py', ('x',)), + (_s, '/c/d/x.py', ('x',)), + (_s, '/c/d/x/y.py', ('x', 'y')), + # If dots are in there they also resolve. These are obviously illegal + # in Python, but Jedi can handle them. Give the user a bit more freedom + # that he will have to correct eventually. + (_s, '/a/b.c.py', ('b.c',)), + (_s, '/a/b.d/foo.bar.py', ('b.d', 'foo.bar')), + + (_s, '/a/.py', None), + (_s, '/a/c/.py', None), + + (['/foo'], '/foo/bar/__init__.py', ('bar',)), + (['/foo'], '/foo/bar/baz/__init__.py', ('bar', 'baz')), + (['/foo'], '/foo/bar.so', ('bar',)), + (['/foo'], '/foo/bar/__init__.so', ('bar',)), + (['/foo'], '/x/bar.py', None), + (['/foo'], '/foo/bar.xyz', ('bar.xyz',)), + ]) +def test_calculate_dotted_from_path(sys_path_, module_path, result): + assert sys_path.transform_path_to_dotted(sys_path_, module_path) == result