From 85c490cd6401b9b86e6b1355665e6c0a60e10e4c Mon Sep 17 00:00:00 2001 From: Julian Eberius Date: Tue, 5 Feb 2013 20:55:21 +0100 Subject: [PATCH] - added a lot more caching to Rope - added case-insensitive completion --- lib/python2/rope/base/project.py | 3 +- lib/python2/rope/base/pycore.py | 2 ++ lib/python2/rope/base/pynames.py | 11 ++++-- lib/python2/rope/base/utils.py | 48 ++++++++++++++++++++++++-- lib/python2/rope/contrib/codeassist.py | 30 ++++++++++------ 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/lib/python2/rope/base/project.py b/lib/python2/rope/base/project.py index 97d2dd3..c6f2b59 100644 --- a/lib/python2/rope/base/project.py +++ b/lib/python2/rope/base/project.py @@ -18,6 +18,7 @@ def __init__(self, fscommands): self.prefs = prefs.Prefs() self.data_files = _DataFiles(self) + @utils.memoize def get_resource(self, resource_name): """Get a resource in a project. @@ -360,7 +361,7 @@ def _get_file(self, name, compress): path += '.gz' return self.project.get_file(path) - +@utils.cached(1000) def _realpath(path): """Return the real path of `path` diff --git a/lib/python2/rope/base/pycore.py b/lib/python2/rope/base/pycore.py index 32056a0..3aff2ff 100644 --- a/lib/python2/rope/base/pycore.py +++ b/lib/python2/rope/base/pycore.py @@ -183,6 +183,7 @@ def _find_module(self, modname, folder=None): # packages, that is most of the time # - We need a separate resource observer; `self.observer` # does not get notified about module and folder creations + @utils.memoize def get_source_folders(self): """Returns project source folders""" if self.project.root is None: @@ -206,6 +207,7 @@ def _is_package(self, folder): else: return False + @utils.memoize def _find_source_folders(self, folder): for resource in folder.get_folders(): if self._is_package(resource): diff --git a/lib/python2/rope/base/pynames.py b/lib/python2/rope/base/pynames.py index 79bba15..82ee2cc 100644 --- a/lib/python2/rope/base/pynames.py +++ b/lib/python2/rope/base/pynames.py @@ -101,6 +101,7 @@ def __init__(self, importing_module, module_name=None, self.level = level self.resource = resource self.pymodule = _get_concluded_data(self.importing_module) + self.cached_pyobject = None def _current_folder(self): resource = self.importing_module.get_module().get_resource() @@ -127,9 +128,13 @@ def _get_pymodule(self): return self.pymodule.get() def get_object(self): - if self._get_pymodule() is None: - return rope.base.pyobjects.get_unknown() - return self._get_pymodule() + if not self.cached_pyobject: + pymod = self._get_pymodule() + if pymod is None: + self.cached_pyobject = rope.base.pyobjects.get_unknown() + else: + self.cached_pyobject = pymod + return self.cached_pyobject def get_definition_location(self): pymodule = self._get_pymodule() diff --git a/lib/python2/rope/base/utils.py b/lib/python2/rope/base/utils.py index e35ecbf..4e433b8 100644 --- a/lib/python2/rope/base/utils.py +++ b/lib/python2/rope/base/utils.py @@ -1,5 +1,5 @@ import warnings - +from functools import partial def saveit(func): """A decorator that caches the return value of a function""" @@ -52,7 +52,6 @@ def newfunc(*args, **kwds): return newfunc return _decorator - def cached(count): """A caching decorator based on parameter objects""" def decorator(func): @@ -76,3 +75,48 @@ def __call__(self, *args, **kwds): if len(self.cache) > self.count: del self.cache[0] return result + +class memoize(object): + """cache the return value of a method + + This class is meant to be used as a decorator of methods. The return value + from a given method invocation will be cached on the instance whose method + was invoked. All arguments passed to a method decorated with memoize must + be hashable. + + If a memoized method is invoked directly on its class the result will not + be cached. Instead the method will be invoked like a static method: + class Obj(object): + @memoize + def add_to(self, arg): + return self + arg + Obj.add_to(1) # not enough arguments + Obj.add_to(1, 2) # returns 3, result is not cached + """ + def __init__(self, func): + self.func = func + def __get__(self, obj, objtype=None): + if obj is None: + return self.func + return partial(self, obj) + def __call__(self, *args, **kw): + obj = args[0] + try: + cache = obj.__cache + except AttributeError: + cache = obj.__cache = {} + key = (self.func, args[1:], frozenset(kw.items())) + try: + res = cache[key] + except KeyError: + res = cache[key] = self.func(*args, **kw) + return res + +def lazyprop(fn): + attr_name = '_lazy_' + fn.__name__ + @property + def _lazyprop(self): + if not hasattr(self, attr_name): + setattr(self, attr_name, fn(self)) + return getattr(self, attr_name) + return _lazyprop diff --git a/lib/python2/rope/contrib/codeassist.py b/lib/python2/rope/contrib/codeassist.py index 37433c2..4b02c89 100644 --- a/lib/python2/rope/contrib/codeassist.py +++ b/lib/python2/rope/contrib/codeassist.py @@ -4,14 +4,14 @@ import rope.base.codeanalyze import rope.base.evaluate -from rope.base import pyobjects, pyobjectsdef, pynames, builtins, exceptions, worder +from rope.base import pyobjects, pyobjectsdef, pynames, builtins, exceptions, worder, utils from rope.base.codeanalyze import SourceLinesAdapter from rope.contrib import fixsyntax from rope.refactor import functionutils def code_assist(project, source_code, offset, resource=None, - templates=None, maxfixes=1, later_locals=True): + templates=None, maxfixes=1, later_locals=True, case_sensitive=False): """Return python code completions as a list of `CodeAssistProposal`\s `resource` is a `rope.base.resources.Resource` object. If @@ -29,7 +29,7 @@ def code_assist(project, source_code, offset, resource=None, DeprecationWarning, stacklevel=2) assist = _PythonCodeAssist( project, source_code, offset, resource=resource, - maxfixes=maxfixes, later_locals=later_locals) + maxfixes=maxfixes, later_locals=later_locals, case_sensitive=case_sensitive) return assist() @@ -176,7 +176,7 @@ def parameters(self): if isinstance(pyobject, pyobjects.AbstractFunction): return pyobject.get_param_names() - @property + @utils.lazyprop def type(self): pyname = self.pyname if isinstance(pyname, builtins.BuiltinName): @@ -289,16 +289,26 @@ def default_templates(): return {} +def _startswith(s1, s2): + return s1.startswith(s2) + + +def _case_insensitive_startswith(s1, s2): + return s1.lower().startswith(s2.lower()) + + class _PythonCodeAssist(object): def __init__(self, project, source_code, offset, resource=None, - maxfixes=1, later_locals=True): + maxfixes=1, later_locals=True, case_sensitive=False): self.project = project self.pycore = self.project.pycore self.code = source_code self.resource = resource self.maxfixes = maxfixes self.later_locals = later_locals + self.case_sensitive = case_sensitive + self.startswith = _startswith if case_sensitive else _case_insensitive_startswith self.word_finder = worder.Worder(source_code, True) self.expression, self.starting, self.offset = \ self.word_finder.get_splitted_primary_before(offset) @@ -315,7 +325,7 @@ def _find_starting_offset(self, source_code, offset): def _matching_keywords(self, starting): result = [] for kw in self.keywords: - if kw.startswith(starting): + if self.startswith(kw, starting): result.append(CompletionProposal(kw, 'keyword')) return result @@ -338,7 +348,7 @@ def _dotted_completions(self, module_scope, holding_scope): pyobjectsdef.PyPackage)): compl_scope = 'imported' for name, pyname in element.get_attributes().items(): - if name.startswith(self.starting): + if self.startswith(name, self.starting): result[name] = CompletionProposal(name, compl_scope, pyname) return result @@ -350,7 +360,7 @@ def _undotted_completions(self, scope, result, lineno=None): else: names = scope.get_names() for name, pyname in names.items(): - if name.startswith(self.starting): + if self.startswith(name, self.starting): compl_scope = 'local' if scope.get_kind() == 'Module': compl_scope = 'global' @@ -366,7 +376,7 @@ def _from_import_completions(self, pymodule): pymodule = self._find_module(pymodule, module_name) result = {} for name in pymodule: - if name.startswith(self.starting): + if self.startswith(name, self.starting): result[name] = CompletionProposal(name, scope='global', pyname=pymodule[name]) return result @@ -440,7 +450,7 @@ def _keyword_parameters(self, pymodule, scope): pyobject.get_param_names(special_args=False)) result = {} for name in param_names: - if name.startswith(self.starting): + if self.startswith(name, self.starting): result[name + '='] = NamedParamProposal( name, pyobject )