"""Elpy backend using the Rope library. This backend uses the Rope library: http://rope.sourceforge.net/ """ import os import time import traceback import rope.contrib.codeassist import rope.base.project import rope.base.libutils import rope.base.exceptions import rope.contrib.findit from rope.base.exceptions import RopeError from elpy import rpc import elpy.pydocutils VALIDATE_EVERY_SECONDS = 5 MAXFIXES = 5 class RopeBackend(object): """The Rope backend class. Implements the RPC calls we can pass on to Rope. Also subclasses the native backend to provide methods Rope does not provide, if any. """ name = "rope" def __init__(self, project_root): super(RopeBackend, self).__init__() self.last_validation = 0 if not os.path.exists(project_root): raise rpc.Fault( "rope does not support files without a local project root", code=400 ) self.project_root = project_root self.completions = {} prefs = dict(ignored_resources=['*.pyc', '*~', '.ropeproject', '.hg', '.svn', '_svn', '.git'], python_files=['*.py'], save_objectdb=False, compress_objectdb=False, automatic_soa=True, soa_followed_calls=0, perform_doa=True, validate_objectdb=True, max_history_items=32, save_history=False, compress_history=False, indent_size=4, extension_modules=[], import_dynload_stdmods=True, ignore_syntax_errors=False, ignore_bad_imports=False) self.project = rope.base.project.Project(self.project_root, ropefolder=None, **prefs) def get_resource(self, filename): if filename is not None and os.path.exists(filename): return rope.base.libutils.path_to_resource(self.project, filename, 'file') else: return None def validate(self): """Validate the stored project. This should be called before every use of Rope. It will revalidate the project, but do some call throttling. """ now = time.time() if now > self.last_validation + VALIDATE_EVERY_SECONDS: try: self.project.validate() except rope.base.exceptions.ResourceNotFoundError: pass self.last_validation = now def call_rope(self, rope_function, filename, source, offset, **kwargs): self.validate() resource = self.get_resource(filename) try: return rope_function(self.project, source, offset, resource, maxfixes=MAXFIXES, **kwargs) except (RopeError, IndentationError, LookupError, AttributeError): return None except Exception as e: data = { "traceback": traceback.format_exc(), "rope_debug_info": { "project_root": self.project_root, "filename": filename, "source": source, "function_name": (rope_function.__module__ + "." + rope_function.__name__), "function_args": ", ".join([ "project", "source", str(offset), "resource", "maxfixes={0}".format(MAXFIXES) ] + [ u"{}={}".format(k, v) for (k, v) in kwargs.items() ]) } } raise rpc.Fault( code=500, message=str(e), data=data ) def rpc_get_completions(self, filename, source, offset): proposals = self.call_rope( rope.contrib.codeassist.code_assist, filename, source, offset ) if proposals is None: return [] try: starting_offset = rope.contrib.codeassist.starting_offset(source, offset) except (rope.base.exceptions.BadIdentifierError, rope.base.exceptions.ModuleSyntaxError, IndentationError, LookupError, AttributeError): return [] prefixlen = offset - starting_offset try: self.completions = dict((proposal.name, proposal) for proposal in proposals) return [{'name': proposal.name, 'suffix': proposal.name[prefixlen:], 'annotation': proposal.type, 'meta': str(proposal)} for proposal in proposals] except rope.base.exceptions.ModuleSyntaxError: # Bug#406 return [] except AttributeError: # Bug#699 return [] def rpc_get_completion_docstring(self, completion): proposal = self.completions.get(completion) if proposal is None: return None else: return proposal.get_doc() def rpc_get_completion_location(self, completion): proposal = self.completions.get(completion) if proposal is None: return None else: if not proposal.pyname: return None module, lineno = proposal.pyname.get_definition_location() if module is None: return None resource = module.get_module().get_resource() return (resource.real_path, lineno) def rpc_get_definition(self, filename, source, offset): location = self.call_rope( rope.contrib.findit.find_definition, filename, source, offset ) if location is None: return None else: return (location.resource.real_path, location.offset) def rpc_get_calltip(self, filename, source, offset): offset = find_called_name_offset(source, offset) if 0 < offset < len(source) and source[offset] == ')': offset -= 1 calltip = self.call_rope( rope.contrib.codeassist.get_calltip, filename, source, offset, remove_self=True ) if calltip is None: return None calltip = calltip.replace(".__init__(", "(") calltip = calltip.replace("(self)", "()") calltip = calltip.replace("(self, ", "(") # "elpy.tests.support.source_and_offset(source)" # => # "support.source_and_offset(source)" try: openpos = calltip.index("(") period2 = calltip.rindex(".", 0, openpos) period1 = calltip.rindex(".", 0, period2) calltip = calltip[period1 + 1:] except ValueError: pass return calltip def rpc_get_docstring(self, filename, source, offset): return self.call_rope( rope.contrib.codeassist.get_doc, filename, source, offset ) def find_called_name_offset(source, orig_offset): """Return the offset of a calling function. This only approximates movement. """ offset = min(orig_offset, len(source) - 1) paren_count = 0 while True: if offset <= 1: return orig_offset elif source[offset] == '(': if paren_count == 0: return offset - 1 else: paren_count -= 1 elif source[offset] == ')': paren_count += 1 offset -= 1 ################################################################## # A recurring problem in Rope for Elpy is that it searches the whole # project root for Python files. If the user edits a file in their # home directory, this can easily read a whole lot of files, making # Rope practically useless. We change the file finding algorithm here # to only recurse into directories with an __init__.py file in them. def find_source_folders(self, folder): for resource in folder.get_folders(): if self._is_package(resource): return [folder] result = [] for resource in folder.get_files(): if resource.name.endswith('.py'): result.append(folder) break for resource in folder.get_folders(): if self._is_package(resource): result.append(resource) return result import rope.base.pycore rope.base.pycore.PyCore._find_source_folders = find_source_folders def get_files(self): if self.files is None: self.files = get_python_project_files(self.project) return self.files rope.base.project._FileListCacher.get_files = get_files def get_python_project_files(project): for dirname, subdirs, files in os.walk(project.root.real_path): for filename in files: yield rope.base.libutils.path_to_resource( project, os.path.join(dirname, filename), 'file') subdirs[:] = [subdir for subdir in subdirs if os.path.exists(os.path.join(dirname, subdir, "__init__.py"))] ################################################################## # Monkey patching a method in rope because it doesn't complete import # statements. orig_code_completions = (rope.contrib.codeassist. _PythonCodeAssist._code_completions) def code_completions(self): proposals = get_import_completions(self) if proposals: return proposals else: return orig_code_completions(self) def get_import_completions(self): if not self.word_finder.is_import_statement(self.offset): return [] modulename = self.word_finder.get_primary_at(self.offset) # Rope can handle modules in packages if "." in modulename: return [] return dict((name, FakeProposal(name)) for name in elpy.pydocutils.get_modules() if name.startswith(modulename)) class FakeProposal(object): def __init__(self, name): self.name = name self.type = "mock" def get_doc(self): return None rope.contrib.codeassist._PythonCodeAssist._code_completions = code_completions