From 119455c3b33b600e9dce505a13105f1e33e8c22d Mon Sep 17 00:00:00 2001 From: David Halter Date: Wed, 12 Sep 2012 21:36:16 +0200 Subject: [PATCH] cleaned up and fixed most cases of import related stuff, fixes #2 --- README.mdown | 6 ++-- evaluate.py | 36 +++++------------------ functions.py | 34 +++++++++++++++++----- imports.py | 43 ++++++++++++++++++++-------- parsing.py | 17 ++++++++++- test/completion/imports.py | 21 +++++++++++++- test/completion/thirdparty/pylab_.py | 2 +- 7 files changed, 105 insertions(+), 54 deletions(-) diff --git a/README.mdown b/README.mdown index 5966d612..10292449 100644 --- a/README.mdown +++ b/README.mdown @@ -1,7 +1,7 @@ Jedi - a clever Python auto-completion ============================ -** now in alpha testing phase ** +**now in alpha testing phase** *If you have any comments or feature request, please tell me! I really want to know, what you think of Jedi.* @@ -55,8 +55,8 @@ Jedi supports many of the widely used Python features: However, it does not yet support (and probably will in future versions, because they are on my todo list): - - getattr() / __getattr__ / __getattribute__ - - sys.path modifications + - `getattr()` / `__getattr__` / `__getattribute__` + - `sys.path` modifications - manipulations of instances outside the instance variables, without using functions - mro diff --git a/evaluate.py b/evaluate.py index 5c3dcf7d..18e49466 100644 --- a/evaluate.py +++ b/evaluate.py @@ -1448,39 +1448,17 @@ def goto(scopes, search_name=None, statement_path_offset=1): except imports.ModuleNotFound: continue else: - try: + if not isinstance(s, parsing.Module): s = statement_path[0] - except IndexError: - pass definitions.append(s) else: - def remove_unreal_imports(names): - """ - These imports are only virtual, because of multi-line imports. - """ - new_names = [] - for n in names: - par = n.parent() - # This is a special case: If the Import is "virtual" (which - # means the position is not defined), follow those modules. - if isinstance(par, parsing.Import) and not par.start_pos[0]: - module_count = 0 - for scope in imports.ImportPath(par).follow(): - if isinstance(scope, parsing.Import): - temp = scope.get_defined_names() - new_names += remove_unreal_imports(temp) - elif isinstance(scope, parsing.Module) \ - and not module_count: - # only first module (others are star imports) - module_count += 1 - new_names.append(scope.get_module_name(n.names)) - else: - new_names.append(n) - return new_names - names = [] for s in scopes: - names += s.get_defined_names() - names = remove_unreal_imports(names) + if isinstance(s, imports.ImportPath): + modules = s.follow() + if modules: + names.append(modules[0].get_module_name()) + else: + names += s.get_defined_names() definitions = [n for n in names if n.names[-1] == search_name] return definitions diff --git a/functions.py b/functions.py index a3c860be..73a847f2 100644 --- a/functions.py +++ b/functions.py @@ -144,7 +144,7 @@ def complete(source, line, column, source_path): path, dot, like = _get_completion_parts(path) try: - scopes = _prepare_goto(source, pos, source_path, f, path, True) + scopes = _prepare_goto(pos, source_path, f, path, True) except NotFoundError: scope_generator = evaluate.get_names_for_scope(f.parser.user_scope, pos) @@ -159,7 +159,11 @@ def complete(source, line, column, source_path): # TODO is this really the right way? just ignore the functions? \ # do the magic functions first? and then recheck here? if not isinstance(s, evaluate.Function): - for c in s.get_defined_names(): + if isinstance(s, imports.ImportPath): + names = s.get_defined_names(on_import_stmt=True) + else: + names = s.get_defined_names() + for c in names: completions.append((c, s)) completions = [(c, s) for c, s in completions @@ -174,7 +178,7 @@ def complete(source, line, column, source_path): return c -def _prepare_goto(source, position, source_path, module, goto_path, +def _prepare_goto(position, source_path, module, goto_path, is_like_search=False): scope = module.parser.user_scope debug.dbg('start: %s in %s' % (goto_path, scope)) @@ -187,7 +191,23 @@ def _prepare_goto(source, position, source_path, module, goto_path, return [] if isinstance(user_stmt, parsing.Import): - scopes = [imports.ImportPath(user_stmt, is_like_search)] + import_names = user_stmt.get_all_import_names() + count = 0 + found_count = None + for i in import_names: + for name_part in i.names: + count += 1 + if name_part.start_pos <= position <= name_part.end_pos: + found_count = count + if found_count is None: + found_count = count + if is_like_search: + # is_like_search will decrease also one, so change this here. + found_count += 1 + else: + return [] + scopes = [imports.ImportPath(user_stmt, is_like_search, + kill_count=(count - found_count), direct_resolve=True)] else: # just parse one statement, take it and evaluate it r = parsing.PyFuzzyParser(goto_path, source_path, no_docstr=True) @@ -231,7 +251,7 @@ def get_definition(source, line, column, source_path): op = f.get_operator_under_cursor() scopes = set([keywords.get_operator(op, pos)] if op else []) else: - scopes = set(_prepare_goto(source, pos, source_path, f, goto_path)) + scopes = set(_prepare_goto(pos, source_path, f, goto_path)) for s in scopes.copy(): if isinstance(s, imports.ImportPath): @@ -265,7 +285,7 @@ def goto(source, line, column, source_path): if next(context) in ('class', 'def'): definitions = set([f.parser.user_scope]) else: - scopes = _prepare_goto(source, pos, source_path, f, goto_path) + scopes = _prepare_goto(pos, source_path, f, goto_path) definitions = evaluate.goto(scopes, search_name_new) d = [Definition(d) for d in set(definitions)] @@ -303,7 +323,7 @@ def related_names(source, line, column, source_path): elif isinstance(f.parser.user_stmt, (parsing.Param, parsing.Import)): definitions = [f.parser.user_stmt] else: - scopes = _prepare_goto(source, pos, source_path, f, goto_path) + scopes = _prepare_goto(pos, source_path, f, goto_path) definitions = evaluate.goto(scopes, search_name_new) module = set([d.get_parent_until() for d in definitions]) diff --git a/imports.py b/imports.py index 6e797580..bbf32cb3 100644 --- a/imports.py +++ b/imports.py @@ -24,23 +24,27 @@ class ImportPath(object): class GlobalNamespace(object): pass - def __init__(self, import_stmt, is_like_search=False): + def __init__(self, import_stmt, is_like_search=False, kill_count=0, + direct_resolve=False): self.import_stmt = import_stmt + self.is_like_search = is_like_search + self.direct_resolve = direct_resolve + self.is_partial_import = bool(kill_count) + self.file_path = os.path.dirname(import_stmt.get_parent_until().path) + + # rest is import_path resolution self.import_path = [] if import_stmt.from_ns: self.import_path += import_stmt.from_ns.names if import_stmt.namespace: - if self.is_nested_import(): + if self.is_nested_import() and not direct_resolve: self.import_path.append(import_stmt.namespace.names[0]) else: self.import_path += import_stmt.namespace.names - self.is_like_search = is_like_search - if is_like_search: - # drop one path part, because that is used by the like search + for i in range(kill_count + int(is_like_search)): self.import_path.pop() - self.file_path = os.path.dirname(import_stmt.get_parent_until().path) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.import_stmt) @@ -52,7 +56,8 @@ class ImportPath(object): >>> import foo.bar """ return not self.import_stmt.alias and not self.import_stmt.from_ns \ - and len(self.import_stmt.namespace.names) > 1 + and len(self.import_stmt.namespace.names) > 1 \ + and not self.direct_resolve def get_nested_import(self, parent): """ @@ -70,16 +75,30 @@ class ImportPath(object): debug.dbg('Generated a nested import: %s' % new) return new - def get_defined_names(self): + def get_defined_names(self, on_import_stmt=False): names = [] for scope in self.follow(): if scope is ImportPath.GlobalNamespace: names += self.get_module_names() names += self.get_module_names([self.file_path]) else: - for s, n in evaluate.get_names_for_scope(scope, + if on_import_stmt and isinstance(scope, parsing.Module) \ + and scope.path.endswith('__init__.py'): + pkg_path = os.path.dirname(scope.path) + names += self.get_module_names([pkg_path]) + for s, scope_names in evaluate.get_names_for_scope(scope, include_builtin=False): - names += n + for n in scope_names: + if not isinstance(n.parent(), parsing.Import) \ + and (self.import_stmt.from_ns is None \ + or self.is_partial_import): + # from_ns must be defined to access module + # values plus a partial import means that there + # is something after the import, which + # automatically implies that there must not be + # any non-module scope. + continue + names.append(n) return names def get_module_names(self, search_path=None): @@ -99,7 +118,7 @@ class ImportPath(object): Returns the imported modules. """ if self.import_path: - scope, rest = self.follow_file_system() + scope, rest = self._follow_file_system() if len(rest) > 1 or rest and self.is_like_search: scopes = [] elif rest: @@ -119,7 +138,7 @@ class ImportPath(object): debug.dbg('after import', scopes) return scopes - def follow_file_system(self): + def _follow_file_system(self): """ Find a module with a path (of the module, like usb.backend.libusb10). """ diff --git a/parsing.py b/parsing.py index 20a3d730..afea141b 100644 --- a/parsing.py +++ b/parsing.py @@ -35,6 +35,7 @@ import tokenize import re import keyword import weakref +import os import debug @@ -287,8 +288,12 @@ class Module(Scope): n += self.global_vars return n - def get_module_name(self, names): + def get_module_name(self): """ This is used for the goto function. """ + sep = (os.path.sep,) * 2 + r = re.search(r'([^%s]+?)(%s__init__)?(\.py)?$' % sep, self.path) + string = r.group(1) + names = [(string, (0, 0))] if not self._name: self._name = Name(names, self.start_pos, self.end_pos, self) return self._name @@ -557,6 +562,16 @@ class Import(Simple): else: return [self.namespace] + def get_all_import_names(self): + n = [] + if self.from_ns: + n.append(self.from_ns) + if self.namespace: + n.append(self.namespace) + if self.alias: + n.append(self.alias) + return n + class Statement(Simple): """ diff --git a/test/completion/imports.py b/test/completion/imports.py index ddca5d5b..b79cc911 100644 --- a/test/completion/imports.py +++ b/test/completion/imports.py @@ -165,6 +165,25 @@ from . import import_tree as imp_tree #? str() imp_tree.a -from . import datetime +from . import datetime as mod1 #? [] mod1. + +#? str() +imp_tree.a + +import datetime + +#? 6 datetime +from datetime.time import time + +#? [] +import datetime. +#? [] +import datetime.date + +#? 18 ['mod1', 'random', 'pkg'] +from import_tree. import pkg + +#? 18 ['pkg'] +from import_tree.p import pkg diff --git a/test/completion/thirdparty/pylab_.py b/test/completion/thirdparty/pylab_.py index 10149fee..d57ec734 100644 --- a/test/completion/thirdparty/pylab_.py +++ b/test/completion/thirdparty/pylab_.py @@ -5,7 +5,7 @@ pylab. #! ['module numpy'] import numpy -#! ['import random', 'module random'] +#! ['module random'] import numpy.random #? ['array2string']