"""Elpy backend using the Rope library.
|
|
|
|
This backend uses the Rope library:
|
|
|
|
http://rope.sourceforge.net/
|
|
|
|
"""
|
|
import os
|
|
import time
|
|
|
|
import rope.contrib.codeassist
|
|
import rope.base.project
|
|
import rope.base.libutils
|
|
import rope.base.exceptions
|
|
import rope.contrib.findit
|
|
|
|
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
|
|
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:
|
|
self.project.validate()
|
|
self.last_validation = now
|
|
|
|
def rpc_get_completions(self, filename, source, offset):
|
|
self.validate()
|
|
resource = self.get_resource(filename)
|
|
try:
|
|
proposals = rope.contrib.codeassist.code_assist(self.project,
|
|
source, offset,
|
|
resource,
|
|
maxfixes=MAXFIXES)
|
|
starting_offset = rope.contrib.codeassist.starting_offset(source,
|
|
offset)
|
|
except (rope.base.exceptions.BadIdentifierError,
|
|
rope.base.exceptions.ModuleSyntaxError,
|
|
IndentationError,
|
|
IndexError,
|
|
LookupError):
|
|
# Rope can't parse this file
|
|
return []
|
|
|
|
prefixlen = offset - starting_offset
|
|
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]
|
|
|
|
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):
|
|
self.validate()
|
|
|
|
# The find_definition call fails on an empty strings
|
|
if source == '':
|
|
return None
|
|
|
|
resource = self.get_resource(filename)
|
|
try:
|
|
location = rope.contrib.findit.find_definition(self.project,
|
|
source, offset,
|
|
resource, MAXFIXES)
|
|
except (rope.base.exceptions.BadIdentifierError,
|
|
rope.base.exceptions.ModuleSyntaxError,
|
|
IndentationError,
|
|
LookupError):
|
|
# Rope can't parse this file
|
|
return None
|
|
|
|
if location is None:
|
|
return None
|
|
else:
|
|
return (location.resource.real_path, location.offset)
|
|
|
|
def rpc_get_calltip(self, filename, source, offset):
|
|
self.validate()
|
|
offset = find_called_name_offset(source, offset)
|
|
resource = self.get_resource(filename)
|
|
if 0 < offset < len(source) and source[offset] == ')':
|
|
offset -= 1
|
|
try:
|
|
calltip = rope.contrib.codeassist.get_calltip(
|
|
self.project, source, offset, resource, MAXFIXES,
|
|
remove_self=True)
|
|
if calltip:
|
|
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
|
|
except (rope.base.exceptions.BadIdentifierError,
|
|
rope.base.exceptions.ModuleSyntaxError,
|
|
IndentationError,
|
|
IndexError,
|
|
LookupError):
|
|
# Rope can't parse this file
|
|
return None
|
|
|
|
def rpc_get_docstring(self, filename, source, offset):
|
|
self.validate()
|
|
resource = self.get_resource(filename)
|
|
try:
|
|
docstring = rope.contrib.codeassist.get_doc(self.project,
|
|
source, offset,
|
|
resource, MAXFIXES)
|
|
except (rope.base.exceptions.BadIdentifierError,
|
|
rope.base.exceptions.ModuleSyntaxError,
|
|
IndentationError,
|
|
IndexError,
|
|
LookupError):
|
|
# Rope can't parse this file
|
|
docstring = None
|
|
return docstring
|
|
|
|
|
|
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
|