diff --git a/README.mdown b/README.mdown index 0f8a1948..e4709362 100644 --- a/README.mdown +++ b/README.mdown @@ -53,11 +53,11 @@ Jedi supports many of the widely used Python features: I don't know what to do with them.) - class decorators (py3k feature, are being ignored too, until I find a use case, that doesn't work with Jedi) + - simple/usual `sys.path` modifications However, it does not yet support (and probably will in future versions, because they are on my todo list): - - `sys.path` modifications - assert / isinstance - manipulations of instances outside the instance variables, without using functions diff --git a/dynamic.py b/dynamic.py index 502dd434..af3985c3 100644 --- a/dynamic.py +++ b/dynamic.py @@ -10,6 +10,7 @@ import re import os import parsing +import modules import evaluate import helpers import settings diff --git a/functions.py b/functions.py index 8da0a3b7..0d5bea98 100644 --- a/functions.py +++ b/functions.py @@ -1,13 +1,12 @@ import re import weakref -import os import parsing -import dynamic # must be before evaluate, because it needs to be loaded first. +import dynamic +import imports import evaluate import modules import debug -import imports import settings import keywords diff --git a/imports.py b/imports.py index b762fdd2..c1818889 100644 --- a/imports.py +++ b/imports.py @@ -115,13 +115,18 @@ class ImportPath(object): Get the names of all modules in the search_path. This means file names and not names defined in the files. """ + if not search_path: + search_path = self.sys_path_with_modifications() names = [] for module_loader, name, is_pkg in pkgutil.iter_modules(search_path): - inf = float('inf') - inf_pos = (inf, inf) + inf_pos = (float('inf'), float('inf')) names.append(parsing.Name([(name, inf_pos)], inf_pos, inf_pos)) return names + def sys_path_with_modifications(self): + module = self.import_stmt.get_parent_until() + return modules.sys_path_with_modifications(module) + def follow(self): """ Returns the imported modules. diff --git a/modules.py b/modules.py index 694d7c4c..1453e798 100644 --- a/modules.py +++ b/modules.py @@ -1,11 +1,16 @@ from __future__ import with_statement + +from _compatibility import exec_function + import re import tokenize import sys +import os import parsing import builtin import debug +import evaluate class Module(builtin.CachedModule): @@ -170,3 +175,64 @@ class ModuleWithCursor(Module): return self._line_cache[line_nr - 1] except IndexError: raise StopIteration() + + +@evaluate.memoize_default([]) +def sys_path_with_modifications(module): + def execute_code(code): + c = "import os; from os.path import *; result=%s" + variables = {} + try: + exec_function(c % code, variables) + except Exception: + return None + try: + return os.path.abspath(variables['result']) + except KeyError: + return None + + curdir = os.path.abspath(os.curdir) + try: + os.chdir(os.path.dirname(module.path)) + except OSError: + pass + sys_path = list(builtin.module_find_path) # copy + try: + possible_stmts = module.used_names['path'] + except KeyError: + return [] + + for p in possible_stmts: + try: + call = p.get_assignment_calls().get_only_subelement() + except AttributeError: + continue + n = call.name + if not isinstance(n, parsing.Name) or len(n.names) != 3: + continue + if n.names[:2] != ('sys', 'path'): + continue + array_cmd = n.names[2] + if call.execution is None: + continue + exe = call.execution + if not (array_cmd == 'insert' and len(exe) == 2 \ + or array_cmd == 'append' and len(exe) == 1): + continue + + if array_cmd == 'insert': + exe_type, exe.type = exe.type, parsing.Array.NOARRAY + exe_pop = exe.values.pop(0) + res = execute_code(exe.get_code()) + if res: + sys_path.insert(0, res) + exe.type = exe_type + exe.values.insert(0, exe_pop) + elif array_cmd == 'append': + res = execute_code(exe.get_code()) + if res: + sys_path.append(res) + + # cleanup, back to old directory + os.chdir(curdir) + return sys_path diff --git a/parsing.py b/parsing.py index 9b882758..90883d64 100644 --- a/parsing.py +++ b/parsing.py @@ -900,6 +900,17 @@ class Call(Base): for y in self.next.generate_call_path(): yield y + def get_code(self): + if self.type == Call.NAME: + s = self.name.get_code() + else: + s = repr(self.name) + if self.execution is not None: + s += '(%s)' % self.execution.get_code() + if self.next is not None: + s += self.next.get_code() + return s + def __repr__(self): return "<%s: %s>" % \ (self.__class__.__name__, self.name) @@ -991,6 +1002,34 @@ class Array(Call): else: return iter(self.values) + def get_code(self): + def to_str(el): + try: + return el.get_code() + except AttributeError: + return str(el) + + map = {Array.NOARRAY: '%s', + Array.TUPLE: '(%s)', + Array.LIST: '[%s]', + Array.DICT: '{%s}', + Array.SET: '{%s}' + } + inner = [] + for i, value in enumerate(self.values): + s = '' + try: + key = self.keys[i] + except IndexError: + pass + else: + for el in key[i]: + s += to_str(el) + for el in value: + s += to_str(el) + inner.append(s) + return map[self.type] % ', '.join(inner) + def __repr__(self): if self.type == self.NOARRAY: type = 'noarray' diff --git a/test/completion/sys_path.py b/test/completion/sys_path.py new file mode 100644 index 00000000..7ad63034 --- /dev/null +++ b/test/completion/sys_path.py @@ -0,0 +1,22 @@ + +import sys +import os +from os import dirname + +sys.path.insert(0, '../..') +sys.path.append(dirname(os.path.abspath('thirdparty' + os.path.sep + 'asdf'))) + +# modifications, that should fail: +# because of sys module +sys.path.append(sys.path[1] + '/thirdparty') +# syntax err +sys.path.append('a' +* '/thirdparty') + +#? ['evaluate'] +import evaluate + +#? ['goto'] +evaluate.goto + +#? ['pylab_'] +import pylab_ diff --git a/test/run.py b/test/run.py index ff7ccad5..5427311e 100755 --- a/test/run.py +++ b/test/run.py @@ -12,6 +12,8 @@ from _compatibility import unicode, BytesIO, reduce, literal_eval, is_py25 import functions import debug +sys.path.pop() # pop again, because it might affect the completion + def run_completion_test(correct, source, line_nr, index, line, path): """