1 """Elpy backend using the Jedi library.
3 This backend uses the Jedi library:
5 https://github.com/davidhalter/jedi
16 from elpy
.rpc
import Fault
18 # in case pkg_resources is not properly installed
19 # (see https://github.com/jorgenschaefer/elpy/issues/1674).
21 from pkg_resources
import parse_version
22 except ImportError: # pragma: no cover
23 def parse_version(*arg
, **kwargs
):
24 raise Fault("`pkg_resources` could not be imported, "
25 "please reinstall Elpy RPC virtualenv with"
26 " `M-x elpy-rpc-reinstall-virtualenv`", code
=400)
27 JEDISUP17
= parse_version(jedi
.__version
__) >= parse_version("0.17.0")
28 JEDISUP18
= parse_version(jedi
.__version
__) >= parse_version("0.18.0")
31 class JediBackend(object):
32 """The Jedi backend class.
34 Implements the RPC calls we can pass on to Jedi.
36 Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html
41 def __init__(self
, project_root
, environment_binaries_path
):
42 self
.project_root
= project_root
43 self
.environment
= None
44 if environment_binaries_path
is not None:
45 self
.environment
= jedi
.create_environment(environment_binaries_path
,
48 sys
.path
.append(project_root
)
49 # Backward compatibility to jedi<17
50 if not JEDISUP17
: # pragma: no cover
51 self
.rpc_get_completions
= self
.rpc_get_completions_jedi16
52 self
.rpc_get_docstring
= self
.rpc_get_docstring_jedi16
53 self
.rpc_get_definition
= self
.rpc_get_definition_jedi16
54 self
.rpc_get_assignment
= self
.rpc_get_assignment_jedi16
55 self
.rpc_get_calltip
= self
.rpc_get_calltip_jedi16
56 self
.rpc_get_oneline_docstring
= self
.rpc_get_oneline_docstring_jedi16
57 self
.rpc_get_usages
= self
.rpc_get_usages_jedi16
58 self
.rpc_get_names
= self
.rpc_get_names_jedi16
60 def rpc_get_completions(self
, filename
, source
, offset
):
61 line
, column
= pos_to_linecol(source
, offset
)
62 proposals
= run_with_debug(jedi
, 'complete', code
=source
,
64 environment
=self
.environment
,
65 fun_kwargs
={'line': line
, 'column': column
})
66 self
.completions
= dict((proposal
.name
, proposal
)
67 for proposal
in proposals
)
68 return [{'name': proposal
.name
.rstrip("="),
69 'suffix': proposal
.complete
.rstrip("="),
70 'annotation': proposal
.type,
71 'meta': proposal
.description
}
72 for proposal
in proposals
]
74 def rpc_get_completions_jedi16(self
, filename
, source
, offset
):
75 # Backward compatibility to jedi<17
76 line
, column
= pos_to_linecol(source
, offset
)
77 proposals
= run_with_debug(jedi
, 'completions',
78 source
=source
, line
=line
, column
=column
,
79 path
=filename
, encoding
='utf-8',
80 environment
=self
.environment
)
83 self
.completions
= dict((proposal
.name
, proposal
)
84 for proposal
in proposals
)
85 return [{'name': proposal
.name
.rstrip("="),
86 'suffix': proposal
.complete
.rstrip("="),
87 'annotation': proposal
.type,
88 'meta': proposal
.description
}
89 for proposal
in proposals
]
91 def rpc_get_completion_docstring(self
, completion
):
92 proposal
= self
.completions
.get(completion
)
96 return proposal
.docstring(fast
=False)
98 def rpc_get_completion_location(self
, completion
):
99 proposal
= self
.completions
.get(completion
)
103 return (proposal
.module_path
, proposal
.line
)
105 def rpc_get_docstring(self
, filename
, source
, offset
):
106 line
, column
= pos_to_linecol(source
, offset
)
107 locations
= run_with_debug(jedi
, 'goto',
110 environment
=self
.environment
,
111 fun_kwargs
={'line': line
,
113 'follow_imports': True,
114 'follow_builtin_imports': True})
117 # Filter uninteresting things
118 if locations
[-1].name
in ["str", "int", "float", "bool", "tuple",
121 if locations
[-1].docstring():
122 return ('Documentation for {0}:\n\n'.format(
123 locations
[-1].full_name
) + locations
[-1].docstring())
127 def rpc_get_docstring_jedi16(self
, filename
, source
, offset
):
128 # Backward compatibility to jedi<17
129 line
, column
= pos_to_linecol(source
, offset
)
130 locations
= run_with_debug(jedi
, 'goto_definitions',
131 source
=source
, line
=line
, column
=column
,
132 path
=filename
, encoding
='utf-8',
133 environment
=self
.environment
)
136 # Filter uninteresting things
137 if locations
[-1].name
in ["str", "int", "float", "bool", "tuple",
140 if locations
[-1].docstring():
141 return ('Documentation for {0}:\n\n'.format(
142 locations
[-1].full_name
) + locations
[-1].docstring())
146 def rpc_get_definition(self
, filename
, source
, offset
):
147 line
, column
= pos_to_linecol(source
, offset
)
148 locations
= run_with_debug(jedi
, 'goto',
151 environment
=self
.environment
,
152 fun_kwargs
={'line': line
,
154 'follow_imports': True,
155 'follow_builtin_imports': True})
158 # goto_definitions() can return silly stuff like __builtin__
159 # for int variables, so we remove them. See issue #76.
161 loc
for loc
in locations
162 if (loc
.module_path
is not None
163 and loc
.module_name
!= 'builtins'
164 and loc
.module_name
!= '__builtin__')]
165 if len(locations
) == 0:
169 if loc
.module_path
== filename
:
170 offset
= linecol_to_pos(source
,
174 with
open(loc
.module_path
) as f
:
175 offset
= linecol_to_pos(f
.read(),
178 except IOError: # pragma: no cover
180 return (loc
.module_path
, offset
)
182 def rpc_get_definition_jedi16(self
, filename
, source
, offset
): # pragma: no cover
183 # Backward compatibility to jedi<17
184 line
, column
= pos_to_linecol(source
, offset
)
185 locations
= run_with_debug(jedi
, 'goto_definitions',
186 source
=source
, line
=line
, column
=column
,
187 path
=filename
, encoding
='utf-8',
188 environment
=self
.environment
)
189 # goto_definitions() can return silly stuff like __builtin__
190 # for int variables, so we fall back on goto() in those
191 # cases. See issue #76.
194 (locations
[0].module_path
is None
195 or locations
[0].module_name
== 'builtins'
196 or locations
[0].module_name
== '__builtin__')
198 locations
= run_with_debug(jedi
, 'goto_assignments',
199 source
=source
, line
=line
,
203 environment
=self
.environment
)
210 if loc
.module_path
== filename
:
211 offset
= linecol_to_pos(source
,
215 with
open(loc
.module_path
) as f
:
216 offset
= linecol_to_pos(f
.read(),
223 return (loc
.module_path
, offset
)
225 def rpc_get_assignment(self
, filename
, source
, offset
):
226 raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.")
228 def rpc_get_assignment_jedi16(self
, filename
, source
, offset
): # pragma: no cover
229 # Backward compatibility to jedi<17
230 line
, column
= pos_to_linecol(source
, offset
)
231 locations
= run_with_debug(jedi
, 'goto_assignments',
232 source
=source
, line
=line
, column
=column
,
233 path
=filename
, encoding
='utf-8',
234 environment
=self
.environment
)
242 if loc
.module_path
== filename
:
243 offset
= linecol_to_pos(source
,
247 with
open(loc
.module_path
) as f
:
248 offset
= linecol_to_pos(f
.read(),
255 return (loc
.module_path
, offset
)
257 def rpc_get_calltip(self
, filename
, source
, offset
):
258 line
, column
= pos_to_linecol(source
, offset
)
259 calls
= run_with_debug(jedi
, 'get_signatures',
262 environment
=self
.environment
,
263 fun_kwargs
={'line': line
,
267 params
= [re
.sub("^param ", '', param
.description
)
268 for param
in calls
[0].params
]
269 return {"name": calls
[0].name
,
270 "index": calls
[0].index
,
273 def rpc_get_calltip_jedi16(self
, filename
, source
, offset
): # pragma: no cover
274 # Backward compatibility to jedi<17
275 line
, column
= pos_to_linecol(source
, offset
)
276 calls
= run_with_debug(jedi
, 'call_signatures',
277 source
=source
, line
=line
, column
=column
,
278 path
=filename
, encoding
='utf-8',
279 environment
=self
.environment
)
286 # Strip 'param' added by jedi at the beginning of
287 # parameter names. Should be unecessary for jedi > 0.13.0
288 params
= [re
.sub("^param ", '', param
.description
)
289 for param
in call
.params
]
290 return {"name": call
.name
,
294 def rpc_get_calltip_or_oneline_docstring(self
, filename
, source
, offset
):
296 Return the current function calltip or its oneline docstring.
298 Meant to be used with eldoc.
300 # Try to get a oneline docstring then
301 docs
= self
.rpc_get_oneline_docstring(filename
=filename
,
305 if docs
['doc'] != "No documentation":
306 docs
['kind'] = 'oneline_doc'
308 # Try to get a calltip
309 calltip
= self
.rpc_get_calltip(filename
=filename
, source
=source
,
311 if calltip
is not None:
312 calltip
['kind'] = 'calltip'
314 # Ok, no calltip, just display the function name
316 docs
['kind'] = 'oneline_doc'
321 def rpc_get_oneline_docstring(self
, filename
, source
, offset
):
322 """Return a oneline docstring for the symbol at offset"""
323 line
, column
= pos_to_linecol(source
, offset
)
324 definitions
= run_with_debug(jedi
, 'goto',
327 environment
=self
.environment
,
328 fun_kwargs
={'line': line
,
332 # avoid unintersting stuff
333 definitions
= [defi
for defi
in definitions
335 ["str", "int", "float", "bool", "tuple",
337 if len(definitions
) == 0:
339 definition
= definitions
[0]
341 if definition
.type in ['function', 'class']:
342 raw_name
= definition
.name
343 name
= '{}()'.format(raw_name
)
344 doc
= definition
.docstring().split('\n')
345 elif definition
.type in ['module']:
346 raw_name
= definition
.name
347 name
= '{} {}'.format(raw_name
, definition
.type)
348 doc
= definition
.docstring().split('\n')
349 elif (definition
.type in ['instance']
350 and hasattr(definition
, "name")):
351 raw_name
= definition
.name
353 doc
= definition
.docstring().split('\n')
356 # Keep only the first paragraph that is not a function declaration
358 call
= "{}(".format(raw_name
)
361 for i
in range(len(doc
)):
362 if doc
[i
] == '' and len(lines
) != 0:
363 paragraph
= " ".join(lines
)
365 if call
!= paragraph
[0:len(call
)]:
370 # Keep only the first sentence
371 onelinedoc
= paragraph
.split('. ', 1)
372 if len(onelinedoc
) == 2:
373 onelinedoc
= onelinedoc
[0] + '.'
375 onelinedoc
= onelinedoc
[0]
377 onelinedoc
= "No documentation"
378 return {"name": name
,
381 def rpc_get_oneline_docstring_jedi16(self
, filename
, source
, offset
): # pragma: no cover
382 """Return a oneline docstring for the symbol at offset"""
383 # Backward compatibility to jedi<17
384 line
, column
= pos_to_linecol(source
, offset
)
385 definitions
= run_with_debug(jedi
, 'goto_definitions',
386 source
=source
, line
=line
, column
=column
,
387 path
=filename
, encoding
='utf-8',
388 environment
=self
.environment
)
389 # avoid unintersting stuff
391 if definitions
[0].name
in ["str", "int", "float", "bool", "tuple",
396 assignments
= run_with_debug(jedi
, 'goto_assignments',
397 source
=source
, line
=line
, column
=column
,
398 path
=filename
, encoding
='utf-8',
399 environment
=self
.environment
)
402 definition
= definitions
[0]
406 assignment
= assignments
[0]
411 if definition
.type in ['function', 'class']:
412 raw_name
= definition
.name
413 name
= '{}()'.format(raw_name
)
414 doc
= definition
.docstring().split('\n')
415 elif definition
.type in ['module']:
416 raw_name
= definition
.name
417 name
= '{} {}'.format(raw_name
, definition
.type)
418 doc
= definition
.docstring().split('\n')
419 elif (definition
.type in ['instance']
420 and hasattr(assignment
, "name")):
421 raw_name
= assignment
.name
423 doc
= assignment
.docstring().split('\n')
426 # Keep only the first paragraph that is not a function declaration
428 call
= "{}(".format(raw_name
)
431 for i
in range(len(doc
)):
432 if doc
[i
] == '' and len(lines
) != 0:
433 paragraph
= " ".join(lines
)
435 if call
!= paragraph
[0:len(call
)]:
440 # Keep only the first sentence
441 onelinedoc
= paragraph
.split('. ', 1)
442 if len(onelinedoc
) == 2:
443 onelinedoc
= onelinedoc
[0] + '.'
445 onelinedoc
= onelinedoc
[0]
447 onelinedoc
= "No documentation"
448 return {"name": name
,
452 def rpc_get_usages(self
, filename
, source
, offset
):
453 """Return the uses of the symbol at offset.
455 Returns a list of occurrences of the symbol, as dicts with the
456 fields name, filename, and offset.
459 line
, column
= pos_to_linecol(source
, offset
)
460 uses
= run_with_debug(jedi
, 'get_references',
463 environment
=self
.environment
,
464 fun_kwargs
={'line': line
,
470 if use
.module_path
== filename
:
471 offset
= linecol_to_pos(source
, use
.line
, use
.column
)
472 elif use
.module_path
is not None:
473 with
open(use
.module_path
) as f
:
475 offset
= linecol_to_pos(text
, use
.line
, use
.column
)
476 result
.append({"name": use
.name
,
477 "filename": use
.module_path
,
481 def rpc_get_usages_jedi16(self
, filename
, source
, offset
): # pragma: no cover
482 """Return the uses of the symbol at offset.
484 Returns a list of occurrences of the symbol, as dicts with the
485 fields name, filename, and offset.
488 # Backward compatibility to jedi<17
489 line
, column
= pos_to_linecol(source
, offset
)
490 uses
= run_with_debug(jedi
, 'usages',
491 source
=source
, line
=line
, column
=column
,
492 path
=filename
, encoding
='utf-8',
493 environment
=self
.environment
)
498 if use
.module_path
== filename
:
499 offset
= linecol_to_pos(source
, use
.line
, use
.column
)
500 elif use
.module_path
is not None:
501 with
open(use
.module_path
) as f
:
503 offset
= linecol_to_pos(text
, use
.line
, use
.column
)
505 result
.append({"name": use
.name
,
506 "filename": use
.module_path
,
511 def rpc_get_names(self
, filename
, source
, offset
):
512 """Return the list of possible names"""
513 names
= run_with_debug(jedi
, 'get_names',
516 environment
=self
.environment
,
517 fun_kwargs
={'all_scopes': True,
522 if name
.module_path
== filename
:
523 offset
= linecol_to_pos(source
, name
.line
, name
.column
)
524 elif name
.module_path
is not None:
525 with
open(name
.module_path
) as f
:
527 offset
= linecol_to_pos(text
, name
.line
, name
.column
)
528 result
.append({"name": name
.name
,
529 "filename": name
.module_path
,
533 def rpc_get_names_jedi16(self
, filename
, source
, offset
): # pragma: no cover
534 """Return the list of possible names"""
535 # Backward compatibility to jedi<17
536 names
= jedi
.api
.names(source
=source
,
537 path
=filename
, encoding
='utf-8',
544 if name
.module_path
== filename
:
545 offset
= linecol_to_pos(source
, name
.line
, name
.column
)
546 elif name
.module_path
is not None:
547 with
open(name
.module_path
) as f
:
549 offset
= linecol_to_pos(text
, name
.line
, name
.column
)
550 result
.append({"name": name
.name
,
551 "filename": name
.module_path
,
555 def rpc_get_rename_diff(self
, filename
, source
, offset
, new_name
):
556 """Get the diff resulting from renaming the thing at point"""
557 if not hasattr(jedi
.Script
, "rename"): # pragma: no cover
558 return {'success': "Not available"}
559 line
, column
= pos_to_linecol(source
, offset
)
560 ren
= run_with_debug(jedi
, 'rename', code
=source
,
562 environment
=self
.environment
,
563 fun_kwargs
={'line': line
,
565 'new_name': new_name
})
567 return {'success': False}
569 return {'success': True,
570 'project_path': ren
._inference
_state
.project
._path
,
571 'diff': ren
.get_diff(),
572 'changed_files': list(ren
.get_changed_files().keys())}
574 def rpc_get_extract_variable_diff(self
, filename
, source
, offset
, new_name
,
575 line_beg
, line_end
, col_beg
, col_end
):
576 """Get the diff resulting from extracting the selected code"""
577 if not hasattr(jedi
.Script
, "extract_variable"): # pragma: no cover
578 return {'success': "Not available"}
579 ren
= run_with_debug(jedi
, 'extract_variable', code
=source
,
581 environment
=self
.environment
,
582 fun_kwargs
={'line': line_beg
,
583 'until_line': line_end
,
585 'until_column': col_end
,
586 'new_name': new_name
})
588 return {'success': False}
590 return {'success': True,
591 'project_path': ren
._inference
_state
.project
._path
,
592 'diff': ren
.get_diff(),
593 'changed_files': list(ren
.get_changed_files().keys())}
595 def rpc_get_extract_function_diff(self
, filename
, source
, offset
, new_name
,
596 line_beg
, line_end
, col_beg
, col_end
):
597 """Get the diff resulting from extracting the selected code"""
598 if not hasattr(jedi
.Script
, "extract_function"): # pragma: no cover
599 return {'success': "Not available"}
600 ren
= run_with_debug(jedi
, 'extract_function', code
=source
,
602 environment
=self
.environment
,
603 fun_kwargs
={'line': line_beg
,
604 'until_line': line_end
,
606 'until_column': col_end
,
607 'new_name': new_name
})
609 return {'success': False}
611 return {'success': True,
612 'project_path': ren
._inference
_state
.project
._path
,
613 'diff': ren
.get_diff(),
614 'changed_files': list(ren
.get_changed_files().keys())}
616 def rpc_get_inline_diff(self
, filename
, source
, offset
):
617 """Get the diff resulting from inlining the selected variable"""
618 if not hasattr(jedi
.Script
, "inline"): # pragma: no cover
619 return {'success': "Not available"}
620 line
, column
= pos_to_linecol(source
, offset
)
621 ren
= run_with_debug(jedi
, 'inline', code
=source
,
623 environment
=self
.environment
,
624 fun_kwargs
={'line': line
,
627 return {'success': False}
629 return {'success': True,
630 'project_path': ren
._inference
_state
.project
._path
,
631 'diff': ren
.get_diff(),
632 'changed_files': list(ren
.get_changed_files().keys())}
635 # From the Jedi documentation:
637 # line is the current line you want to perform actions on (starting
638 # with line #1 as the first line). column represents the current
639 # column/indent of the cursor (starting with zero). source_path
640 # should be the path of your file in the file system.
642 def pos_to_linecol(text
, pos
):
643 """Return a tuple of line and column for offset pos in text.
645 Lines are one-based, columns zero-based.
647 This is how Jedi wants it. Don't ask me why.
650 line_start
= text
.rfind("\n", 0, pos
) + 1
651 line
= text
.count("\n", 0, line_start
) + 1
652 col
= pos
- line_start
656 def linecol_to_pos(text
, line
, col
):
657 """Return the offset of this line and column in text.
659 Lines are one-based, columns zero-based.
661 This is how Jedi wants it. Don't ask me why.
664 nth_newline_offset
= 0
665 for i
in range(line
- 1):
666 new_offset
= text
.find("\n", nth_newline_offset
)
668 raise ValueError("Text does not have {0} lines."
670 nth_newline_offset
= new_offset
+ 1
671 offset
= nth_newline_offset
+ col
672 if offset
> len(text
):
673 raise ValueError("Line {0} column {1} is not within the text"
678 def run_with_debug(jedi
, name
, fun_kwargs
={}, *args
, **kwargs
):
679 re_raise
= kwargs
.pop('re_raise', ())
681 script
= jedi
.Script(*args
, **kwargs
)
682 return getattr(script
, name
)(**fun_kwargs
)
683 except Exception as e
:
684 if isinstance(e
, re_raise
):
687 if isinstance(e
, jedi
.RefactoringError
):
691 isinstance(e
, ValueError) and
692 "invalid \\x escape" in str(e
)
695 # Bug jedi#485 in Python 3
697 isinstance(e
, SyntaxError) and
698 "truncated \\xXX escape" in str(e
)
702 from jedi
import debug
706 def _debug(level
, str_out
):
707 if level
== debug
.NOTICE
:
709 elif level
== debug
.WARNING
:
713 debug_info
.append(u
"{0} {1}".format(prefix
, str_out
))
715 jedi
.set_debug_function(_debug
, speed
=False)
717 script
= jedi
.Script(*args
, **kwargs
)
718 return getattr(script
, name
)()
719 except Exception as e
:
720 source
= kwargs
.get('source')
722 sc_args
.extend(repr(arg
) for arg
in args
)
723 sc_args
.extend("{0}={1}".format(k
, "source" if k
== "source"
725 for (k
, v
) in kwargs
.items())
728 "traceback": traceback
.format_exc(),
729 "jedi_debug_info": {'script_args': ", ".join(sc_args
),
732 'debug_info': debug_info
}
734 raise rpc
.Fault(message
=str(e
),
738 jedi
.set_debug_function(None)