diff --git a/evaluate.py b/evaluate.py index b5f6cb03..4df3c51e 100644 --- a/evaluate.py +++ b/evaluate.py @@ -12,6 +12,14 @@ import debug import builtin +memoize_caches = [] + + +def clear_caches(): + for m in memoize_caches: + m.clear() + + def memoize(default=None): """ This is a typical memoization decorator, BUT there is one difference: @@ -23,6 +31,7 @@ def memoize(default=None): """ def func(function): memo = {} + memoize_caches.append(memo) def wrapper(*args): if args in memo: @@ -201,12 +210,13 @@ def strip_imports(scopes): if isinstance(s, parsing.Import): print 'dini mueter, steile griech!' try: - new = follow_import(s) + new_scopes = follow_import(s) except modules.ModuleNotFound: debug.dbg('Module not found: ' + str(s)) else: - result.append(new) - result += strip_imports(i for i in new.get_imports() if i.star) + result += new_scopes + for n in new_scopes: + result += strip_imports(i for i in n.get_imports() if i.star) else: result.append(s) return result @@ -319,9 +329,13 @@ def follow_import(_import): if _import.namespace: ns_list += _import.namespace.names - scope, rest = modules.find_module(ns_list) - if rest: - scope = follow_path(rest.__iter__(), scope) + loaded_in = _import.get_parent_until() - debug.dbg('after import', scope, rest) - return scope + scope, rest = modules.find_module(loaded_in, ns_list) + if rest: + scopes = follow_path(rest.__iter__(), scope) + else: + scopes = [scope] + + debug.dbg('after import', scopes, rest) + return scopes diff --git a/ftest.py b/ftest.py index c4100098..2a810715 100755 --- a/ftest.py +++ b/ftest.py @@ -3,26 +3,16 @@ import functions #functions.debug.debug_function = functions.debug.print_to_stdout -functions.debug.ignored_modules += ['parsing', 'builtin'] +#functions.debug.ignored_modules += ['parsing', 'builtin'] functions.modules.module_find_path.insert(0, '.') -with open('test.py') as f: +f_name = 'test.py' +import os +path = os.getcwd() + '/' + f_name + +with open(path) as f: code = f.read() for i in range(1): - completions = functions.complete(code, 50, 200) + completions = functions.complete(code, 150, 200, path) print '\n', ', '.join(str(c) for c in completions) - -out = [] -for c in completions: - d = dict(word=str(c), - abbr=c.complete, - menu=c.description, # the stuff directly behind the completion - info=c.help, # docstr and similar stuff - kind=c.type, # completion type - icase=1, # case insensitive - dup=1, # allow duplicates (maybe later remove this) - ) - out.append(d) - -print str(out) diff --git a/functions.py b/functions.py index 8733ae23..1be4af48 100644 --- a/functions.py +++ b/functions.py @@ -12,23 +12,23 @@ __all__ = ['complete', 'get_completion_parts', 'complete_test', 'set_debug_funct class FileWithCursor(modules.File): """ Manages all files, that are parsed and caches them. - Important are the params source and module_name, one of them has to + Important are the params source and module_path, one of them has to be there. :param source: The source code of the file. - :param module_name: The module name of the file. + :param module_path: The module name of the file. :param row: The row, the user is currently in. Only important for the \ main file. """ - def __init__(self, module_name, source, row): - super(FileWithCursor, self).__init__(module_name, source) + def __init__(self, module_path, source, row): + super(FileWithCursor, self).__init__(module_path, source) self.row = row # this two are only used, because there is no nonlocal in Python 2 self._row_temp = None self._relevant_temp = None - self._parser = parsing.PyFuzzyParser(source, row) + self._parser = parsing.PyFuzzyParser(source, module_path, row) def get_row_path(self, column): """ Get the path under the cursor. """ @@ -122,9 +122,29 @@ class Completion(object): except: return '' - @property - def type(self): - return '' # type(self.name) + def get_type(self): + return type(self.name.parent) + + def get_vim_type(self): + """ + This is the only function, which is vim specific, it returns the vim + type, see help(complete-items) + """ + typ = self.get_type() + if typ == parsing.Statement: + return 'v' # variable + elif typ == parsing.Function: + return 'f' # function / method + elif typ in [parsing.Class, evaluate.Instance]: + return 't' # typedef -> abused as class + elif typ == parsing.Import: + return 'd' # define -> abused as import + if typ == parsing.Param: + return 'm' # member -> abused as param + else: + debug.dbg('other python type: ', typ) + + return '' def __str__(self): return self.name.names[-1] @@ -138,7 +158,7 @@ def get_completion_parts(path): match = re.match(r'^(.*?)(\.|)(\w?[\w\d]*)$', path, flags=re.S) return match.groups() -def complete(source, row, column, file_callback=None): +def complete(source, row, column, source_path): """ An auto completer for python files. @@ -148,17 +168,20 @@ def complete(source, row, column, file_callback=None): :type row: int :param col: The column to complete in. :type col: int + :param source_path: The path in the os, the current module is in. + :type source_path: int + :return: list of completion objects :rtype: list """ - f = FileWithCursor('__main__', source=source, row=row) + f = FileWithCursor(source_path, source=source, row=row) scope = f.parser.user_scope path = f.get_row_path(column) debug.dbg('completion_start: %s in %s' % (path, scope)) # just parse one statement, take it and evaluate it path, dot, like = get_completion_parts(path) - r = parsing.PyFuzzyParser(path) + r = parsing.PyFuzzyParser(path, source_path) try: stmt = r.top.statements[0] except IndexError: @@ -171,13 +194,11 @@ def complete(source, row, column, file_callback=None): for s in scopes: completions += s.get_defined_names() - print repr(path), repr(dot), repr(like), row, column - print len(completions) needs_dot = not dot and path completions = [Completion(c, needs_dot, len(like)) for c in completions if c.names[-1].lower().startswith(like.lower())] - print 'nr2', len(completions) + _clear_caches() return completions @@ -245,3 +266,6 @@ def set_debug_function(func_cb): :param func_cb: The callback function for debug messages, with n params. """ debug.debug_function = func_cb + +def _clear_caches(): + evaluate.clear_caches() diff --git a/modules.py b/modules.py index c1030b5f..80ff7c5c 100644 --- a/modules.py +++ b/modules.py @@ -1,5 +1,6 @@ import imp import sys +import os import debug import parsing @@ -20,11 +21,11 @@ class File(object): Manages all files, that are parsed and caches them. :param source: The source code of the file. - :param module_name: The module name of the file. + :param module_path: The module path of the file. """ - def __init__(self, module_name, source): + def __init__(self, module_path, source): self.source = source - self.module_name = module_name + self.module_path = module_path self._line_cache = None self._parser = None @@ -32,30 +33,34 @@ class File(object): def parser(self): if self._parser: return self._parser - if not self.module_name and not self.source: + if not self.module_path and not self.source: raise AttributeError("Submit a module name or the source code") - elif self.module_name: + elif self.module_path: return self._load_module() def _load_module(self): - self._parser = parsing.PyFuzzyParser(self.source) + self._parser = parsing.PyFuzzyParser(self.source, self.module_path) del self.source # efficiency return self._parser -def find_module(point_path): +def find_module(current_module, point_path): """ Find a module with a path (of the module, like usb.backend.libusb10). + Relative imports: http://www.python.org/dev/peps/pep-0328 + are only used like this (py3000): from .module import name. + + :param current_ns_path: A path to the current namespace. :param point_path: A name from the parser. :return: The rest of the path, and the module top scope. """ def follow_str(ns, string): debug.dbg('follow_module', ns, string) if ns: - path = ns[1] + path = [ns[1]] else: # TODO modules can be system modules, without '.' in path - path = module_find_path + path = None debug.dbg('search_module', string, path) try: i = imp.find_module(string, path) @@ -67,8 +72,9 @@ def find_module(point_path): raise return i - # now execute those paths current_namespace = None + sys.path.insert(0, os.path.dirname(current_module.module_path)) + # now execute those paths rest = [] for i, s in enumerate(point_path): try: @@ -80,8 +86,19 @@ def find_module(point_path): raise ModuleNotFound( 'The module you searched has not been found') - if current_namespace[0]: - f = File(current_namespace[2], current_namespace[0].read()) + sys.path.pop(0) + path = current_namespace[1] + is_package_directory = current_namespace[2][2] == imp.PKG_DIRECTORY + + if is_package_directory or current_namespace[0]: + # is a directory module + if is_package_directory: + path += '/__init__.py' + with open(path) as f: + source = f.read() + else: + source = current_namespace[0].read() + f = File(path, source) else: - f = builtin.Parser(current_namespace[1]) + f = builtin.Parser(path) return f.parser.top, rest diff --git a/parsing.py b/parsing.py index 3b36cdd9..e9037a46 100644 --- a/parsing.py +++ b/parsing.py @@ -99,7 +99,6 @@ class Scope(Simple): self.subscopes = [] self.imports = [] self.statements = [] - self.global_vars = [] self.docstr = docstr def add_scope(self, sub, decorators): @@ -154,18 +153,6 @@ class Scope(Simple): i += s.get_imports() return i - def add_global(self, name): - """ - Global means in these context a function (subscope) which has a global - statement. - This is only relevant for the top scope. - - :param name: The name of the global. - :type name: Name - """ - self.global_vars.append(name) - # set no parent here, because globals are not defined in this scope. - def get_code(self, first_indent=False, indention=" "): """ :return: Returns the code of the current scope. @@ -203,11 +190,10 @@ class Scope(Simple): # function and class names n += [s.name for s in self.subscopes] - n += self.global_vars for i in self.imports: if not i.star: - n += i.get_names() + n += i.get_defined_names() return n @@ -229,12 +215,41 @@ class Scope(Simple): try: name = self.command except: - name = 'global' + name = self.module_path return "<%s: %s@%s-%s>" % \ (self.__class__.__name__, name, self.line_nr, self.line_end) +class GlobalScope(Scope): + """ + The top scope, which is a module. + I don't know why I didn't name it Module :-) + """ + def __init__(self, module_path, docstr=''): + super(GlobalScope, self).__init__(module_path, docstr) + self.module_path = module_path + self.global_vars = [] + + def add_global(self, name): + """ + Global means in these context a function (subscope) which has a global + statement. + This is only relevant for the top scope. + + :param name: The name of the global. + :type name: Name + """ + self.global_vars.append(name) + # set no parent here, because globals are not defined in this scope. + + def get_set_vars(self): + n = [] + n += super(GlobalScope, self).get_set_vars() + n += self.global_vars + return n + + class Class(Scope): """ Used to store the parsed contents of a python class. @@ -420,7 +435,7 @@ class Import(Simple): :type star: bool """ def __init__(self, indent, line_nr, line_end, namespace, alias='', \ - from_ns='', star=False): + from_ns='', star=False, relative_count=None): super(Import, self).__init__(indent, line_nr, line_end) self.namespace = namespace @@ -436,6 +451,7 @@ class Import(Simple): from_ns.parent = self self.star = star + self.relative_count = relative_count def get_code(self): if self.alias: @@ -449,7 +465,7 @@ class Import(Simple): else: return "import " + ns_str + '\n' - def get_names(self): + def get_defined_names(self): if self.star: return [self] return [self.alias] if self.alias else [self.namespace] @@ -805,12 +821,12 @@ class PyFuzzyParser(object): :param user_line: The line, the user is currently on. :type user_line: int """ - def __init__(self, code, user_line=None): + def __init__(self, code, module_path=None, user_line=None): self.user_line = user_line self.code = code + '\n' # end with \n, because the parser needs it # initialize global Scope - self.top = Scope(0, 0) + self.top = GlobalScope(module_path) self.scope = self.top self.current = (None, None, None) @@ -1072,8 +1088,9 @@ class PyFuzzyParser(object): continue elif '=' in tok and not tok in ['>=', '<=', '==', '!=']: # there has been an assignement -> change vars - set_vars = used_vars - used_vars = [] + if level == 0: + set_vars = used_vars + used_vars = [] elif tok in ['{', '(', '[']: level += 1 elif tok in ['}', ')', ']']: @@ -1139,8 +1156,8 @@ class PyFuzzyParser(object): while True: try: token_type, tok, indent = self.next() - debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\ - % (tok, token_type, indent)) + #debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\ + # % (tok, token_type, indent)) while token_type == tokenize.DEDENT and self.scope != self.top: debug.dbg('dedent', self.scope) @@ -1187,10 +1204,19 @@ class PyFuzzyParser(object): for m, alias in imports: i = Import(indent, start_line, self.line_nr, m, alias) self.scope.add_import(i) + debug.dbg("new import: %s" % (i)) self.freshscope = False elif tok == 'from': + # take care for relative imports + relative_count = 0 + while 1: + token_type, tok, indent = self.next() + if tok != '.': + break; + relative_count += 1 + # the from import mod, token_type, tok, start_indent, start_line2 = \ - self._parsedotname() + self._parsedotname(self.current) if not mod or tok != "import": debug.warning("from: syntax error@%s" % self.line_nr) @@ -1202,8 +1228,9 @@ class PyFuzzyParser(object): if star: name = None i = Import(indent, start_line, self.line_nr, name, - alias, mod, star) + alias, mod, star, relative_count) self.scope.add_import(i) + debug.dbg("new from: %s" % (i)) self.freshscope = False #loops elif tok == 'for': diff --git a/plugin/jedi.vim b/plugin/jedi.vim index 01db24e6..54324f55 100644 --- a/plugin/jedi.vim +++ b/plugin/jedi.vim @@ -10,6 +10,9 @@ if !has('python') finish endif +" ------------------------------------------------------------------------ +" Completion +" ------------------------------------------------------------------------ function! jedi#Complete(findstart, base) if a:findstart == 1 @@ -17,17 +20,19 @@ function! jedi#Complete(findstart, base) else python << PYTHONEOF if 1: + # TODO change the finstart column, to switch cases, if they are not right. row, column = vim.current.window.cursor + buf_path = vim.current.buffer.name source = '\n'.join(vim.current.buffer) try: - completions = functions.complete(source, row, column) + completions = functions.complete(source, row, column, buf_path) out = [] for c in completions: d = dict(word=c.complete, abbr=str(c), menu=c.description, # stuff directly behind the completion info=c.help, # docstr and similar stuff - kind=c.type, # completion type + kind=c.get_vim_type(), # completion type icase=1, # case insensitive dup=1, # allow duplicates (maybe later remove this) ) @@ -35,10 +40,11 @@ if 1: strout = str(out) except Exception as e: - print 'error:', e + # print to stdout, will be in :messages + print(traceback.format_exc()) strout = '' - print 'end', strout + #print 'end', strout vim.command('return ' + strout) PYTHONEOF endif @@ -48,7 +54,7 @@ endfunction " ------------------------------------------------------------------------ " Initialization of Jedi " ------------------------------------------------------------------------ -" + let s:current_file=expand("") python << PYTHONEOF @@ -60,6 +66,8 @@ import sys from os.path import dirname sys.path.insert(0, dirname(dirname(vim.eval('s:current_file')))) +import traceback # for exception output + import functions PYTHONEOF diff --git a/test.py b/test.py index 516f0b32..a4eee565 100644 --- a/test.py +++ b/test.py @@ -147,4 +147,4 @@ b= asdf2 #import parsing as test c = b().c3() -1.0.fromhex() +1.0.fromhex(); from flask import Flask; Flask.