diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index cbb3b7a8..8f089810 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -111,7 +111,9 @@ class Script(object): # Load the Python grammar of the current interpreter. self._grammar = parso.load_grammar() - self._evaluator = Evaluator(self._grammar, Project(sys_path=sys_path)) + project = Project(sys_path=sys_path) + self._evaluator = Evaluator(self._grammar, project) + project.add_script_path(self.path) debug.speed('init') @cache.memoize_method diff --git a/jedi/cache.py b/jedi/cache.py index f875a638..01138e75 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -12,7 +12,6 @@ there are global variables, which are holding the cache information. Some of these variables are being cleaned after every API usage. """ import time -import inspect from jedi import settings from parso.cache import parser_cache @@ -46,8 +45,6 @@ def underscore_memoization(func): return getattr(self, name) except AttributeError: result = func(self) - if inspect.isgenerator(result): - result = list(result) setattr(self, name, result) return result diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index d0825797..d1453f32 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -62,7 +62,6 @@ only *evaluates* what needs to be *evaluated*. All the statements and modules that are not used are just being ignored. """ -import copy import sys from parso.python import tree @@ -100,8 +99,8 @@ class Evaluator(object): self.dynamic_params_depth = 0 self.is_analysis = False self.python_version = sys.version_info[:2] - self.project = project + project.add_evaluator(self) self.reset_recursion_limitations() diff --git a/jedi/evaluate/project.py b/jedi/evaluate/project.py index e76aab75..fbb0b59d 100644 --- a/jedi/evaluate/project.py +++ b/jedi/evaluate/project.py @@ -1,7 +1,8 @@ import os import sys -from jedi.evaluate.sys_path import get_venv_path +from jedi.evaluate.sys_path import get_venv_path, detect_additional_paths +from jedi.cache import underscore_memoization class Project(object): @@ -14,11 +15,24 @@ class Project(object): if sys_path is None: sys_path = sys.path - sys_path = list(sys_path) - + base_sys_path = list(sys_path) try: - sys_path.remove('') + base_sys_path.remove('') except ValueError: pass - self.sys_path = sys_path + self._base_sys_path = base_sys_path + + def add_script_path(self, script_path): + self._script_path = script_path + + def add_evaluator(self, evaluator): + self._evaluator = evaluator + + @property + @underscore_memoization + def sys_path(self): + if self._script_path is None: + return self._base_sys_path + + return self._base_sys_path + detect_additional_paths(self._evaluator, self._script_path) diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index 842a9ae2..748a5457 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -6,7 +6,6 @@ from jedi.evaluate.site import addsitedir from jedi._compatibility import exec_function, unicode from jedi.evaluate.cache import evaluator_function_cache -from jedi.evaluate.compiled import CompiledObject from jedi.evaluate.base_context import ContextualizedNode from jedi import settings from jedi import debug @@ -80,12 +79,24 @@ def _execute_code(module_path, code): try: res = variables['result'] if isinstance(res, str): - return [os.path.abspath(res)] + return _abs_path(module_path, res) except KeyError: pass - return [] + return None +def _abs_path(module_path, path): + if os.path.isabs(path): + return path + + if module_path is None: + # In this case we have no idea where we actually are in the file + # system. + return None + + base_dir = os.path.dirname(module_path) + return os.path.abspath(os.path.join(base_dir, path)) + def _paths_from_assignment(module_context, expr_stmt): """ Extracts the assigned strings from an assignment that looks as follows:: @@ -125,7 +136,9 @@ def _paths_from_assignment(module_context, expr_stmt): for lazy_context in cn.infer().iterate(cn): for context in lazy_context.infer(): if is_string(context): - yield context.obj + abs_path = _abs_path(module_context.py__file__(), context.obj) + if abs_path is not None: + yield abs_path def _paths_from_list_modifications(module_path, trailer1, trailer2): @@ -143,10 +156,11 @@ def _paths_from_list_modifications(module_path, trailer1, trailer2): arg = trailer2.children[1] if name == 'insert' and len(arg.children) in (3, 4): # Possible trailing comma. arg = arg.children[2] - return _execute_code(module_path, arg.get_code()) + path = _execute_code(module_path, arg.get_code()) + return [] if path is None else [path] -def _check_module(module_context): +def check_sys_path_modifications(module_context): """ Detect sys.path modifications within module. """ @@ -161,10 +175,10 @@ def _check_module(module_context): if n.type == 'name' and n.value == 'path': yield name, power - sys_path = list(module_context.evaluator.project.sys_path) # copy - if isinstance(module_context, CompiledObject): - return sys_path + if module_context.tree_node is None: + return [] + added = [] try: possible_names = module_context.tree_node.get_used_names()['path'] except KeyError: @@ -173,39 +187,30 @@ def _check_module(module_context): for name, power in get_sys_path_powers(possible_names): expr_stmt = power.parent if len(power.children) >= 4: - sys_path.extend( + added.extend( _paths_from_list_modifications( module_context.py__file__(), *power.children[2:4] ) ) elif expr_stmt is not None and expr_stmt.type == 'expr_stmt': - sys_path.extend(_paths_from_assignment(module_context, expr_stmt)) - return sys_path + added.extend(_paths_from_assignment(module_context, expr_stmt)) + return added @evaluator_function_cache(default=[]) def sys_path_with_modifications(evaluator, module_context): - path = module_context.py__file__() - if path is None: - # Support for modules without a path is bad, therefore return the - # normal path. - return evaluator.project.sys_path + return evaluator.project.sys_path + check_sys_path_modifications(module_context) - curdir = os.path.abspath(os.curdir) - #TODO why do we need a chdir? - with ignored(OSError): - os.chdir(os.path.dirname(path)) +def detect_additional_paths(evaluator, script_path): + django_paths = _detect_django_path(script_path) buildout_script_paths = set() - result = _check_module(module_context) - result += _detect_django_path(path) - for buildout_script_path in _get_buildout_script_paths(path): + for buildout_script_path in _get_buildout_script_paths(script_path): for path in _get_paths_from_buildout_script(evaluator, buildout_script_path): buildout_script_paths.add(path) - # cleanup, back to old directory - os.chdir(curdir) - return list(result) + list(buildout_script_paths) + + return django_paths + list(buildout_script_paths) def _get_paths_from_buildout_script(evaluator, buildout_script_path): @@ -220,7 +225,8 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): return from jedi.evaluate.context import ModuleContext - for path in _check_module(ModuleContext(evaluator, module_node, buildout_script_path)): + module = ModuleContext(evaluator, module_node, buildout_script_path) + for path in check_sys_path_modifications(module): yield path diff --git a/test/test_evaluate/test_buildout_detection.py b/test/test_evaluate/test_buildout_detection.py index e5e51ba7..bb65201e 100644 --- a/test/test_evaluate/test_buildout_detection.py +++ b/test/test_evaluate/test_buildout_detection.py @@ -1,25 +1,17 @@ import os from textwrap import dedent -import parso - -from jedi._compatibility import u +from jedi import Script from jedi.evaluate.sys_path import (_get_parent_dir_with_file, _get_buildout_script_paths, - sys_path_with_modifications, - _check_module) -from jedi.evaluate import Evaluator -from jedi.evaluate.project import Project -from jedi.evaluate.context.module import ModuleContext + check_sys_path_modifications) from ..helpers import cwd_at def check_module_test(code): - grammar = parso.load_grammar() - e = Evaluator(grammar, Project()) - module_context = ModuleContext(e, parso.parse(code), path=None) - return _check_module(module_context) + module_context = Script(code)._get_module() + return check_sys_path_modifications(module_context) @cwd_at('test/test_evaluate/buildout_project/src/proj_name') @@ -40,25 +32,27 @@ def test_buildout_detection(): def test_append_on_non_sys_path(): - code = dedent(u(""" + code = dedent(""" class Dummy(object): path = [] d = Dummy() - d.path.append('foo')""")) + d.path.append('foo')""" + ) paths = check_module_test(code) - assert len(paths) > 0 + assert not paths assert 'foo' not in paths def test_path_from_invalid_sys_path_assignment(): - code = dedent(u(""" + code = dedent(""" import sys - sys.path = 'invalid'""")) + sys.path = 'invalid'""" + ) paths = check_module_test(code) - assert len(paths) > 0 + assert not paths assert 'invalid' not in paths @@ -69,15 +63,12 @@ def test_sys_path_with_modifications(): """) path = os.path.abspath(os.path.join(os.curdir, 'module_name.py')) - grammar = parso.load_grammar() - module_node = parso.parse(code, path=path) - module_context = ModuleContext(Evaluator(grammar, Project()), module_node, path=path) - paths = sys_path_with_modifications(module_context.evaluator, module_context) + paths = Script(code, path=path)._evaluator.project.sys_path assert '/tmp/.buildout/eggs/important_package.egg' in paths def test_path_from_sys_path_assignment(): - code = dedent(u(""" + code = dedent(""" #!/usr/bin/python import sys @@ -91,7 +82,8 @@ def test_path_from_sys_path_assignment(): import important_package if __name__ == '__main__': - sys.exit(important_package.main())""")) + sys.exit(important_package.main())""" + ) paths = check_module_test(code) assert 1 not in paths diff --git a/test/test_evaluate/test_sys_path.py b/test/test_evaluate/test_sys_path.py index dc61f01e..7bedfa17 100644 --- a/test/test_evaluate/test_sys_path.py +++ b/test/test_evaluate/test_sys_path.py @@ -10,13 +10,13 @@ from jedi import Script def test_paths_from_assignment(): def paths(src): - script = Script(src) + script = Script(src, path='/foo/bar.py') expr_stmt = script._get_module_node().children[0] return set(sys_path._paths_from_assignment(script._get_module(), expr_stmt)) - assert paths('sys.path[0:0] = ["a"]') == set(['a']) - assert paths('sys.path = ["b", 1, x + 3, y, "c"]') == set(['b', 'c']) - assert paths('sys.path = a = ["a"]') == set(['a']) + assert paths('sys.path[0:0] = ["a"]') == set(['/foo/a']) + assert paths('sys.path = ["b", 1, x + 3, y, "c"]') == set(['/foo/b', '/foo/c']) + assert paths('sys.path = a = ["a"]') == set(['/foo/a']) # Fail for complicated examples. assert paths('sys.path, other = ["a"], 2') == set()