Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

422 lines
16 KiB

  1. """Elpy backend using the Jedi library.
  2. This backend uses the Jedi library:
  3. https://github.com/davidhalter/jedi
  4. """
  5. import sys
  6. import traceback
  7. import re
  8. import jedi
  9. from elpy import rpc
  10. class JediBackend(object):
  11. """The Jedi backend class.
  12. Implements the RPC calls we can pass on to Jedi.
  13. Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html
  14. """
  15. name = "jedi"
  16. def __init__(self, project_root, environment_binaries_path):
  17. self.project_root = project_root
  18. self.environment = None
  19. if environment_binaries_path is not None:
  20. self.environment = jedi.create_environment(environment_binaries_path,
  21. safe=False)
  22. self.completions = {}
  23. sys.path.append(project_root)
  24. def rpc_get_completions(self, filename, source, offset):
  25. line, column = pos_to_linecol(source, offset)
  26. proposals = run_with_debug(jedi, 'completions',
  27. source=source, line=line, column=column,
  28. path=filename, encoding='utf-8',
  29. environment=self.environment)
  30. if proposals is None:
  31. return []
  32. self.completions = dict((proposal.name, proposal)
  33. for proposal in proposals)
  34. return [{'name': proposal.name.rstrip("="),
  35. 'suffix': proposal.complete.rstrip("="),
  36. 'annotation': proposal.type,
  37. 'meta': proposal.description}
  38. for proposal in proposals]
  39. def rpc_get_completion_docstring(self, completion):
  40. proposal = self.completions.get(completion)
  41. if proposal is None:
  42. return None
  43. else:
  44. return proposal.docstring(fast=False)
  45. def rpc_get_completion_location(self, completion):
  46. proposal = self.completions.get(completion)
  47. if proposal is None:
  48. return None
  49. else:
  50. return (proposal.module_path, proposal.line)
  51. def rpc_get_docstring(self, filename, source, offset):
  52. line, column = pos_to_linecol(source, offset)
  53. locations = run_with_debug(jedi, 'goto_definitions',
  54. source=source, line=line, column=column,
  55. path=filename, encoding='utf-8',
  56. environment=self.environment)
  57. if not locations:
  58. return None
  59. # Filter uninteresting things
  60. if locations[-1].name in ["str", "int", "float", "bool", "tuple",
  61. "list", "dict"]:
  62. return None
  63. if locations[-1].docstring():
  64. return ('Documentation for {0}:\n\n'.format(
  65. locations[-1].full_name) + locations[-1].docstring())
  66. else:
  67. return None
  68. def rpc_get_definition(self, filename, source, offset):
  69. line, column = pos_to_linecol(source, offset)
  70. locations = run_with_debug(jedi, 'goto_definitions',
  71. source=source, line=line, column=column,
  72. path=filename, encoding='utf-8',
  73. environment=self.environment)
  74. # goto_definitions() can return silly stuff like __builtin__
  75. # for int variables, so we fall back on goto() in those
  76. # cases. See issue #76.
  77. if (
  78. locations and
  79. (locations[0].module_path is None
  80. or locations[0].module_name == 'builtins'
  81. or locations[0].module_name == '__builtin__')
  82. ):
  83. locations = run_with_debug(jedi, 'goto_assignments',
  84. source=source, line=line,
  85. column=column,
  86. path=filename, encoding='utf-8',
  87. environment=self.environment)
  88. if not locations:
  89. return None
  90. else:
  91. loc = locations[-1]
  92. try:
  93. if loc.module_path:
  94. if loc.module_path == filename:
  95. offset = linecol_to_pos(source,
  96. loc.line,
  97. loc.column)
  98. else:
  99. with open(loc.module_path) as f:
  100. offset = linecol_to_pos(f.read(),
  101. loc.line,
  102. loc.column)
  103. else:
  104. return None
  105. except IOError:
  106. return None
  107. return (loc.module_path, offset)
  108. def rpc_get_assignment(self, filename, source, offset):
  109. line, column = pos_to_linecol(source, offset)
  110. locations = run_with_debug(jedi, 'goto_assignments',
  111. source=source, line=line, column=column,
  112. path=filename, encoding='utf-8',
  113. environment=self.environment)
  114. if not locations:
  115. return None
  116. else:
  117. loc = locations[-1]
  118. try:
  119. if loc.module_path:
  120. if loc.module_path == filename:
  121. offset = linecol_to_pos(source,
  122. loc.line,
  123. loc.column)
  124. else:
  125. with open(loc.module_path) as f:
  126. offset = linecol_to_pos(f.read(),
  127. loc.line,
  128. loc.column)
  129. else:
  130. return None
  131. except IOError:
  132. return None
  133. return (loc.module_path, offset)
  134. def rpc_get_calltip(self, filename, source, offset):
  135. line, column = pos_to_linecol(source, offset)
  136. calls = run_with_debug(jedi, 'call_signatures',
  137. source=source, line=line, column=column,
  138. path=filename, encoding='utf-8',
  139. environment=self.environment)
  140. if calls:
  141. call = calls[0]
  142. else:
  143. call = None
  144. if not call:
  145. return None
  146. # Strip 'param' added by jedi at the beginning of
  147. # parameter names. Should be unecessary for jedi > 0.13.0
  148. params = [re.sub("^param ", '', param.description)
  149. for param in call.params]
  150. return {"name": call.name,
  151. "index": call.index,
  152. "params": params}
  153. def rpc_get_calltip_or_oneline_docstring(self, filename, source, offset):
  154. """
  155. Return the current function calltip or its oneline docstring.
  156. Meant to be used with eldoc.
  157. """
  158. # Try to get a oneline docstring then
  159. docs = self.rpc_get_oneline_docstring(filename=filename,
  160. source=source,
  161. offset=offset)
  162. if docs is not None:
  163. if docs['doc'] != "No documentation":
  164. docs['kind'] = 'oneline_doc'
  165. return docs
  166. # Try to get a calltip
  167. calltip = self.rpc_get_calltip(filename=filename, source=source,
  168. offset=offset)
  169. if calltip is not None:
  170. calltip['kind'] = 'calltip'
  171. return calltip
  172. # Ok, no calltip, just display the function name
  173. if docs is not None:
  174. docs['kind'] = 'oneline_doc'
  175. return docs
  176. # Giving up...
  177. return None
  178. def rpc_get_oneline_docstring(self, filename, source, offset):
  179. """Return a oneline docstring for the symbol at offset"""
  180. line, column = pos_to_linecol(source, offset)
  181. definitions = run_with_debug(jedi, 'goto_definitions',
  182. source=source, line=line, column=column,
  183. path=filename, encoding='utf-8',
  184. environment=self.environment)
  185. # avoid unintersting stuff
  186. try:
  187. if definitions[0].name in ["str", "int", "float", "bool", "tuple",
  188. "list", "dict"]:
  189. return None
  190. except:
  191. pass
  192. assignments = run_with_debug(jedi, 'goto_assignments',
  193. source=source, line=line, column=column,
  194. path=filename, encoding='utf-8',
  195. environment=self.environment)
  196. if definitions:
  197. definition = definitions[0]
  198. else:
  199. definition = None
  200. if assignments:
  201. assignment = assignments[0]
  202. else:
  203. assignment = None
  204. if definition:
  205. # Get name
  206. if definition.type in ['function', 'class']:
  207. raw_name = definition.name
  208. name = '{}()'.format(raw_name)
  209. doc = definition.docstring().split('\n')
  210. elif definition.type in ['module']:
  211. raw_name = definition.name
  212. name = '{} {}'.format(raw_name, definition.type)
  213. doc = definition.docstring().split('\n')
  214. elif (definition.type in ['instance']
  215. and hasattr(assignment, "name")):
  216. raw_name = assignment.name
  217. name = raw_name
  218. doc = assignment.docstring().split('\n')
  219. else:
  220. return None
  221. # Keep only the first paragraph that is not a function declaration
  222. lines = []
  223. call = "{}(".format(raw_name)
  224. # last line
  225. doc.append('')
  226. for i in range(len(doc)):
  227. if doc[i] == '' and len(lines) != 0:
  228. paragraph = " ".join(lines)
  229. lines = []
  230. if call != paragraph[0:len(call)]:
  231. break
  232. paragraph = ""
  233. continue
  234. lines.append(doc[i])
  235. # Keep only the first sentence
  236. onelinedoc = paragraph.split('. ', 1)
  237. if len(onelinedoc) == 2:
  238. onelinedoc = onelinedoc[0] + '.'
  239. else:
  240. onelinedoc = onelinedoc[0]
  241. if onelinedoc == '':
  242. onelinedoc = "No documentation"
  243. return {"name": name,
  244. "doc": onelinedoc}
  245. return None
  246. def rpc_get_usages(self, filename, source, offset):
  247. """Return the uses of the symbol at offset.
  248. Returns a list of occurrences of the symbol, as dicts with the
  249. fields name, filename, and offset.
  250. """
  251. line, column = pos_to_linecol(source, offset)
  252. uses = run_with_debug(jedi, 'usages',
  253. source=source, line=line, column=column,
  254. path=filename, encoding='utf-8',
  255. environment=self.environment)
  256. if uses is None:
  257. return None
  258. result = []
  259. for use in uses:
  260. if use.module_path == filename:
  261. offset = linecol_to_pos(source, use.line, use.column)
  262. elif use.module_path is not None:
  263. with open(use.module_path) as f:
  264. text = f.read()
  265. offset = linecol_to_pos(text, use.line, use.column)
  266. result.append({"name": use.name,
  267. "filename": use.module_path,
  268. "offset": offset})
  269. return result
  270. def rpc_get_names(self, filename, source, offset):
  271. """Return the list of possible names"""
  272. names = jedi.api.names(source=source,
  273. path=filename, encoding='utf-8',
  274. all_scopes=True,
  275. definitions=True,
  276. references=True)
  277. result = []
  278. for name in names:
  279. if name.module_path == filename:
  280. offset = linecol_to_pos(source, name.line, name.column)
  281. elif name.module_path is not None:
  282. with open(name.module_path) as f:
  283. text = f.read()
  284. offset = linecol_to_pos(text, name.line, name.column)
  285. result.append({"name": name.name,
  286. "filename": name.module_path,
  287. "offset": offset})
  288. return result
  289. # From the Jedi documentation:
  290. #
  291. # line is the current line you want to perform actions on (starting
  292. # with line #1 as the first line). column represents the current
  293. # column/indent of the cursor (starting with zero). source_path
  294. # should be the path of your file in the file system.
  295. def pos_to_linecol(text, pos):
  296. """Return a tuple of line and column for offset pos in text.
  297. Lines are one-based, columns zero-based.
  298. This is how Jedi wants it. Don't ask me why.
  299. """
  300. line_start = text.rfind("\n", 0, pos) + 1
  301. line = text.count("\n", 0, line_start) + 1
  302. col = pos - line_start
  303. return line, col
  304. def linecol_to_pos(text, line, col):
  305. """Return the offset of this line and column in text.
  306. Lines are one-based, columns zero-based.
  307. This is how Jedi wants it. Don't ask me why.
  308. """
  309. nth_newline_offset = 0
  310. for i in range(line - 1):
  311. new_offset = text.find("\n", nth_newline_offset)
  312. if new_offset < 0:
  313. raise ValueError("Text does not have {0} lines."
  314. .format(line))
  315. nth_newline_offset = new_offset + 1
  316. offset = nth_newline_offset + col
  317. if offset > len(text):
  318. raise ValueError("Line {0} column {1} is not within the text"
  319. .format(line, col))
  320. return offset
  321. def run_with_debug(jedi, name, *args, **kwargs):
  322. re_raise = kwargs.pop('re_raise', ())
  323. try:
  324. script = jedi.Script(*args, **kwargs)
  325. return getattr(script, name)()
  326. except Exception as e:
  327. if isinstance(e, re_raise):
  328. raise
  329. # Bug jedi#485
  330. if (
  331. isinstance(e, ValueError) and
  332. "invalid \\x escape" in str(e)
  333. ):
  334. return None
  335. # Bug jedi#485 in Python 3
  336. if (
  337. isinstance(e, SyntaxError) and
  338. "truncated \\xXX escape" in str(e)
  339. ):
  340. return None
  341. from jedi import debug
  342. debug_info = []
  343. def _debug(level, str_out):
  344. if level == debug.NOTICE:
  345. prefix = "[N]"
  346. elif level == debug.WARNING:
  347. prefix = "[W]"
  348. else:
  349. prefix = "[?]"
  350. debug_info.append(u"{0} {1}".format(prefix, str_out))
  351. jedi.set_debug_function(_debug, speed=False)
  352. try:
  353. script = jedi.Script(*args, **kwargs)
  354. return getattr(script, name)()
  355. except Exception as e:
  356. source = kwargs.get('source')
  357. sc_args = []
  358. sc_args.extend(repr(arg) for arg in args)
  359. sc_args.extend("{0}={1}".format(k, "source" if k == "source"
  360. else repr(v))
  361. for (k, v) in kwargs.items())
  362. data = {
  363. "traceback": traceback.format_exc(),
  364. "jedi_debug_info": {'script_args': ", ".join(sc_args),
  365. 'source': source,
  366. 'method': name,
  367. 'debug_info': debug_info}
  368. }
  369. raise rpc.Fault(message=str(e),
  370. code=500,
  371. data=data)
  372. finally:
  373. jedi.set_debug_function(None)