]>
Commit | Line | Data |
---|---|---|
1 | """Method implementations for the Elpy JSON-RPC server. | |
2 | ||
3 | This file implements the methods exported by the JSON-RPC server. It | |
4 | handles backend selection and passes methods on to the selected | |
5 | backend. | |
6 | ||
7 | """ | |
8 | import io | |
9 | import os | |
10 | import pydoc | |
11 | ||
12 | from elpy.pydocutils import get_pydoc_completions | |
13 | from elpy.rpc import JSONRPCServer, Fault | |
14 | from elpy.auto_pep8 import fix_code | |
15 | from elpy.yapfutil import fix_code as fix_code_with_yapf | |
16 | from elpy.blackutil import fix_code as fix_code_with_black | |
17 | ||
18 | ||
19 | try: | |
20 | from elpy import jedibackend | |
21 | except ImportError: # pragma: no cover | |
22 | jedibackend = None | |
23 | ||
24 | ||
25 | class ElpyRPCServer(JSONRPCServer): | |
26 | """The RPC server for elpy. | |
27 | ||
28 | See the rpc_* methods for exported method documentation. | |
29 | ||
30 | """ | |
31 | def __init__(self, *args, **kwargs): | |
32 | super(ElpyRPCServer, self).__init__(*args, **kwargs) | |
33 | self.backend = None | |
34 | self.project_root = None | |
35 | ||
36 | def _call_backend(self, method, default, *args, **kwargs): | |
37 | """Call the backend method with args. | |
38 | ||
39 | If there is currently no backend, return default.""" | |
40 | meth = getattr(self.backend, method, None) | |
41 | if meth is None: | |
42 | return default | |
43 | else: | |
44 | return meth(*args, **kwargs) | |
45 | ||
46 | def rpc_echo(self, *args): | |
47 | """Return the arguments. | |
48 | ||
49 | This is a simple test method to see if the protocol is | |
50 | working. | |
51 | ||
52 | """ | |
53 | return args | |
54 | ||
55 | def rpc_init(self, options): | |
56 | self.project_root = options["project_root"] | |
57 | self.env = options["environment"] | |
58 | ||
59 | if jedibackend: | |
60 | self.backend = jedibackend.JediBackend(self.project_root, self.env) | |
61 | else: | |
62 | self.backend = None | |
63 | ||
64 | return { | |
65 | 'jedi_available': (self.backend is not None) | |
66 | } | |
67 | ||
68 | def rpc_get_calltip(self, filename, source, offset): | |
69 | """Get the calltip for the function at the offset. | |
70 | ||
71 | """ | |
72 | return self._call_backend("rpc_get_calltip", None, filename, | |
73 | get_source(source), offset) | |
74 | ||
75 | def rpc_get_oneline_docstring(self, filename, source, offset): | |
76 | """Get a oneline docstring for the symbol at the offset. | |
77 | ||
78 | """ | |
79 | return self._call_backend("rpc_get_oneline_docstring", None, filename, | |
80 | get_source(source), offset) | |
81 | ||
82 | def rpc_get_calltip_or_oneline_docstring(self, filename, source, offset): | |
83 | """Get a calltip or a oneline docstring for the symbol at the offset. | |
84 | ||
85 | """ | |
86 | return self._call_backend("rpc_get_calltip_or_oneline_docstring", | |
87 | None, filename, | |
88 | get_source(source), offset) | |
89 | ||
90 | def rpc_get_completions(self, filename, source, offset): | |
91 | """Get a list of completion candidates for the symbol at offset. | |
92 | ||
93 | """ | |
94 | results = self._call_backend("rpc_get_completions", [], filename, | |
95 | get_source(source), offset) | |
96 | # Uniquify by name | |
97 | results = list(dict((res['name'], res) for res in results) | |
98 | .values()) | |
99 | results.sort(key=lambda cand: _pysymbol_key(cand["name"])) | |
100 | return results | |
101 | ||
102 | def rpc_get_completion_docstring(self, completion): | |
103 | """Return documentation for a previously returned completion. | |
104 | ||
105 | """ | |
106 | return self._call_backend("rpc_get_completion_docstring", | |
107 | None, completion) | |
108 | ||
109 | def rpc_get_completion_location(self, completion): | |
110 | """Return the location for a previously returned completion. | |
111 | ||
112 | This returns a list of [file name, line number]. | |
113 | ||
114 | """ | |
115 | return self._call_backend("rpc_get_completion_location", None, | |
116 | completion) | |
117 | ||
118 | def rpc_get_definition(self, filename, source, offset): | |
119 | """Get the location of the definition for the symbol at the offset. | |
120 | ||
121 | """ | |
122 | return self._call_backend("rpc_get_definition", None, filename, | |
123 | get_source(source), offset) | |
124 | ||
125 | def rpc_get_assignment(self, filename, source, offset): | |
126 | """Get the location of the assignment for the symbol at the offset. | |
127 | ||
128 | """ | |
129 | return self._call_backend("rpc_get_assignment", None, filename, | |
130 | get_source(source), offset) | |
131 | ||
132 | def rpc_get_docstring(self, filename, source, offset): | |
133 | """Get the docstring for the symbol at the offset. | |
134 | ||
135 | """ | |
136 | return self._call_backend("rpc_get_docstring", None, filename, | |
137 | get_source(source), offset) | |
138 | ||
139 | def rpc_get_pydoc_completions(self, name=None): | |
140 | """Return a list of possible strings to pass to pydoc. | |
141 | ||
142 | If name is given, the strings are under name. If not, top | |
143 | level modules are returned. | |
144 | ||
145 | """ | |
146 | return get_pydoc_completions(name) | |
147 | ||
148 | def rpc_get_pydoc_documentation(self, symbol): | |
149 | """Get the Pydoc documentation for the given symbol. | |
150 | ||
151 | Uses pydoc and can return a string with backspace characters | |
152 | for bold highlighting. | |
153 | ||
154 | """ | |
155 | try: | |
156 | docstring = pydoc.render_doc(str(symbol), | |
157 | "Elpy Pydoc Documentation for %s", | |
158 | False) | |
159 | except (ImportError, pydoc.ErrorDuringImport): | |
160 | return None | |
161 | else: | |
162 | if isinstance(docstring, bytes): | |
163 | docstring = docstring.decode("utf-8", "replace") | |
164 | return docstring | |
165 | ||
166 | def rpc_get_usages(self, filename, source, offset): | |
167 | """Get usages for the symbol at point. | |
168 | ||
169 | """ | |
170 | source = get_source(source) | |
171 | ||
172 | return self._call_backend("rpc_get_usages", | |
173 | None, filename, source, offset) | |
174 | ||
175 | def rpc_get_names(self, filename, source, offset): | |
176 | """Get all possible names | |
177 | ||
178 | """ | |
179 | source = get_source(source) | |
180 | return self._call_backend("rpc_get_names", | |
181 | None, filename, source, offset) | |
182 | ||
183 | def rpc_get_rename_diff(self, filename, source, offset, new_name): | |
184 | """Get the diff resulting from renaming the thing at point | |
185 | ||
186 | """ | |
187 | source = get_source(source) | |
188 | ||
189 | return self._call_backend("rpc_get_rename_diff", | |
190 | None, filename, source, offset, new_name) | |
191 | ||
192 | def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, | |
193 | line_beg, line_end, col_beg, col_end): | |
194 | """Get the diff resulting from extracting the selected code | |
195 | ||
196 | """ | |
197 | source = get_source(source) | |
198 | return self._call_backend("rpc_get_extract_variable_diff", | |
199 | None, filename, source, offset, | |
200 | new_name, line_beg, line_end, col_beg, | |
201 | col_end) | |
202 | ||
203 | def rpc_get_extract_function_diff(self, filename, source, offset, new_name, | |
204 | line_beg, line_end, col_beg, col_end): | |
205 | """Get the diff resulting from extracting the selected code | |
206 | ||
207 | """ | |
208 | source = get_source(source) | |
209 | return self._call_backend("rpc_get_extract_function_diff", | |
210 | None, filename, source, offset, new_name, | |
211 | line_beg, line_end, col_beg, col_end) | |
212 | ||
213 | def rpc_get_inline_diff(self, filename, source, offset): | |
214 | """Get the diff resulting from inlining the thing at point. | |
215 | ||
216 | """ | |
217 | source = get_source(source) | |
218 | return self._call_backend("rpc_get_inline_diff", | |
219 | None, filename, source, offset) | |
220 | ||
221 | def rpc_fix_code(self, source, directory): | |
222 | """Formats Python code to conform to the PEP 8 style guide. | |
223 | ||
224 | """ | |
225 | source = get_source(source) | |
226 | return fix_code(source, directory) | |
227 | ||
228 | def rpc_fix_code_with_yapf(self, source, directory): | |
229 | """Formats Python code to conform to the PEP 8 style guide. | |
230 | ||
231 | """ | |
232 | source = get_source(source) | |
233 | return fix_code_with_yapf(source, directory) | |
234 | ||
235 | def rpc_fix_code_with_black(self, source, directory): | |
236 | """Formats Python code to conform to the PEP 8 style guide. | |
237 | ||
238 | """ | |
239 | source = get_source(source) | |
240 | return fix_code_with_black(source, directory) | |
241 | ||
242 | ||
243 | def get_source(fileobj): | |
244 | """Translate fileobj into file contents. | |
245 | ||
246 | fileobj is either a string or a dict. If it's a string, that's the | |
247 | file contents. If it's a string, then the filename key contains | |
248 | the name of the file whose contents we are to use. | |
249 | ||
250 | If the dict contains a true value for the key delete_after_use, | |
251 | the file should be deleted once read. | |
252 | ||
253 | """ | |
254 | if not isinstance(fileobj, dict): | |
255 | return fileobj | |
256 | else: | |
257 | try: | |
258 | with io.open(fileobj["filename"], encoding="utf-8", | |
259 | errors="ignore") as f: | |
260 | return f.read() | |
261 | finally: | |
262 | if fileobj.get('delete_after_use'): | |
263 | try: | |
264 | os.remove(fileobj["filename"]) | |
265 | except: # pragma: no cover | |
266 | pass | |
267 | ||
268 | ||
269 | def _pysymbol_key(name): | |
270 | """Return a sortable key index for name. | |
271 | ||
272 | Sorting is case-insensitive, with the first underscore counting as | |
273 | worse than any character, but subsequent underscores do not. This | |
274 | means that dunder symbols (like __init__) are sorted after symbols | |
275 | that start with an alphabetic character, but before those that | |
276 | start with only a single underscore. | |
277 | ||
278 | """ | |
279 | if name.startswith("_"): | |
280 | name = "~" + name[1:] | |
281 | return name.lower() |