|
|
- """Elpy backend using the Jedi library.
-
- This backend uses the Jedi library:
-
- https://github.com/davidhalter/jedi
-
- """
-
- import sys
- import traceback
- import re
-
- import jedi
-
- from elpy import rpc
-
-
- 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):
- self.project_root = project_root
- self.completions = {}
- sys.path.append(project_root)
-
- def rpc_get_completions(self, filename, source, offset):
- line, column = pos_to_linecol(source, offset)
- proposals = run_with_debug(jedi, 'completions',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- 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_definitions',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- if locations and 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_definitions',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- # 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')
- 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):
- 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')
- 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, 'call_signatures',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- if calls:
- call = calls[0]
- else:
- call = None
- if not call:
- return None
- # Strip 'param' added by jedi at the beggining 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_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_definitions',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- assignments = run_with_debug(jedi, 'goto_assignments',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- 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, 'usages',
- source=source, line=line, column=column,
- path=filename, encoding='utf-8')
- 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 = 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
-
-
- # 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, *args, **kwargs):
- re_raise = kwargs.pop('re_raise', ())
- try:
- script = jedi.Script(*args, **kwargs)
- return getattr(script, name)()
- except Exception as e:
- if isinstance(e, re_raise):
- raise
- # 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)
|