"""Method implementations for the Elpy JSON-RPC server. This file implements the methods exported by the JSON-RPC server. It handles backend selection and passes methods on to the selected backend. """ import io import os import pydoc from elpy.pydocutils import get_pydoc_completions from elpy.rpc import JSONRPCServer, Fault from elpy.auto_pep8 import fix_code from elpy.yapfutil import fix_code as fix_code_with_yapf from elpy.blackutil import fix_code as fix_code_with_black try: from elpy import jedibackend except ImportError: # pragma: no cover jedibackend = None class ElpyRPCServer(JSONRPCServer): """The RPC server for elpy. See the rpc_* methods for exported method documentation. """ def __init__(self, *args, **kwargs): super(ElpyRPCServer, self).__init__(*args, **kwargs) self.backend = None self.project_root = None def _call_backend(self, method, default, *args, **kwargs): """Call the backend method with args. If there is currently no backend, return default.""" meth = getattr(self.backend, method, None) if meth is None: return default else: return meth(*args, **kwargs) def rpc_echo(self, *args): """Return the arguments. This is a simple test method to see if the protocol is working. """ return args def rpc_init(self, options): self.project_root = options["project_root"] if jedibackend: self.backend = jedibackend.JediBackend(self.project_root) else: self.backend = None return { 'jedi_available': (self.backend is not None) } def rpc_get_calltip(self, filename, source, offset): """Get the calltip for the function at the offset. """ return self._call_backend("rpc_get_calltip", None, filename, get_source(source), offset) def rpc_get_oneline_docstring(self, filename, source, offset): """Get a oneline docstring for the symbol at the offset. """ return self._call_backend("rpc_get_oneline_docstring", None, filename, get_source(source), offset) def rpc_get_completions(self, filename, source, offset): """Get a list of completion candidates for the symbol at offset. """ results = self._call_backend("rpc_get_completions", [], filename, get_source(source), offset) # Uniquify by name results = list(dict((res['name'], res) for res in results) .values()) results.sort(key=lambda cand: _pysymbol_key(cand["name"])) return results def rpc_get_completion_docstring(self, completion): """Return documentation for a previously returned completion. """ return self._call_backend("rpc_get_completion_docstring", None, completion) def rpc_get_completion_location(self, completion): """Return the location for a previously returned completion. This returns a list of [file name, line number]. """ return self._call_backend("rpc_get_completion_location", None, completion) def rpc_get_definition(self, filename, source, offset): """Get the location of the definition for the symbol at the offset. """ return self._call_backend("rpc_get_definition", None, filename, get_source(source), offset) def rpc_get_assignment(self, filename, source, offset): """Get the location of the assignment for the symbol at the offset. """ return self._call_backend("rpc_get_assignment", None, filename, get_source(source), offset) def rpc_get_docstring(self, filename, source, offset): """Get the docstring for the symbol at the offset. """ return self._call_backend("rpc_get_docstring", None, filename, get_source(source), offset) def rpc_get_pydoc_completions(self, name=None): """Return a list of possible strings to pass to pydoc. If name is given, the strings are under name. If not, top level modules are returned. """ return get_pydoc_completions(name) def rpc_get_pydoc_documentation(self, symbol): """Get the Pydoc documentation for the given symbol. Uses pydoc and can return a string with backspace characters for bold highlighting. """ try: docstring = pydoc.render_doc(str(symbol), "Elpy Pydoc Documentation for %s", False) except (ImportError, pydoc.ErrorDuringImport): return None else: if isinstance(docstring, bytes): docstring = docstring.decode("utf-8", "replace") return docstring def rpc_get_refactor_options(self, filename, start, end=None): """Return a list of possible refactoring options. This list will be filtered depending on whether it's applicable at the point START and possibly the region between START and END. """ try: from elpy import refactor except: raise ImportError("Rope not installed, refactorings unavailable") ref = refactor.Refactor(self.project_root, filename) return ref.get_refactor_options(start, end) def rpc_refactor(self, filename, method, args): """Return a list of changes from the refactoring action. A change is a dictionary describing the change. See elpy.refactor.translate_changes for a description. """ try: from elpy import refactor except: raise ImportError("Rope not installed, refactorings unavailable") if args is None: args = () ref = refactor.Refactor(self.project_root, filename) return ref.get_changes(method, *args) def rpc_get_usages(self, filename, source, offset): """Get usages for the symbol at point. """ source = get_source(source) if hasattr(self.backend, "rpc_get_usages"): return self.backend.rpc_get_usages(filename, source, offset) else: raise Fault("get_usages not implemented by current backend", code=400) def rpc_get_names(self, filename, source, offset): """Get all possible names """ source = get_source(source) if hasattr(self.backend, "rpc_get_names"): return self.backend.rpc_get_names(filename, source, offset) else: raise Fault("get_names not implemented by current backend", code=400) def rpc_fix_code(self, source, directory): """Formats Python code to conform to the PEP 8 style guide. """ source = get_source(source) return fix_code(source, directory) def rpc_fix_code_with_yapf(self, source, directory): """Formats Python code to conform to the PEP 8 style guide. """ source = get_source(source) return fix_code_with_yapf(source, directory) def rpc_fix_code_with_black(self, source, directory): """Formats Python code to conform to the PEP 8 style guide. """ source = get_source(source) return fix_code_with_black(source, directory) def get_source(fileobj): """Translate fileobj into file contents. fileobj is either a string or a dict. If it's a string, that's the file contents. If it's a string, then the filename key contains the name of the file whose contents we are to use. If the dict contains a true value for the key delete_after_use, the file should be deleted once read. """ if not isinstance(fileobj, dict): return fileobj else: try: with io.open(fileobj["filename"], encoding="utf-8", errors="ignore") as f: return f.read() finally: if fileobj.get('delete_after_use'): try: os.remove(fileobj["filename"]) except: # pragma: no cover pass def _pysymbol_key(name): """Return a sortable key index for name. Sorting is case-insensitive, with the first underscore counting as worse than any character, but subsequent underscores do not. This means that dunder symbols (like __init__) are sorted after symbols that start with an alphabetic character, but before those that start with only a single underscore. """ if name.startswith("_"): name = "~" + name[1:] return name.lower()