|
|
"""Elpy backend using the Jedi library.
This backend uses the Jedi library:
https://github.com/davidhalter/jedi
"""
import sysimport tracebackimport re
import jedi
from elpy import rpcfrom elpy.rpc import Fault
# in case pkg_resources is not properly installed# (see https://github.com/jorgenschaefer/elpy/issues/1674).try: from pkg_resources import parse_versionexcept ImportError: # pragma: no cover def parse_version(*arg, **kwargs): raise Fault("`pkg_resources` could not be imported, " "please reinstall Elpy RPC virtualenv with" " `M-x elpy-rpc-reinstall-virtualenv`", code=400)JEDISUP17 = parse_version(jedi.__version__) >= parse_version("0.17.0")
class JediBackend(object): """The Jedi backend class.
Implements the RPC calls we can pass on to Jedi.
Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html
"""
name = "jedi"
def __init__(self, project_root, environment_binaries_path): self.project_root = project_root self.environment = None if environment_binaries_path is not None: self.environment = jedi.create_environment(environment_binaries_path, safe=False) self.completions = {} sys.path.append(project_root) # Backward compatibility to jedi<17 if not JEDISUP17: # pragma: no cover self.rpc_get_completions = self.rpc_get_completions_jedi16 self.rpc_get_docstring = self.rpc_get_docstring_jedi16 self.rpc_get_definition = self.rpc_get_definition_jedi16 self.rpc_get_assignment = self.rpc_get_assignment_jedi16 self.rpc_get_calltip = self.rpc_get_calltip_jedi16 self.rpc_get_oneline_docstring = self.rpc_get_oneline_docstring_jedi16 self.rpc_get_usages = self.rpc_get_usages_jedi16 self.rpc_get_names = self.rpc_get_names_jedi16
def rpc_get_completions(self, filename, source, offset): line, column = pos_to_linecol(source, offset) proposals = run_with_debug(jedi, 'complete', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) self.completions = dict((proposal.name, proposal) for proposal in proposals) return [{'name': proposal.name.rstrip("="), 'suffix': proposal.complete.rstrip("="), 'annotation': proposal.type, 'meta': proposal.description} for proposal in proposals]
def rpc_get_completions_jedi16(self, filename, source, offset): # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) proposals = run_with_debug(jedi, 'completions', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) if proposals is None: return [] self.completions = dict((proposal.name, proposal) for proposal in proposals) return [{'name': proposal.name.rstrip("="), 'suffix': proposal.complete.rstrip("="), 'annotation': proposal.type, 'meta': proposal.description} 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.docstring(fast=False)
def rpc_get_completion_location(self, completion): proposal = self.completions.get(completion) if proposal is None: return None else: return (proposal.module_path, proposal.line)
def rpc_get_docstring(self, filename, source, offset): line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column, 'follow_imports': True, 'follow_builtin_imports': True}) if not locations: return None # Filter uninteresting things if locations[-1].name in ["str", "int", "float", "bool", "tuple", "list", "dict"]: return None if locations[-1].docstring(): return ('Documentation for {0}:\n\n'.format( locations[-1].full_name) + locations[-1].docstring()) else: return None
def rpc_get_docstring_jedi16(self, filename, source, offset): # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto_definitions', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) if not locations: return None # Filter uninteresting things if locations[-1].name in ["str", "int", "float", "bool", "tuple", "list", "dict"]: return None if locations[-1].docstring(): return ('Documentation for {0}:\n\n'.format( locations[-1].full_name) + locations[-1].docstring()) else: return None
def rpc_get_definition(self, filename, source, offset): line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column, 'follow_imports': True, 'follow_builtin_imports': True}) if not locations: return None # goto_definitions() can return silly stuff like __builtin__ # for int variables, so we remove them. See issue #76. locations = [ loc for loc in locations if (loc.module_path is not None and loc.module_name != 'builtins' and loc.module_name != '__builtin__')] if len(locations) == 0: return None loc = locations[-1] try: if loc.module_path == filename: offset = linecol_to_pos(source, loc.line, loc.column) else: with open(loc.module_path) as f: offset = linecol_to_pos(f.read(), loc.line, loc.column) except IOError: # pragma: no cover return None return (loc.module_path, offset)
def rpc_get_definition_jedi16(self, filename, source, offset): # pragma: no cover # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto_definitions', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) # goto_definitions() can return silly stuff like __builtin__ # for int variables, so we fall back on goto() in those # cases. See issue #76. if ( locations and (locations[0].module_path is None or locations[0].module_name == 'builtins' or locations[0].module_name == '__builtin__') ): locations = run_with_debug(jedi, 'goto_assignments', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) if not locations: return None else: loc = locations[-1] try: if loc.module_path: if loc.module_path == filename: offset = linecol_to_pos(source, loc.line, loc.column) else: with open(loc.module_path) as f: offset = linecol_to_pos(f.read(), loc.line, loc.column) else: return None except IOError: return None return (loc.module_path, offset)
def rpc_get_assignment(self, filename, source, offset): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.")
def rpc_get_assignment_jedi16(self, filename, source, offset): # pragma: no cover # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto_assignments', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment)
if not locations: return None else: loc = locations[-1] try: if loc.module_path: if loc.module_path == filename: offset = linecol_to_pos(source, loc.line, loc.column) else: with open(loc.module_path) as f: offset = linecol_to_pos(f.read(), loc.line, loc.column) else: return None except IOError: return None return (loc.module_path, offset)
def rpc_get_calltip(self, filename, source, offset): line, column = pos_to_linecol(source, offset) calls = run_with_debug(jedi, 'get_signatures', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) if not calls: return None params = [re.sub("^param ", '', param.description) for param in calls[0].params] return {"name": calls[0].name, "index": calls[0].index, "params": params}
def rpc_get_calltip_jedi16(self, filename, source, offset): # pragma: no cover # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) calls = run_with_debug(jedi, 'call_signatures', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) if calls: call = calls[0] else: call = None if not call: return None # Strip 'param' added by jedi at the beginning of # parameter names. Should be unecessary for jedi > 0.13.0 params = [re.sub("^param ", '', param.description) for param in call.params] return {"name": call.name, "index": call.index, "params": params}
def rpc_get_calltip_or_oneline_docstring(self, filename, source, offset): """
Return the current function calltip or its oneline docstring.
Meant to be used with eldoc. """
# Try to get a oneline docstring then docs = self.rpc_get_oneline_docstring(filename=filename, source=source, offset=offset) if docs is not None: if docs['doc'] != "No documentation": docs['kind'] = 'oneline_doc' return docs # Try to get a calltip calltip = self.rpc_get_calltip(filename=filename, source=source, offset=offset) if calltip is not None: calltip['kind'] = 'calltip' return calltip # Ok, no calltip, just display the function name if docs is not None: docs['kind'] = 'oneline_doc' return docs # Giving up... return None
def rpc_get_oneline_docstring(self, filename, source, offset): """Return a oneline docstring for the symbol at offset""" line, column = pos_to_linecol(source, offset) definitions = run_with_debug(jedi, 'goto', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) if not definitions: return None # avoid unintersting stuff definitions = [defi for defi in definitions if defi.name not in ["str", "int", "float", "bool", "tuple", "list", "dict"]] if len(definitions) == 0: return None definition = definitions[0] # Get name if definition.type in ['function', 'class']: raw_name = definition.name name = '{}()'.format(raw_name) doc = definition.docstring().split('\n') elif definition.type in ['module']: raw_name = definition.name name = '{} {}'.format(raw_name, definition.type) doc = definition.docstring().split('\n') elif (definition.type in ['instance'] and hasattr(definition, "name")): raw_name = definition.name name = raw_name doc = definition.docstring().split('\n') else: return None # Keep only the first paragraph that is not a function declaration lines = [] call = "{}(".format(raw_name) # last line doc.append('') for i in range(len(doc)): if doc[i] == '' and len(lines) != 0: paragraph = " ".join(lines) lines = [] if call != paragraph[0:len(call)]: break paragraph = "" continue lines.append(doc[i]) # Keep only the first sentence onelinedoc = paragraph.split('. ', 1) if len(onelinedoc) == 2: onelinedoc = onelinedoc[0] + '.' else: onelinedoc = onelinedoc[0] if onelinedoc == '': onelinedoc = "No documentation" return {"name": name, "doc": onelinedoc}
def rpc_get_oneline_docstring_jedi16(self, filename, source, offset): # pragma: no cover """Return a oneline docstring for the symbol at offset""" # Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) definitions = run_with_debug(jedi, 'goto_definitions', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) # avoid unintersting stuff try: if definitions[0].name in ["str", "int", "float", "bool", "tuple", "list", "dict"]: return None except: pass assignments = run_with_debug(jedi, 'goto_assignments', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment)
if definitions: definition = definitions[0] else: definition = None if assignments: assignment = assignments[0] else: assignment = None if definition: # Get name if definition.type in ['function', 'class']: raw_name = definition.name name = '{}()'.format(raw_name) doc = definition.docstring().split('\n') elif definition.type in ['module']: raw_name = definition.name name = '{} {}'.format(raw_name, definition.type) doc = definition.docstring().split('\n') elif (definition.type in ['instance'] and hasattr(assignment, "name")): raw_name = assignment.name name = raw_name doc = assignment.docstring().split('\n') else: return None # Keep only the first paragraph that is not a function declaration lines = [] call = "{}(".format(raw_name) # last line doc.append('') for i in range(len(doc)): if doc[i] == '' and len(lines) != 0: paragraph = " ".join(lines) lines = [] if call != paragraph[0:len(call)]: break paragraph = "" continue lines.append(doc[i]) # Keep only the first sentence onelinedoc = paragraph.split('. ', 1) if len(onelinedoc) == 2: onelinedoc = onelinedoc[0] + '.' else: onelinedoc = onelinedoc[0] if onelinedoc == '': onelinedoc = "No documentation" return {"name": name, "doc": onelinedoc} return None
def rpc_get_usages(self, filename, source, offset): """Return the uses of the symbol at offset.
Returns a list of occurrences of the symbol, as dicts with the fields name, filename, and offset.
"""
line, column = pos_to_linecol(source, offset) uses = run_with_debug(jedi, 'get_references', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) if uses is None: return None result = [] for use in uses: if use.module_path == filename: offset = linecol_to_pos(source, use.line, use.column) elif use.module_path is not None: with open(use.module_path) as f: text = f.read() offset = linecol_to_pos(text, use.line, use.column) result.append({"name": use.name, "filename": use.module_path, "offset": offset}) return result
def rpc_get_usages_jedi16(self, filename, source, offset): # pragma: no cover """Return the uses of the symbol at offset.
Returns a list of occurrences of the symbol, as dicts with the fields name, filename, and offset.
"""
# Backward compatibility to jedi<17 line, column = pos_to_linecol(source, offset) uses = run_with_debug(jedi, 'usages', source=source, line=line, column=column, path=filename, encoding='utf-8', environment=self.environment) if uses is None: return None result = [] for use in uses: if use.module_path == filename: offset = linecol_to_pos(source, use.line, use.column) elif use.module_path is not None: with open(use.module_path) as f: text = f.read() offset = linecol_to_pos(text, use.line, use.column)
result.append({"name": use.name, "filename": use.module_path, "offset": offset})
return result
def rpc_get_names(self, filename, source, offset): """Return the list of possible names""" names = run_with_debug(jedi, 'get_names', code=source, path=filename, environment=self.environment, fun_kwargs={'all_scopes': True, 'definitions': True, 'references': True}) result = [] for name in names: if name.module_path == filename: offset = linecol_to_pos(source, name.line, name.column) elif name.module_path is not None: with open(name.module_path) as f: text = f.read() offset = linecol_to_pos(text, name.line, name.column) result.append({"name": name.name, "filename": name.module_path, "offset": offset}) return result
def rpc_get_names_jedi16(self, filename, source, offset): # pragma: no cover """Return the list of possible names""" # Backward compatibility to jedi<17 names = jedi.api.names(source=source, path=filename, encoding='utf-8', all_scopes=True, definitions=True, references=True)
result = [] for name in names: if name.module_path == filename: offset = linecol_to_pos(source, name.line, name.column) elif name.module_path is not None: with open(name.module_path) as f: text = f.read() offset = linecol_to_pos(text, name.line, name.column) result.append({"name": name.name, "filename": name.module_path, "offset": offset}) return result
def rpc_get_rename_diff(self, filename, source, offset, new_name): """Get the diff resulting from renaming the thing at point""" if not hasattr(jedi.Script, "rename"): # pragma: no cover return {'success': "Not available"} line, column = pos_to_linecol(source, offset) ren = run_with_debug(jedi, 'rename', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column, 'new_name': new_name}) if ren is None: return {'success': False} else: return {'success': True, 'project_path': ren._inference_state.project._path, 'diff': ren.get_diff(), 'changed_files': list(ren.get_changed_files().keys())}
def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): """Get the diff resulting from extracting the selected code""" if not hasattr(jedi.Script, "extract_variable"): # pragma: no cover return {'success': "Not available"} ren = run_with_debug(jedi, 'extract_variable', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line_beg, 'until_line': line_end, 'column': col_beg, 'until_column': col_end, 'new_name': new_name}) if ren is None: return {'success': False} else: return {'success': True, 'project_path': ren._inference_state.project._path, 'diff': ren.get_diff(), 'changed_files': list(ren.get_changed_files().keys())}
def rpc_get_extract_function_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): """Get the diff resulting from extracting the selected code""" if not hasattr(jedi.Script, "extract_function"): # pragma: no cover return {'success': "Not available"} ren = run_with_debug(jedi, 'extract_function', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line_beg, 'until_line': line_end, 'column': col_beg, 'until_column': col_end, 'new_name': new_name}) if ren is None: return {'success': False} else: return {'success': True, 'project_path': ren._inference_state.project._path, 'diff': ren.get_diff(), 'changed_files': list(ren.get_changed_files().keys())}
def rpc_get_inline_diff(self, filename, source, offset): """Get the diff resulting from inlining the selected variable""" if not hasattr(jedi.Script, "inline"): # pragma: no cover return {'success': "Not available"} line, column = pos_to_linecol(source, offset) ren = run_with_debug(jedi, 'inline', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) if ren is None: return {'success': False} else: return {'success': True, 'project_path': ren._inference_state.project._path, 'diff': ren.get_diff(), 'changed_files': list(ren.get_changed_files().keys())}
# From the Jedi documentation:## line is the current line you want to perform actions on (starting# with line #1 as the first line). column represents the current# column/indent of the cursor (starting with zero). source_path# should be the path of your file in the file system.
def pos_to_linecol(text, pos): """Return a tuple of line and column for offset pos in text.
Lines are one-based, columns zero-based.
This is how Jedi wants it. Don't ask me why.
"""
line_start = text.rfind("\n", 0, pos) + 1 line = text.count("\n", 0, line_start) + 1 col = pos - line_start return line, col
def linecol_to_pos(text, line, col): """Return the offset of this line and column in text.
Lines are one-based, columns zero-based.
This is how Jedi wants it. Don't ask me why.
"""
nth_newline_offset = 0 for i in range(line - 1): new_offset = text.find("\n", nth_newline_offset) if new_offset < 0: raise ValueError("Text does not have {0} lines." .format(line)) nth_newline_offset = new_offset + 1 offset = nth_newline_offset + col if offset > len(text): raise ValueError("Line {0} column {1} is not within the text" .format(line, col)) return offset
def run_with_debug(jedi, name, fun_kwargs={}, *args, **kwargs): re_raise = kwargs.pop('re_raise', ()) try: script = jedi.Script(*args, **kwargs) return getattr(script, name)(**fun_kwargs) except Exception as e: if isinstance(e, re_raise): raise if JEDISUP17: if isinstance(e, jedi.RefactoringError): return None # Bug jedi#485 if ( isinstance(e, ValueError) and "invalid \\x escape" in str(e) ): return None # Bug jedi#485 in Python 3 if ( isinstance(e, SyntaxError) and "truncated \\xXX escape" in str(e) ): return None
from jedi import debug
debug_info = []
def _debug(level, str_out): if level == debug.NOTICE: prefix = "[N]" elif level == debug.WARNING: prefix = "[W]" else: prefix = "[?]" debug_info.append(u"{0} {1}".format(prefix, str_out))
jedi.set_debug_function(_debug, speed=False) try: script = jedi.Script(*args, **kwargs) return getattr(script, name)() except Exception as e: source = kwargs.get('source') sc_args = [] sc_args.extend(repr(arg) for arg in args) sc_args.extend("{0}={1}".format(k, "source" if k == "source" else repr(v)) for (k, v) in kwargs.items())
data = { "traceback": traceback.format_exc(), "jedi_debug_info": {'script_args': ", ".join(sc_args), 'source': source, 'method': name, 'debug_info': debug_info} } raise rpc.Fault(message=str(e), code=500, data=data) finally: jedi.set_debug_function(None)
|