From 466c8b084a54e792e2ef040b331ae0ec7dbec72a Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 7 Jan 2021 11:59:19 +0700 Subject: [PATCH 01/24] Removed jedi16 compatibility. Starting create adecvate error messages if error occured in rpc-server. --- elpy-refactor.el | 6 +- elpy/jedibackend.py | 336 +++++--------------------------------------- 2 files changed, 40 insertions(+), 302 deletions(-) diff --git a/elpy-refactor.el b/elpy-refactor.el index 19193b159..c64b7c2cc 100644 --- a/elpy-refactor.el +++ b/elpy-refactor.el @@ -202,11 +202,11 @@ do not display the diff before applying." (diff (elpy-rpc-get-rename-diff new-name)) (proj-path (alist-get 'project_path diff)) (success (alist-get 'success diff)) + (error-msg (alist-get 'error_msg diff)) + (error-type (alist-get 'error_type diff)) (diff (alist-get 'diff diff))) (cond ((not success) - (error "Refactoring failed for some reason")) - ((string= success "Not available") - (error "This functionnality needs jedi > 0.17.0, please update")) + (error "Elpy-RPC Error (%s): %s" error-type error-msg)) ((or dontask current-prefix-arg) (message "Replacing '%s' with '%s'..." (thing-at-point 'symbol) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index b7d7a0551..6cb6739f1 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -10,6 +10,7 @@ import traceback import re +from typing import Type import jedi from elpy import rpc @@ -24,8 +25,6 @@ def parse_version(*arg, **kwargs): raise Fault("`pkg_resources` could not be imported, " "please reinstall Elpy RPC virtualenv with" " `M-x elpy-rpc-reinstall-virtualenv`", code=400) -JEDISUP17 = parse_version(jedi.__version__) >= parse_version("0.17.0") - class JediBackend(object): """The Jedi backend class. @@ -45,16 +44,12 @@ def __init__(self, project_root, environment_binaries_path): safe=False) self.completions = {} sys.path.append(project_root) - # Backward compatibility to jedi<17 - if not JEDISUP17: # pragma: no cover - self.rpc_get_completions = self.rpc_get_completions_jedi16 - self.rpc_get_docstring = self.rpc_get_docstring_jedi16 - self.rpc_get_definition = self.rpc_get_definition_jedi16 - self.rpc_get_assignment = self.rpc_get_assignment_jedi16 - self.rpc_get_calltip = self.rpc_get_calltip_jedi16 - self.rpc_get_oneline_docstring = self.rpc_get_oneline_docstring_jedi16 - self.rpc_get_usages = self.rpc_get_usages_jedi16 - self.rpc_get_names = self.rpc_get_names_jedi16 + + def __make_error_msg(self, e: Type[Exception]) -> dict: + return {'success': False, + 'error_type': type(e).__name__, + 'error_msg': str(e)} + def rpc_get_completions(self, filename, source, offset): line, column = pos_to_linecol(source, offset) @@ -70,23 +65,6 @@ def rpc_get_completions(self, filename, source, offset): 'meta': proposal.description} for proposal in proposals] - def rpc_get_completions_jedi16(self, filename, source, offset): - # Backward compatibility to jedi<17 - line, column = pos_to_linecol(source, offset) - proposals = run_with_debug(jedi, 'completions', - source=source, line=line, column=column, - path=filename, encoding='utf-8', - environment=self.environment) - 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: @@ -123,25 +101,6 @@ def rpc_get_docstring(self, filename, source, offset): else: return None - def rpc_get_docstring_jedi16(self, filename, source, offset): - # Backward compatibility to jedi<17 - 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', - environment=self.environment) - if not locations: - return None - # Filter uninteresting things - if locations[-1].name in ["str", "int", "float", "bool", "tuple", - "list", "dict"]: - return None - if 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', @@ -178,80 +137,9 @@ def rpc_get_definition(self, filename, source, offset): return None return (loc.module_path, offset) - def rpc_get_definition_jedi16(self, filename, source, offset): # pragma: no cover - # Backward compatibility to jedi<17 - 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', - environment=self.environment) - # 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', - environment=self.environment) - 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): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.") - def rpc_get_assignment_jedi16(self, filename, source, offset): # pragma: no cover - # Backward compatibility to jedi<17 - 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', - environment=self.environment) - - 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) @@ -269,27 +157,6 @@ def rpc_get_calltip(self, filename, source, offset): "index": calls[0].index, "params": params} - def rpc_get_calltip_jedi16(self, filename, source, offset): # pragma: no cover - # Backward compatibility to jedi<17 - 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', - environment=self.environment) - if calls: - call = calls[0] - else: - call = None - if not call: - return None - # Strip 'param' added by jedi at the beginning 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_calltip_or_oneline_docstring(self, filename, source, offset): """ Return the current function calltip or its oneline docstring. @@ -377,77 +244,6 @@ def rpc_get_oneline_docstring(self, filename, source, offset): return {"name": name, "doc": onelinedoc} - def rpc_get_oneline_docstring_jedi16(self, filename, source, offset): # pragma: no cover - """Return a oneline docstring for the symbol at offset""" - # Backward compatibility to jedi<17 - 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', - environment=self.environment) - # avoid unintersting stuff - try: - if definitions[0].name in ["str", "int", "float", "bool", "tuple", - "list", "dict"]: - return None - except: - pass - assignments = run_with_debug(jedi, 'goto_assignments', - source=source, line=line, column=column, - path=filename, encoding='utf-8', - environment=self.environment) - - 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. @@ -477,36 +273,6 @@ def rpc_get_usages(self, filename, source, offset): "offset": offset}) return result - def rpc_get_usages_jedi16(self, filename, source, offset): # pragma: no cover - """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. - - """ - # Backward compatibility to jedi<17 - line, column = pos_to_linecol(source, offset) - uses = run_with_debug(jedi, 'usages', - source=source, line=line, column=column, - path=filename, encoding='utf-8', - environment=self.environment) - 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 = run_with_debug(jedi, 'get_names', @@ -528,47 +294,23 @@ def rpc_get_names(self, filename, source, offset): "filename": name.module_path, "offset": offset}) return result - - def rpc_get_names_jedi16(self, filename, source, offset): # pragma: no cover - """Return the list of possible names""" - # Backward compatibility to jedi<17 - 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 - + def rpc_get_rename_diff(self, filename, source, offset, new_name): """Get the diff resulting from renaming the thing at point""" - if not hasattr(jedi.Script, "rename"): # pragma: no cover - return {'success': "Not available"} line, column = pos_to_linecol(source, offset) - ren = run_with_debug(jedi, 'rename', code=source, - path=filename, - environment=self.environment, - fun_kwargs={'line': line, - 'column': column, - 'new_name': new_name}) - if ren is None: - return {'success': False} - else: - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + script = jedi.Script(code=source, path=filename, + environment=self.environment) + try: + ren = script.extract_function(line=line, + column=column, + new_name=new_name) + except Exception as e: + return self.__make_error_msg(e) + + return {'success': True, + 'project_path': ren._inference_state.project._path, + 'diff': ren.get_diff(), + 'changed_files': list(ren.get_changed_files().keys())} def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): @@ -594,23 +336,20 @@ def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, def rpc_get_extract_function_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): """Get the diff resulting from extracting the selected code""" - if not hasattr(jedi.Script, "extract_function"): # pragma: no cover - return {'success': "Not available"} - ren = run_with_debug(jedi, 'extract_function', code=source, - path=filename, - environment=self.environment, - fun_kwargs={'line': line_beg, - 'until_line': line_end, - 'column': col_beg, - 'until_column': col_end, - 'new_name': new_name}) - if ren is None: - return {'success': False} - else: - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + script = jedi.Script(code=source, path=filename, + environment=self.environment) + try: + ren = script.extract_function(line=line_beg, + until_line=line_end, + column=col_beg, + until_column=col_end, + new_name=new_name) + except Exception as e: + return self.__make_error_msg(e) + return {'success': True, + 'project_path': ren._inference_state.project._path, + 'diff': ren.get_diff(), + 'changed_files': list(ren.get_changed_files().keys())} def rpc_get_inline_diff(self, filename, source, offset): """Get the diff resulting from inlining the selected variable""" @@ -682,9 +421,8 @@ def run_with_debug(jedi, name, fun_kwargs={}, *args, **kwargs): except Exception as e: if isinstance(e, re_raise): raise - if JEDISUP17: - if isinstance(e, jedi.RefactoringError): - return None + if isinstance(e, jedi.RefactoringError): + return None # Bug jedi#485 if ( isinstance(e, ValueError) and From bd1c32cc9be56361f35551dcad26dfe345378117 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 7 Jan 2021 14:48:29 +0700 Subject: [PATCH 02/24] refactoring --- elpy/jedibackend.py | 83 ++++++++++++++++++++++--------------------- elpy/rpc.py | 3 ++ elpy/tests/support.py | 38 +------------------- requirements.txt | 1 + scripts/test | 2 +- setup.py | 2 +- 6 files changed, 49 insertions(+), 80 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 6cb6739f1..4b8b42ba0 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -13,11 +13,13 @@ from typing import Type import jedi + from elpy import rpc from elpy.rpc import Fault # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). + try: from pkg_resources import parse_version except ImportError: # pragma: no cover @@ -37,6 +39,7 @@ class JediBackend(object): name = "jedi" def __init__(self, project_root, environment_binaries_path): + self.project_root = project_root self.environment = None if environment_binaries_path is not None: @@ -50,7 +53,21 @@ def __make_error_msg(self, e: Type[Exception]) -> dict: 'error_type': type(e).__name__, 'error_msg': str(e)} - + def __make_refactoring_msg(self, x: jedi.api.refactoring.Refactoring): + if type(x) is not jedi.api.refactoring.Refactoring: + raise TypeError("Must be jedi.api.refactoring.Refactoring type") + return {'success': True, + 'project_path': str(x._inference_state.project._path), + 'diff': x.get_diff(), + 'changed_files': list(map(str, x.get_changed_files().keys()))} + + def __make_name_msg(self, x: jedi.api.classes.Name, offset: int): + if type(x) is not jedi.api.classes.Name: + raise TypeError("Must be jedi.api.classes.Name") + return {"name": x.name, + "filename": str(x.module_path), + "offset": offset} + def rpc_get_completions(self, filename, source, offset): line, column = pos_to_linecol(source, offset) proposals = run_with_debug(jedi, 'complete', code=source, @@ -111,6 +128,7 @@ def rpc_get_definition(self, filename, source, offset): 'column': column, 'follow_imports': True, 'follow_builtin_imports': True}) + if not locations: return None # goto_definitions() can return silly stuff like __builtin__ @@ -135,7 +153,7 @@ def rpc_get_definition(self, filename, source, offset): loc.column) except IOError: # pragma: no cover return None - return (loc.module_path, offset) + return (str(loc.module_path), offset) def rpc_get_assignment(self, filename, source, offset): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.") @@ -252,25 +270,23 @@ def rpc_get_usages(self, filename, source, offset): """ line, column = pos_to_linecol(source, offset) - uses = run_with_debug(jedi, 'get_references', + names = run_with_debug(jedi, 'get_references', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) - if uses is None: + if names 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: + 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, use.line, use.column) - result.append({"name": use.name, - "filename": use.module_path, - "offset": offset}) + offset = linecol_to_pos(text, name.line, name.column) + result.append(self.__make_name_msg(name, offset)) return result def rpc_get_names(self, filename, source, offset): @@ -290,9 +306,7 @@ def rpc_get_names(self, filename, source, offset): 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}) + result.append(self.__make_name_msg(name, offset)) return result def rpc_get_rename_diff(self, filename, source, offset, new_name): @@ -301,23 +315,19 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): script = jedi.Script(code=source, path=filename, environment=self.environment) try: - ren = script.extract_function(line=line, - column=column, - new_name=new_name) + ref = script.rename(line=line, + column=column, + new_name=new_name) except Exception as e: return self.__make_error_msg(e) - - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + return self.__make_refactoring_msg(ref) def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): """Get the diff resulting from extracting the selected code""" if not hasattr(jedi.Script, "extract_variable"): # pragma: no cover return {'success': "Not available"} - ren = run_with_debug(jedi, 'extract_variable', code=source, + ref = run_with_debug(jedi, 'extract_variable', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line_beg, @@ -325,13 +335,10 @@ def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, 'column': col_beg, 'until_column': col_end, 'new_name': new_name}) - if ren is None: + if ref is None: return {'success': False} else: - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + return self.__make_error_msg(ref) def rpc_get_extract_function_diff(self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end): @@ -339,35 +346,29 @@ def rpc_get_extract_function_diff(self, filename, source, offset, new_name, script = jedi.Script(code=source, path=filename, environment=self.environment) try: - ren = script.extract_function(line=line_beg, + ref = script.extract_function(line=line_beg, until_line=line_end, column=col_beg, until_column=col_end, new_name=new_name) except Exception as e: return self.__make_error_msg(e) - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + return self.__make_refactoring_msg(ref) def rpc_get_inline_diff(self, filename, source, offset): """Get the diff resulting from inlining the selected variable""" if not hasattr(jedi.Script, "inline"): # pragma: no cover return {'success': "Not available"} line, column = pos_to_linecol(source, offset) - ren = run_with_debug(jedi, 'inline', code=source, + ref = run_with_debug(jedi, 'inline', code=source, path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) - if ren is None: + if ref is None: return {'success': False} else: - return {'success': True, - 'project_path': ren._inference_state.project._path, - 'diff': ren.get_diff(), - 'changed_files': list(ren.get_changed_files().keys())} + return self.__make_refactoring_msg(ref) # From the Jedi documentation: diff --git a/elpy/rpc.py b/elpy/rpc.py index 07b2ce97e..4bd3d6d80 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -10,7 +10,10 @@ import json import sys import traceback +from pydantic import BaseModel +class Msg(BaseModel): + pass class JSONRPCServer(object): """Simple JSON-RPC-like server. diff --git a/elpy/tests/support.py b/elpy/tests/support.py index 1e79654f3..05ce9f57a 100644 --- a/elpy/tests/support.py +++ b/elpy/tests/support.py @@ -629,35 +629,7 @@ def test_should_handle_jedi16(self): self.backend.rpc_get_definition = self.backend.rpc_get_definition_jedi16 self.test_should_return_definition_location_same_file() self.backend.rpc_get_definition = backup - - -class RPCGetAssignmentTests(): - METHOD = "rpc_get_assignment" - - def test_should_raise_fault(self): - if jedibackend.JEDISUP17: - with self.assertRaises(Fault): - self.backend.rpc_get_assignment("test.py", "dummy code", 1) - - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_assignment - self.backend.rpc_get_assignment = self.backend.rpc_get_assignment_jedi16 - source, offset = source_and_offset("import threading\n" - "def test_function(a, b):\n" - " return a + b\n" - "\n" - "test_func_|_tion(\n") - filename = self.project_file("test.py", source) - - location = self.backend.rpc_get_assignment(filename, - source, - offset) - - self.assertEqual(location[0], filename) - # On def or on the function name - self.assertIn(location[1], (17, 21)) - self.backend.rpc_get_assignment = backup - + class RPCGetCalltipTests(GenericRPCTests): METHOD = "rpc_get_calltip" @@ -910,8 +882,6 @@ def test_should_handle_jedi16(self): self.backend.rpc_get_oneline_docstring = backup -@unittest.skipIf(not jedibackend.JEDISUP17, - "Refactoring not available with jedi<17") @unittest.skipIf(sys.version_info < (3, 6), "Jedi refactoring not available for python < 3.6") class RPCGetRenameDiffTests(object): @@ -941,8 +911,6 @@ def test_should_fail_for_invalid_symbol_at_point(self): self.assertFalse(diff['success']) -@unittest.skipIf(not jedibackend.JEDISUP17, - "Refactoring not available with jedi<17") @unittest.skipIf(sys.version_info < (3, 6), "Jedi refactoring not available for python < 3.6") class RPCGetExtractFunctionDiffTests(object): @@ -966,8 +934,6 @@ def test_should_return_function_extraction_diff(self): diff['diff']) -@unittest.skipIf(not jedibackend.JEDISUP17, - "Refactoring not available with jedi<17") @unittest.skipIf(sys.version_info < (3, 6), "Jedi refactoring not available for python < 3.6") class RPCGetExtractVariableDiffTests(object): @@ -988,8 +954,6 @@ def test_should_return_variable_extraction_diff(self): diff['diff']) -@unittest.skipIf(not jedibackend.JEDISUP17, - "Refactoring not available with jedi<17") @unittest.skipIf(sys.version_info < (3, 6), "Jedi refactoring not available for python < 3.6") class RPCGetInlineDiffTests(object): diff --git a/requirements.txt b/requirements.txt index d0acd85e8..8a7c0f5b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ flake8==3.8.3 +pydantic diff --git a/scripts/test b/scripts/test index 104f24e69..ee5c856e9 100755 --- a/scripts/test +++ b/scripts/test @@ -10,7 +10,7 @@ fi if [ -z "$1" -o "$1" = "python" ] then - python -tt -m unittest discover elpy + python3 -tt -m unittest discover elpy fi if [ "$1" = "coverage" ] diff --git a/setup.py b/setup.py index 47972c114..620a0264a 100755 --- a/setup.py +++ b/setup.py @@ -29,6 +29,6 @@ ], packages=find_packages(), include_package_data=True, - install_requires=["flake8>=2.0"], + install_requires=["flake8>=2.0", "pydantic"], test_suite="elpy" ) From 99b87b2ba5a65e8b0d153665a1ef5c130db89071 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 7 Jan 2021 22:45:21 +0700 Subject: [PATCH 03/24] integrate pydantic --- elpy/jedibackend.py | 41 +++++++++++++++++++++++++++----------- elpy/rpc.py | 36 ++++++++++++++++++++++++++------- elpy/tests/test_rpc.py | 45 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 22 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 4b8b42ba0..84290d259 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -10,12 +10,29 @@ import traceback import re -from typing import Type +from typing import Type, List +from pathlib import Path import jedi from elpy import rpc -from elpy.rpc import Fault +from elpy.rpc import Fault, Msg + +class NameMsg(Msg): + name: str + offset: int + filename: Path + +class RefactoringMsg(Msg): + success: bool + project_path: Path + diff: str + changed_files: List[Path] + +class ErrorMsg(Msg): + success: bool + error_type: str + error_msg: str # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). @@ -49,24 +66,23 @@ def __init__(self, project_root, environment_binaries_path): sys.path.append(project_root) def __make_error_msg(self, e: Type[Exception]) -> dict: - return {'success': False, - 'error_type': type(e).__name__, - 'error_msg': str(e)} + return ErrorMsg(success=False, + error_type=type(e).__name__, + error_msg=str(e)) def __make_refactoring_msg(self, x: jedi.api.refactoring.Refactoring): if type(x) is not jedi.api.refactoring.Refactoring: raise TypeError("Must be jedi.api.refactoring.Refactoring type") - return {'success': True, - 'project_path': str(x._inference_state.project._path), - 'diff': x.get_diff(), - 'changed_files': list(map(str, x.get_changed_files().keys()))} + return RefactoringMsg( + success=True, + project_path=x._inference_state.project._path, + diff=x.get_diff(), + changed_files=list(x.get_changed_files().keys())) def __make_name_msg(self, x: jedi.api.classes.Name, offset: int): if type(x) is not jedi.api.classes.Name: raise TypeError("Must be jedi.api.classes.Name") - return {"name": x.name, - "filename": str(x.module_path), - "offset": offset} + return NameMsg(name=x.name, filename=x.module_path, offset=offset) def rpc_get_completions(self, filename, source, offset): line, column = pos_to_linecol(source, offset) @@ -155,6 +171,7 @@ def rpc_get_definition(self, filename, source, offset): return None return (str(loc.module_path), offset) + def rpc_get_assignment(self, filename, source, offset): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.") diff --git a/elpy/rpc.py b/elpy/rpc.py index 4bd3d6d80..dae544918 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -11,10 +11,23 @@ import sys import traceback from pydantic import BaseModel +from typing import Union, List, Type + class Msg(BaseModel): pass + +class ErrorMsg(BaseModel): + id: int + error: Union[dict, Msg] + + +class ResultMsg(BaseModel): + id: int + result: Union[dict, List, str, Type[Msg]] + + class JSONRPCServer(object): """Simple JSON-RPC-like server. @@ -70,14 +83,14 @@ def read_json(self): raise EOFError() return json.loads(line) - def write_json(self, **kwargs): + def write_json(self, x) -> None: """Write an JSON object on a single line. The keyword arguments are interpreted as a single JSON object. It's not possible with this method to write non-objects. """ - self.stdout.write(json.dumps(kwargs) + "\n") + self.stdout.write(x.json() + '\n') self.stdout.flush() def handle_request(self): @@ -104,19 +117,28 @@ def handle_request(self): else: result = self.handle(method_name, params) if request_id is not None: - self.write_json(result=result, - id=request_id) + self.write_json( + ResultMsg( + result=result, + id=request_id)) except Fault as fault: error = {"message": fault.message, "code": fault.code} if fault.data is not None: error["data"] = fault.data - self.write_json(error=error, id=request_id) + self.write_json( + ErrorMsg( + error=error, + id=request_id)) except Exception as e: error = {"message": str(e), "code": 500, - "data": {"traceback": traceback.format_exc()}} - self.write_json(error=error, id=request_id) + "data": {"traceback": traceback.format_exc()} + } + self.write_json( + ErrorMsg( + error=error, + id=request_id)) def handle(self, method_name, args): """Handle the call to method_name. diff --git a/elpy/tests/test_rpc.py b/elpy/tests/test_rpc.py index 0d35196c1..97ea9075d 100644 --- a/elpy/tests/test_rpc.py +++ b/elpy/tests/test_rpc.py @@ -5,6 +5,7 @@ import sys from elpy import rpc +from elpy.rpc import Msg, ErrorMsg, ResultMsg from elpy.tests.compat import StringIO @@ -83,9 +84,47 @@ def test_should_write_json_line(self): ] for obj in objlist: self.rpc.write_json(**obj) - self.assertEqual(json.loads(self.read()), - obj) - + self.assertEqual(json.loads(self.read()), obj) + + def test_result_msg_shuld_work_with_msg(self): + class DataMsg(Msg): + data: str + self.rpc.write_json( + ResultMsg(id=100, result=DataMsg(data="some data"))) + self.assertEqual( + self.read(), '{"id": 100, "result": {"data": "some data"}}\n') + + def test_result_msg__shuld_work_with_dict(self): + self.rpc.write_json( + ResultMsg(id=100, result={'data': 'some data'})) + self.assertEqual(self.read(), + '{"id": 100, "result": {"data": "some data"}}\n') + + def test_result_msg__shuld_work_with_list(self): + self.rpc.write_json( + ResultMsg(id=100, result=['some data1', 'some data2', 3])) + self.assertEqual( + self.read(), + '{"id": 100, "result": ["some data1", "some data2", 3]}\n') + + def test_result_msg__shuld_work_with_str(self): + self.rpc.write_json(ResultMsg(id=100, result="some string")) + self.assertEqual(self.read(), '{"id": 100, "result": "some string"}\n') + + def test_error_msg__shuld_work_with_dict(self): + self.rpc.write_json( + ErrorMsg(id=100, error={'error': 'error msg'})) + self.assertEqual( + self.read(), '{"id": 100, "error": {"error": "error msg"}}\n') + + def test_error_msg__shuld_work_with_msg(self): + class MyErrorMsg(Msg): + error: str + self.rpc.write_json( + ErrorMsg(id=100, error=MyErrorMsg(error="error msg"))) + self.assertEqual( + self.read(), + '{"id": 100, "error": {"error": "error msg"}}\n') class TestHandleRequest(TestJSONRPCServer): def test_should_fail_if_json_does_not_contain_a_method(self): From d6daf071c35810b09089125749755a5779b8fad8 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 7 Jan 2021 23:30:28 +0700 Subject: [PATCH 04/24] refactoring --- elpy/rpc.py | 43 ++++++++++++++++++------------------------ elpy/tests/test_rpc.py | 20 ++++++-------------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/elpy/rpc.py b/elpy/rpc.py index dae544918..0a79c4f8b 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -11,7 +11,7 @@ import sys import traceback from pydantic import BaseModel -from typing import Union, List, Type +from typing import Union, Optional, List, Type class Msg(BaseModel): @@ -72,7 +72,7 @@ def __init__(self, stdin=None, stdout=None): else: self.stdout = stdout - def read_json(self): + def read_json(self) -> str: """Read a single line and decode it as JSON. Can raise an EOFError() when the input source was closed. @@ -83,17 +83,13 @@ def read_json(self): raise EOFError() return json.loads(line) - def write_json(self, x) -> None: + def send_msg(self, msg: Msg) -> None: """Write an JSON object on a single line. - - The keyword arguments are interpreted as a single JSON object. - It's not possible with this method to write non-objects. - """ - self.stdout.write(x.json() + '\n') + self.stdout.write(msg.json() + '\n') self.stdout.flush() - - def handle_request(self): + + def handle_request(self) -> Type[Msg]: """Handle a single JSON-RPC request. Read a request, call the appropriate handler method, and @@ -110,6 +106,12 @@ def handle_request(self): method_name = request['method'] request_id = request.get('id', None) params = request.get('params') or [] + msg = self._make_msg(request_id, method_name, params) + if msg is not None: + self.send_msg(msg) + + def _make_msg(self, request_id: str, method_name: str, params: List + ) -> Optional[Type[Msg]]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -117,37 +119,28 @@ def handle_request(self): else: result = self.handle(method_name, params) if request_id is not None: - self.write_json( - ResultMsg( - result=result, - id=request_id)) + return ResultMsg(result=result, id=request_id) except Fault as fault: error = {"message": fault.message, "code": fault.code} if fault.data is not None: error["data"] = fault.data - self.write_json( - ErrorMsg( - error=error, - id=request_id)) + return ErrorMsg(error=error, id=request_id) except Exception as e: error = {"message": str(e), "code": 500, "data": {"traceback": traceback.format_exc()} } - self.write_json( - ErrorMsg( - error=error, - id=request_id)) - - def handle(self, method_name, args): + return ErrorMsg(error=error, id=request_id) + + def handle(self, method_name: str, args: List) -> None: """Handle the call to method_name. You should overwrite this method in a subclass. """ raise Fault("Unknown method {0}".format(method_name)) - def serve_forever(self): + def serve_forever(self) -> None: """Serve requests forever. Errors are not caught, so this is a slight misnomer. diff --git a/elpy/tests/test_rpc.py b/elpy/tests/test_rpc.py index 97ea9075d..04da332ed 100644 --- a/elpy/tests/test_rpc.py +++ b/elpy/tests/test_rpc.py @@ -78,41 +78,33 @@ def test_should_fail_on_malformed_json(self): class TestWriteJson(TestJSONRPCServer): - def test_should_write_json_line(self): - objlist = [{'foo': 'bar'}, - {'baz': 'qux', 'fnord': 'argl\nbargl'}, - ] - for obj in objlist: - self.rpc.write_json(**obj) - self.assertEqual(json.loads(self.read()), obj) - def test_result_msg_shuld_work_with_msg(self): class DataMsg(Msg): data: str - self.rpc.write_json( + self.rpc.send_msg( ResultMsg(id=100, result=DataMsg(data="some data"))) self.assertEqual( self.read(), '{"id": 100, "result": {"data": "some data"}}\n') def test_result_msg__shuld_work_with_dict(self): - self.rpc.write_json( + self.rpc.send_msg( ResultMsg(id=100, result={'data': 'some data'})) self.assertEqual(self.read(), '{"id": 100, "result": {"data": "some data"}}\n') def test_result_msg__shuld_work_with_list(self): - self.rpc.write_json( + self.rpc.send_msg( ResultMsg(id=100, result=['some data1', 'some data2', 3])) self.assertEqual( self.read(), '{"id": 100, "result": ["some data1", "some data2", 3]}\n') def test_result_msg__shuld_work_with_str(self): - self.rpc.write_json(ResultMsg(id=100, result="some string")) + self.rpc.send_msg(ResultMsg(id=100, result="some string")) self.assertEqual(self.read(), '{"id": 100, "result": "some string"}\n') def test_error_msg__shuld_work_with_dict(self): - self.rpc.write_json( + self.rpc.send_msg( ErrorMsg(id=100, error={'error': 'error msg'})) self.assertEqual( self.read(), '{"id": 100, "error": {"error": "error msg"}}\n') @@ -120,7 +112,7 @@ def test_error_msg__shuld_work_with_dict(self): def test_error_msg__shuld_work_with_msg(self): class MyErrorMsg(Msg): error: str - self.rpc.write_json( + self.rpc.send_msg( ErrorMsg(id=100, error=MyErrorMsg(error="error msg"))) self.assertEqual( self.read(), From c49d43a70a99f9970e82b9d33d9a6cabb2feb3f1 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 7 Jan 2021 23:34:26 +0700 Subject: [PATCH 05/24] refactoring --- elpy/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elpy/rpc.py b/elpy/rpc.py index 0a79c4f8b..b56fdaf63 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -20,7 +20,7 @@ class Msg(BaseModel): class ErrorMsg(BaseModel): id: int - error: Union[dict, Msg] + error: Union[dict, Type[Msg]] class ResultMsg(BaseModel): From 954c7551e336471b213c2659bb59f4765a76f27d Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Fri, 8 Jan 2021 00:57:15 +0700 Subject: [PATCH 06/24] refactoring --- elpy/jedibackend.py | 85 +++++++++++++++++++++++------------------- elpy/rpc.py | 20 +++++----- elpy/tests/test_rpc.py | 14 +++---- 3 files changed, 64 insertions(+), 55 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 84290d259..2deb197aa 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -5,6 +5,7 @@ https://github.com/davidhalter/jedi """ +from __future__ import annotations import sys import traceback @@ -16,24 +17,49 @@ from elpy import rpc -from elpy.rpc import Fault, Msg +from elpy.rpc import Fault, ResultMsg -class NameMsg(Msg): +class NameMsg(ResultMsg): name: str offset: int filename: Path + @classmethod + def from_name( + cls, x: jedi.api.classes.Name, offset: int) -> NameMsg: + if type(x) is not jedi.api.classes.Name: + raise TypeError("Must be jedi.api.classes.Name") + return cls(name=x.name, filename=x.module_path, offset=offset) -class RefactoringMsg(Msg): + +class RefactoringMsg(ResultMsg): success: bool project_path: Path diff: str changed_files: List[Path] -class ErrorMsg(Msg): + @classmethod + def from_refactoring( + cls, x: jedi.api.refactoring.Refactoring) -> RefactoringMsg: + if type(x) is not jedi.api.refactoring.Refactoring: + raise TypeError("Must be jedi.api.refactoring.Refactoring type") + return cls( + success=True, + project_path=x._inference_state.project._path, + diff=x.get_diff(), + changed_files=list(x.get_changed_files().keys())) + + +class ErrorMsg(ResultMsg): success: bool error_type: str error_msg: str + @classmethod + def from_error(cls, e): + return ErrorMsg(success=False, + error_type=type(e).__name__, + error_msg=str(e)) + # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). @@ -64,25 +90,6 @@ def __init__(self, project_root, environment_binaries_path): safe=False) self.completions = {} sys.path.append(project_root) - - def __make_error_msg(self, e: Type[Exception]) -> dict: - return ErrorMsg(success=False, - error_type=type(e).__name__, - error_msg=str(e)) - - def __make_refactoring_msg(self, x: jedi.api.refactoring.Refactoring): - if type(x) is not jedi.api.refactoring.Refactoring: - raise TypeError("Must be jedi.api.refactoring.Refactoring type") - return RefactoringMsg( - success=True, - project_path=x._inference_state.project._path, - diff=x.get_diff(), - changed_files=list(x.get_changed_files().keys())) - - def __make_name_msg(self, x: jedi.api.classes.Name, offset: int): - if type(x) is not jedi.api.classes.Name: - raise TypeError("Must be jedi.api.classes.Name") - return NameMsg(name=x.name, filename=x.module_path, offset=offset) def rpc_get_completions(self, filename, source, offset): line, column = pos_to_linecol(source, offset) @@ -279,7 +286,7 @@ def rpc_get_oneline_docstring(self, filename, source, offset): return {"name": name, "doc": onelinedoc} - def rpc_get_usages(self, filename, source, offset): + def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: """Return the uses of the symbol at offset. Returns a list of occurrences of the symbol, as dicts with the @@ -303,10 +310,10 @@ def rpc_get_usages(self, filename, source, offset): with open(name.module_path) as f: text = f.read() offset = linecol_to_pos(text, name.line, name.column) - result.append(self.__make_name_msg(name, offset)) + result.append(NameMsg.from_name(name, offset)) return result - def rpc_get_names(self, filename, source, offset): + def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: """Return the list of possible names""" names = run_with_debug(jedi, 'get_names', code=source, @@ -323,7 +330,7 @@ def rpc_get_names(self, filename, source, offset): with open(name.module_path) as f: text = f.read() offset = linecol_to_pos(text, name.line, name.column) - result.append(self.__make_name_msg(name, offset)) + result.append(NameMsg.from_name(name, offset)) return result def rpc_get_rename_diff(self, filename, source, offset, new_name): @@ -336,11 +343,12 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): column=column, new_name=new_name) except Exception as e: - return self.__make_error_msg(e) - return self.__make_refactoring_msg(ref) + return ErrorMsg.from_error(e) + return RefactoringMsg.from_refactoring(ref) - def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end): + def rpc_get_extract_variable_diff( + self, filename, source, offset, new_name, + line_beg, line_end, col_beg, col_end) -> Type[ResultMsg]: """Get the diff resulting from extracting the selected code""" if not hasattr(jedi.Script, "extract_variable"): # pragma: no cover return {'success': "Not available"} @@ -355,10 +363,11 @@ def rpc_get_extract_variable_diff(self, filename, source, offset, new_name, if ref is None: return {'success': False} else: - return self.__make_error_msg(ref) + return ErrorMsg.from_error(ref) - def rpc_get_extract_function_diff(self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end): + def rpc_get_extract_function_diff( + self, filename, source, offset, new_name, + line_beg, line_end, col_beg, col_end) -> Type[ResultMsg]: """Get the diff resulting from extracting the selected code""" script = jedi.Script(code=source, path=filename, environment=self.environment) @@ -369,10 +378,10 @@ def rpc_get_extract_function_diff(self, filename, source, offset, new_name, until_column=col_end, new_name=new_name) except Exception as e: - return self.__make_error_msg(e) - return self.__make_refactoring_msg(ref) + return ErrorMsg.from_error(e) + return RefactoringMsg.from_refactoring(ref) - def rpc_get_inline_diff(self, filename, source, offset): + def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: """Get the diff resulting from inlining the selected variable""" if not hasattr(jedi.Script, "inline"): # pragma: no cover return {'success': "Not available"} @@ -385,7 +394,7 @@ def rpc_get_inline_diff(self, filename, source, offset): if ref is None: return {'success': False} else: - return self.__make_refactoring_msg(ref) + return RefactoringMsg.from_refactoring(ref) # From the Jedi documentation: diff --git a/elpy/rpc.py b/elpy/rpc.py index b56fdaf63..99138b11f 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -14,18 +14,18 @@ from typing import Union, Optional, List, Type -class Msg(BaseModel): +class ResultMsg(BaseModel): pass class ErrorMsg(BaseModel): id: int - error: Union[dict, Type[Msg]] + error: Union[dict, Type[ResultMsg]] -class ResultMsg(BaseModel): +class ResponceMsg(BaseModel): id: int - result: Union[dict, List, str, Type[Msg]] + result: Union[dict, List, str, Type[ResultMsg]] class JSONRPCServer(object): @@ -52,7 +52,7 @@ class JSONRPCServer(object): {"id": 23, "error": "Simple error message"} - See http://www.jsonrpc.org/ for the inspiration of the protocol. + See https://www.jsonrpc.org/ for the inspiration of the protocol. """ @@ -83,13 +83,13 @@ def read_json(self) -> str: raise EOFError() return json.loads(line) - def send_msg(self, msg: Msg) -> None: + def send_msg(self, msg: ResultMsg) -> None: """Write an JSON object on a single line. """ self.stdout.write(msg.json() + '\n') self.stdout.flush() - def handle_request(self) -> Type[Msg]: + def handle_request(self) -> Type[ResultMsg]: """Handle a single JSON-RPC request. Read a request, call the appropriate handler method, and @@ -111,7 +111,7 @@ def handle_request(self) -> Type[Msg]: self.send_msg(msg) def _make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[Type[Msg]]: + ) -> Optional[Type[ResultMsg]]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -119,7 +119,7 @@ def _make_msg(self, request_id: str, method_name: str, params: List else: result = self.handle(method_name, params) if request_id is not None: - return ResultMsg(result=result, id=request_id) + return ResponceMsg(result=result, id=request_id) except Fault as fault: error = {"message": fault.message, "code": fault.code} @@ -133,7 +133,7 @@ def _make_msg(self, request_id: str, method_name: str, params: List } return ErrorMsg(error=error, id=request_id) - def handle(self, method_name: str, args: List) -> None: + def handle(self, method_name: str, args: List) -> Type[ResultMsg]: """Handle the call to method_name. You should overwrite this method in a subclass. diff --git a/elpy/tests/test_rpc.py b/elpy/tests/test_rpc.py index 04da332ed..0d3166213 100644 --- a/elpy/tests/test_rpc.py +++ b/elpy/tests/test_rpc.py @@ -5,7 +5,7 @@ import sys from elpy import rpc -from elpy.rpc import Msg, ErrorMsg, ResultMsg +from elpy.rpc import ResultMsg, ErrorMsg, ResponceMsg from elpy.tests.compat import StringIO @@ -79,28 +79,28 @@ def test_should_fail_on_malformed_json(self): class TestWriteJson(TestJSONRPCServer): def test_result_msg_shuld_work_with_msg(self): - class DataMsg(Msg): + class DataMsg(ResultMsg): data: str self.rpc.send_msg( - ResultMsg(id=100, result=DataMsg(data="some data"))) + ResponceMsg(id=100, result=DataMsg(data="some data"))) self.assertEqual( self.read(), '{"id": 100, "result": {"data": "some data"}}\n') def test_result_msg__shuld_work_with_dict(self): self.rpc.send_msg( - ResultMsg(id=100, result={'data': 'some data'})) + ResponceMsg(id=100, result={'data': 'some data'})) self.assertEqual(self.read(), '{"id": 100, "result": {"data": "some data"}}\n') def test_result_msg__shuld_work_with_list(self): self.rpc.send_msg( - ResultMsg(id=100, result=['some data1', 'some data2', 3])) + ResponceMsg(id=100, result=['some data1', 'some data2', 3])) self.assertEqual( self.read(), '{"id": 100, "result": ["some data1", "some data2", 3]}\n') def test_result_msg__shuld_work_with_str(self): - self.rpc.send_msg(ResultMsg(id=100, result="some string")) + self.rpc.send_msg(ResponceMsg(id=100, result="some string")) self.assertEqual(self.read(), '{"id": 100, "result": "some string"}\n') def test_error_msg__shuld_work_with_dict(self): @@ -110,7 +110,7 @@ def test_error_msg__shuld_work_with_dict(self): self.read(), '{"id": 100, "error": {"error": "error msg"}}\n') def test_error_msg__shuld_work_with_msg(self): - class MyErrorMsg(Msg): + class MyErrorMsg(ResultMsg): error: str self.rpc.send_msg( ErrorMsg(id=100, error=MyErrorMsg(error="error msg"))) From 0b92b61f4c2788b24cb91ef818086c76ded67fd2 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Fri, 8 Jan 2021 23:07:46 +0700 Subject: [PATCH 07/24] fix tests --- elpy/jedibackend.py | 147 ++++++++++++++++++++++----------- elpy/rpc.py | 5 +- elpy/tests/support.py | 119 +++++++------------------- elpy/tests/test_jedibackend.py | 18 ++++ 4 files changed, 149 insertions(+), 140 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 2deb197aa..dd40da585 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -10,8 +10,11 @@ import sys import traceback import re +from functools import reduce +from io import StringIO +from bisect import bisect_right -from typing import Type, List +from typing import Type, List, Optional, NamedTuple, Any from pathlib import Path import jedi @@ -22,13 +25,14 @@ class NameMsg(ResultMsg): name: str offset: int - filename: Path + filename: str + @classmethod def from_name( cls, x: jedi.api.classes.Name, offset: int) -> NameMsg: if type(x) is not jedi.api.classes.Name: raise TypeError("Must be jedi.api.classes.Name") - return cls(name=x.name, filename=x.module_path, offset=offset) + return cls(name=x.name, filename=str(x.module_path), offset=offset) class RefactoringMsg(ResultMsg): @@ -36,7 +40,14 @@ class RefactoringMsg(ResultMsg): project_path: Path diff: str changed_files: List[Path] - + error_msg: Optional[str] + + @classmethod + def fail(cls, error_msg=""): + return cls( + success=False, project_path=Path(), diff="", changed_files=[], + error_msg=error_msg) + @classmethod def from_refactoring( cls, x: jedi.api.refactoring.Refactoring) -> RefactoringMsg: @@ -49,17 +60,6 @@ def from_refactoring( changed_files=list(x.get_changed_files().keys())) -class ErrorMsg(ResultMsg): - success: bool - error_type: str - error_msg: str - - @classmethod - def from_error(cls, e): - return ErrorMsg(success=False, - error_type=type(e).__name__, - error_msg=str(e)) - # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). @@ -86,8 +86,11 @@ def __init__(self, project_root, environment_binaries_path): self.project_root = project_root self.environment = None if environment_binaries_path is not None: - self.environment = jedi.create_environment(environment_binaries_path, - safe=False) + try: + self.environment = jedi.create_environment( + environment_binaries_path, safe=False) + except jedi.api.environment.InvalidPythonEnvironment as e: + Fault(message=str(e)) self.completions = {} sys.path.append(project_root) @@ -105,21 +108,18 @@ def rpc_get_completions(self, filename, source, offset): 'meta': proposal.description} for proposal in proposals] - def rpc_get_completion_docstring(self, completion): + def rpc_get_completion_docstring(self, completion) -> Optional[Any]: proposal = self.completions.get(completion) - if proposal is None: - return None - else: + if proposal is not None: 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) + if proposal is not None: + return (str(proposal.module_path), proposal.line) - def rpc_get_docstring(self, filename, source, offset): + def rpc_get_docstring(self, filename: str, source: str, offset: int + ) -> Optional[Any]: line, column = pos_to_linecol(source, offset) locations = run_with_debug(jedi, 'goto', code=source, @@ -138,11 +138,11 @@ def rpc_get_docstring(self, filename, source, offset): if 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) + src = SourceFile(filename, source) + line, column = src.get_pos(offset) locations = run_with_debug(jedi, 'goto', code=source, path=filename, @@ -165,15 +165,11 @@ def rpc_get_definition(self, filename, source, offset): return None loc = locations[-1] try: - if loc.module_path == filename: - offset = linecol_to_pos(source, - loc.line, - loc.column) + if loc.module_path == Path(filename): + offset = src.get_offset(loc.line, loc.column) else: - with open(loc.module_path) as f: - offset = linecol_to_pos(f.read(), - loc.line, - loc.column) + offset = SourceFile( + path=loc.module_path).get_offset(loc.line, loc.column) except IOError: # pragma: no cover return None return (str(loc.module_path), offset) @@ -304,7 +300,7 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: return None result = [] for name in names: - if name.module_path == filename: + if name.module_path == 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: @@ -324,7 +320,7 @@ def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: 'references': True}) result = [] for name in names: - if name.module_path == filename: + if name.module_path == 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: @@ -343,15 +339,13 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): column=column, new_name=new_name) except Exception as e: - return ErrorMsg.from_error(e) + return RefactoringMsg.fail(error_msg=str(e)) return RefactoringMsg.from_refactoring(ref) def rpc_get_extract_variable_diff( self, filename, source, offset, new_name, line_beg, line_end, col_beg, col_end) -> Type[ResultMsg]: """Get the diff resulting from extracting the selected code""" - if not hasattr(jedi.Script, "extract_variable"): # pragma: no cover - return {'success': "Not available"} ref = run_with_debug(jedi, 'extract_variable', code=source, path=filename, environment=self.environment, @@ -361,9 +355,9 @@ def rpc_get_extract_variable_diff( 'until_column': col_end, 'new_name': new_name}) if ref is None: - return {'success': False} + return RefactoringMsg.fail() else: - return ErrorMsg.from_error(ref) + return RefactoringMsg.from_refactoring(ref) def rpc_get_extract_function_diff( self, filename, source, offset, new_name, @@ -378,13 +372,11 @@ def rpc_get_extract_function_diff( until_column=col_end, new_name=new_name) except Exception as e: - return ErrorMsg.from_error(e) + return RefactoringMsg.fail(error_msg=str(e)) return RefactoringMsg.from_refactoring(ref) def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: """Get the diff resulting from inlining the selected variable""" - if not hasattr(jedi.Script, "inline"): # pragma: no cover - return {'success': "Not available"} line, column = pos_to_linecol(source, offset) ref = run_with_debug(jedi, 'inline', code=source, path=filename, @@ -392,7 +384,7 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: fun_kwargs={'line': line, 'column': column}) if ref is None: - return {'success': False} + return RefactoringMsg.fail() else: return RefactoringMsg.from_refactoring(ref) @@ -403,6 +395,67 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: # 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. +class Pos(NamedTuple): + line: int + col: int + +class SourceFile: + _source: Optional[str] + _path: Path + _index: List[int] + + def __init__( + self, path: Path, source: Optional[str] = None, + line_start: int = 1, col_start: int = 0): + self._source = source + self._path = path + self._index = None + self._line_start = line_start + self._col_start = col_start + + def get_source(self): + if self._source is None: + with open(self._path, 'r') as fh: + self._source = fh.read() + return self._source + + def get_path(self): + return self._path + + def get_pos(self, offset: 0) -> 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. + + """ + idx = self._get_index() + line_num = bisect_right(idx, offset) - 1 + line_offset = idx[line_num] + return Pos(line_num + self._line_start, + offset - line_offset + self._col_start) + + def get_offset(self, line: int, col: int): + """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. + + """ + idx = self._get_index() + return idx[line - self._line_start] + col - self._col_start + + def _get_index(self) -> List[int]: + if self._index is None: + self._index = [0] + i = 0 + for line in StringIO(self.get_source()): + i += len(line) + self._index.append(i) + return self._index + def pos_to_linecol(text, pos): """Return a tuple of line and column for offset pos in text. diff --git a/elpy/rpc.py b/elpy/rpc.py index 99138b11f..778d593fb 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -118,7 +118,7 @@ def _make_msg(self, request_id: str, method_name: str, params: List result = method(*params) else: result = self.handle(method_name, params) - if request_id is not None: + if request_id is not None and result is not None: return ResponceMsg(result=result, id=request_id) except Fault as fault: error = {"message": fault.message, @@ -133,7 +133,8 @@ def _make_msg(self, request_id: str, method_name: str, params: List } return ErrorMsg(error=error, id=request_id) - def handle(self, method_name: str, args: List) -> Type[ResultMsg]: + def handle(self, method_name: str, args: List + ) -> Optional[Type[ResultMsg]]: """Handle the call to method_name. You should overwrite this method in a subclass. diff --git a/elpy/tests/support.py b/elpy/tests/support.py index 05ce9f57a..f46939c90 100644 --- a/elpy/tests/support.py +++ b/elpy/tests/support.py @@ -18,11 +18,12 @@ import tempfile import unittest import re +from pathlib import Path from elpy.tests import compat from elpy.rpc import Fault from elpy import jedibackend - +from elpy.jedibackend import NameMsg class BackendTestCase(unittest.TestCase): """Base class for backend tests. @@ -44,18 +45,14 @@ def project_file(self, relname, contents): Write contents into that file. """ - full_name = os.path.join(self.project_root, relname) + path = Path(self.project_root, relname) try: - os.makedirs(os.path.dirname(full_name)) + os.makedirs(os.path.dirname(path)) except OSError: pass - if compat.PYTHON3: - fobj = open(full_name, "w", encoding="utf-8") - else: - fobj = open(full_name, "w") - with fobj as f: + with open(path, "w") as f: f.write(contents) - return full_name + return str(path) class GenericRPCTests(object): @@ -451,13 +448,6 @@ def test_should_return_nothing_when_no_completion(self): self.assertEqual([], self.backend.rpc_get_completions("test.py", source, offset)) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_completions - self.backend.rpc_get_completions = self.backend.rpc_get_completions_jedi16 - self.test_should_complete_builtin() - self.backend.rpc_get_completions = backup - - class RPCGetCompletionDocstringTests(object): def test_should_return_docstring(self): source, offset = source_and_offset("import json\n" @@ -488,12 +478,6 @@ def test_should_return_none_if_on_a_builtin(self): offset) self.assertIsNone(completions) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_docstring - self.backend.rpc_get_docstring = self.backend.rpc_get_docstring_jedi16 - self.test_should_return_docstring() - self.backend.rpc_get_docstring = backup - class RPCGetCompletionLocationTests(object): def test_should_return_location(self): @@ -624,12 +608,13 @@ def test_should_find_variable_definition(self): offset), (filename, 0)) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_definition - self.backend.rpc_get_definition = self.backend.rpc_get_definition_jedi16 - self.test_should_return_definition_location_same_file() - self.backend.rpc_get_definition = backup - + +class RPCGetAssignmentTests(): + METHOD = "rpc_get_assignment" + def test_should_raise_fault(self): + with self.assertRaises(Fault): + self.backend.rpc_get_assignment("test.py", "dummy code", 1) + class RPCGetCalltipTests(GenericRPCTests): METHOD = "rpc_get_calltip" @@ -776,13 +761,6 @@ def test_should_return_oneline_docstring_if_no_calltip(self): self.assertEqual(calltip['kind'], 'oneline_doc') self.assertEqual(calltip['doc'], 'No documentation') - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_calltip - self.backend.rpc_get_calltip = self.backend.rpc_get_calltip_jedi16 - self.test_should_get_calltip() - self.backend.rpc_get_calltip = backup - - class RPCGetDocstringTests(GenericRPCTests): METHOD = "rpc_get_docstring" @@ -812,12 +790,6 @@ def test_should_return_none_for_bad_identifier(self): offset) self.assertIsNone(docstring) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_docstring - self.backend.rpc_get_docstring = self.backend.rpc_get_docstring_jedi16 - self.test_should_get_docstring() - self.backend.rpc_get_docstring = backup - class RPCGetOnelineDocstringTests(GenericRPCTests): METHOD = "rpc_get_oneline_docstring" @@ -873,15 +845,6 @@ def test_should_return_none_for_bad_identifier(self): offset) self.assertIsNone(docstring) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_oneline_docstring - self.backend.rpc_get_oneline_docstring = self.backend.rpc_get_oneline_docstring_jedi16 - self.test_should_get_oneline_docstring() - self.test_should_get_oneline_docstring_for_modules() - self.test_should_return_none_for_bad_identifier() - self.backend.rpc_get_oneline_docstring = backup - - @unittest.skipIf(sys.version_info < (3, 6), "Jedi refactoring not available for python < 3.6") class RPCGetRenameDiffTests(object): @@ -894,12 +857,12 @@ def test_should_return_rename_diff(self): new_name = "c" diff = self.backend.rpc_get_rename_diff("test.py", source, offset, new_name) - assert diff['success'] + assert diff.success self.assertIn("-def foo(a, b):\n" "- print(a)\n" "+def foo(c, b):\n" "+ print(c)", - diff['diff']) + diff.diff) def test_should_fail_for_invalid_symbol_at_point(self): source, offset = source_and_offset("def foo(a, b):\n" @@ -908,7 +871,7 @@ def test_should_fail_for_invalid_symbol_at_point(self): new_name = "c" diff = self.backend.rpc_get_rename_diff("test.py", source, offset, new_name) - self.assertFalse(diff['success']) + self.assertFalse(diff.success) @unittest.skipIf(sys.version_info < (3, 6), @@ -925,13 +888,13 @@ def test_should_return_function_extraction_diff(self): new_name, line_beg=1, line_end=2, col_beg=0, col_end=8) - assert diff['success'] + assert diff.success self.assertIn('-print(a)\n' '-return b\n' '+def foo(a, b):\n' '+ print(a)\n' '+ return b\n', - diff['diff']) + diff.diff) @unittest.skipIf(sys.version_info < (3, 6), @@ -949,9 +912,9 @@ def test_should_return_variable_extraction_diff(self): new_name, line_beg=3, line_end=3, col_beg=7, col_end=16) - assert diff['success'] + print(diff) self.assertIn("-print(a + 1 + b/2)\n+c = a + 1 + b/2\n+print(c)\n", - diff['diff']) + diff.diff) @unittest.skipIf(sys.version_info < (3, 6), @@ -965,9 +928,9 @@ def test_should_return_inline_diff(self): "x = int(ba_|_r)\n") diff = self.backend.rpc_get_inline_diff("test.py", source, offset) - assert diff['success'] + assert diff.success self.assertIn("-bar = foo + 1\n-x = int(bar)\n+x = int(foo + 1)", - diff['diff']) + diff.diff) def test_should_error_on_refactoring_failure(self): source, offset = source_and_offset("foo = 3.1\n" @@ -975,7 +938,7 @@ def test_should_error_on_refactoring_failure(self): "x = in_|_t(bar)\n") diff = self.backend.rpc_get_inline_diff("test.py", source, offset) - self.assertFalse(diff['success']) + self.assertFalse(diff.success) class RPCGetNamesTests(GenericRPCTests): @@ -1024,13 +987,6 @@ def test_should_not_fail_without_symbol(self): self.assertEqual(names, []) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_names - self.backend.rpc_get_names = self.backend.rpc_get_names_jedi16 - self.test_shouldreturn_names_in_same_file() - self.test_should_not_fail_without_symbol() - self.backend.rpc_get_names = backup - class RPCGetUsagesTests(GenericRPCTests): METHOD = "rpc_get_usages" @@ -1046,15 +1002,9 @@ def test_should_return_uses_in_same_file(self): offset) self.assertEqual(usages, - [{'name': 'x', - 'offset': 8, - 'filename': filename}, - {'name': 'x', - 'filename': filename, - 'offset': 23}, - {'name': u'x', - 'filename': filename, - 'offset': 27}]) + [NameMsg(name='x', offset=8, filename=filename), + NameMsg(name='x', offset=23, filename=filename), + NameMsg(name='x', offset=27, filename=filename)]) def test_should_return_uses_in_other_file(self): file1 = self.project_file("file1.py", "") @@ -1067,13 +1017,8 @@ def test_should_return_uses_in_other_file(self): source, offset) - self.assertEqual(usages, - [{'name': 'x', - 'filename': file1, - 'offset': 19}, - {'name': 'x', - 'filename': file2, - 'offset': 5}]) + self.assertEqual(usages, [NameMsg(name="x", filename=file1, offset=19), + NameMsg(name="x", filename=file2, offset=5)]) def test_should_not_fail_without_symbol(self): filename = self.project_file("file.py", "") @@ -1084,14 +1029,6 @@ def test_should_not_fail_without_symbol(self): self.assertEqual(usages, []) - def test_should_handle_jedi16(self): - backup = self.backend.rpc_get_usages - self.backend.rpc_get_usages = self.backend.rpc_get_usages_jedi16 - self.test_should_return_uses_in_same_file() - self.test_should_return_uses_in_other_file() - self.test_should_not_fail_without_symbol() - self.backend.rpc_get_usages = backup - def source_and_offset(source): """Return a source and offset from a source description. diff --git a/elpy/tests/test_jedibackend.py b/elpy/tests/test_jedibackend.py index 1e8d2c634..83477447a 100644 --- a/elpy/tests/test_jedibackend.py +++ b/elpy/tests/test_jedibackend.py @@ -8,6 +8,7 @@ import re from elpy import jedibackend +from elpy.jedibackend import SourceFile from elpy import rpc from elpy.tests import compat from elpy.tests.support import BackendTestCase @@ -33,6 +34,23 @@ def setUp(self): env = jedi.get_default_environment().path self.backend = jedibackend.JediBackend(self.project_root, env) +class TestSourceFile(JediBackendTestCase): + def setUp(self): + self.src = SourceFile(path="", source="\n1234\n6789") + + def test_building_index(self): + self.assertEqual(self.src._get_index(), [0, 1, 6, 10]) + + def test_get_source_offset(self): + self.assertEqual(self.src.get_offset(1, 0), 0) + self.assertEqual(self.src.get_offset(2, 0), 1) + + def test_get_source_lines(self): + self.assertEqual(self.src.get_pos(0), (1, 0)) + self.assertEqual(self.src.get_pos(1), (2, 0)) + self.assertEqual(self.src.get_pos(5), (2, 4)) + self.assertEqual(self.src.get_pos(9), (3, 3)) + class TestInit(JediBackendTestCase): def test_should_have_jedi_as_name(self): From 835d26728977b0441ff5c71035e2405aa131655b Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 05:28:23 +0700 Subject: [PATCH 08/24] replace linecol_to_pos by SourceCode class method --- elpy/jedibackend.py | 117 ++++++++++++++++----------------- elpy/tests/test_jedibackend.py | 83 ++++++++++------------- 2 files changed, 94 insertions(+), 106 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index dd40da585..996107fe4 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -14,7 +14,7 @@ from io import StringIO from bisect import bisect_right -from typing import Type, List, Optional, NamedTuple, Any +from typing import Type, List, Optional, Union, NamedTuple, Any from pathlib import Path import jedi @@ -95,8 +95,9 @@ def __init__(self, project_root, environment_binaries_path): 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, 'complete', code=source, + src = SourceCode(filename, source) + line, column = src.get_pos(offset) + proposals = run_with_debug(jedi, 'complete', code=str(src), path=filename, environment=self.environment, fun_kwargs={'line': line, 'column': column}) @@ -120,7 +121,8 @@ def rpc_get_completion_location(self, completion): def rpc_get_docstring(self, filename: str, source: str, offset: int ) -> Optional[Any]: - line, column = pos_to_linecol(source, offset) + src = SourceCode(filename, source) + line, column = src.get_pos(offset) locations = run_with_debug(jedi, 'goto', code=source, path=filename, @@ -141,7 +143,7 @@ def rpc_get_docstring(self, filename: str, source: str, offset: int def rpc_get_definition(self, filename, source, offset): line, column = pos_to_linecol(source, offset) - src = SourceFile(filename, source) + src = SourceCode(filename, source) line, column = src.get_pos(offset) locations = run_with_debug(jedi, 'goto', code=source, @@ -168,7 +170,7 @@ def rpc_get_definition(self, filename, source, offset): if loc.module_path == Path(filename): offset = src.get_offset(loc.line, loc.column) else: - offset = SourceFile( + offset = SourceCode( path=loc.module_path).get_offset(loc.line, loc.column) except IOError: # pragma: no cover return None @@ -224,9 +226,10 @@ def rpc_get_calltip_or_oneline_docstring(self, filename, source, offset): 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) + src = SourceCode(filename, source) + line, column = src.get_pos(offset) definitions = run_with_debug(jedi, 'goto', - code=source, + code=str(src), path=filename, environment=self.environment, fun_kwargs={'line': line, @@ -289,9 +292,10 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: fields name, filename, and offset. """ - line, column = pos_to_linecol(source, offset) + src = SourceCode(filename, source) + line, column = src.get_pos(offset) names = run_with_debug(jedi, 'get_references', - code=source, + code=str(src), path=filename, environment=self.environment, fun_kwargs={'line': line, @@ -301,18 +305,18 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: result = [] for name in names: if name.module_path == Path(filename): - offset = linecol_to_pos(source, name.line, name.column) + offset = src.get_offset(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) + other_src = SourceCode(name.module_path) + offset = other_src.get_offset(name.line, name.column) result.append(NameMsg.from_name(name, offset)) return result def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: """Return the list of possible names""" + src = SourceCode(filename, source) names = run_with_debug(jedi, 'get_names', - code=source, + code=str(src), path=filename, environment=self.environment, fun_kwargs={'all_scopes': True, @@ -320,19 +324,19 @@ def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: 'references': True}) result = [] for name in names: - if name.module_path == Path(filename): - offset = linecol_to_pos(source, name.line, name.column) + if name.module_path == src.path: + offset = src.get_offset(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) + other_src = SourceCode(name.module_path) + offset = other_src.get_offset(name.line, name.column) result.append(NameMsg.from_name(name, offset)) return result def rpc_get_rename_diff(self, filename, source, offset, new_name): """Get the diff resulting from renaming the thing at point""" - line, column = pos_to_linecol(source, offset) - script = jedi.Script(code=source, path=filename, + src = SourceCode(filename, source) + line, column = src.get_pos(offset) + script = jedi.Script(code=str(src), path=filename, environment=self.environment) try: ref = script.rename(line=line, @@ -377,8 +381,9 @@ def rpc_get_extract_function_diff( def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: """Get the diff resulting from inlining the selected variable""" - line, column = pos_to_linecol(source, offset) - ref = run_with_debug(jedi, 'inline', code=source, + src = SourceCode(filename, source) + line, column = src.get_pos(offset) + ref = run_with_debug(jedi, 'inline', code=str(src), path=filename, environment=self.environment, fun_kwargs={'line': line, @@ -399,17 +404,18 @@ class Pos(NamedTuple): line: int col: int -class SourceFile: +class SourceCode: _source: Optional[str] _path: Path _index: List[int] def __init__( - self, path: Path, source: Optional[str] = None, + self, path: Union[None, str, Path], source: Optional[str] = None, line_start: int = 1, col_start: int = 0): + if path is not None: + self._path = Path(path) self._source = source - self._path = path - self._index = None + self._line_offsets = None self._line_start = line_start self._col_start = col_start @@ -419,7 +425,11 @@ def get_source(self): self._source = fh.read() return self._source - def get_path(self): + def __str__(self): + return self.get_source() + + @property + def path(self) -> Optional[Path]: return self._path def get_pos(self, offset: 0) -> Pos: @@ -430,7 +440,7 @@ def get_pos(self, offset: 0) -> Pos: This is how Jedi wants it. Don't ask me why. """ - idx = self._get_index() + idx = self._get_line_offsets() line_num = bisect_right(idx, offset) - 1 line_offset = idx[line_num] return Pos(line_num + self._line_start, @@ -444,17 +454,28 @@ def get_offset(self, line: int, col: int): This is how Jedi wants it. Don't ask me why. """ - idx = self._get_index() - return idx[line - self._line_start] + col - self._col_start + line_offsets = self._get_line_offsets() + line_from_zero = line - self._line_start + col_from_zero = col - self._col_start + try: + cur_line_offset = line_offsets[line_from_zero] + except IndexError: + raise ValueError(f"Text does not have {line} lines.") + offset = cur_line_offset + col_from_zero + next_line_offset = line_offsets[line_from_zero + 1] + if not offset < next_line_offset: + raise ValueError( + f"Line {line} column {col} is not within the text") + return offset - def _get_index(self) -> List[int]: - if self._index is None: - self._index = [0] + def _get_line_offsets(self) -> List[int]: + if self._line_offsets is None: + self._line_offsets = [0] i = 0 for line in StringIO(self.get_source()): i += len(line) - self._index.append(i) - return self._index + self._line_offsets.append(i) + return self._line_offsets def pos_to_linecol(text, pos): @@ -471,28 +492,6 @@ def pos_to_linecol(text, pos): 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, fun_kwargs={}, *args, **kwargs): re_raise = kwargs.pop('re_raise', ()) try: diff --git a/elpy/tests/test_jedibackend.py b/elpy/tests/test_jedibackend.py index 83477447a..8cf7765b4 100644 --- a/elpy/tests/test_jedibackend.py +++ b/elpy/tests/test_jedibackend.py @@ -2,13 +2,14 @@ import sys import unittest +from pathlib import Path import jedi import mock import re from elpy import jedibackend -from elpy.jedibackend import SourceFile +from elpy.jedibackend import SourceCode from elpy import rpc from elpy.tests import compat from elpy.tests.support import BackendTestCase @@ -26,7 +27,7 @@ from elpy.tests.support import RPCGetExtractVariableDiffTests from elpy.tests.support import RPCGetInlineDiffTests from elpy.tests.support import RPCGetAssignmentTests - +from elpy.tests.support import source_and_offset class JediBackendTestCase(BackendTestCase): def setUp(self): @@ -34,24 +35,50 @@ def setUp(self): env = jedi.get_default_environment().path self.backend = jedibackend.JediBackend(self.project_root, env) -class TestSourceFile(JediBackendTestCase): +class TestSourceCode(BackendTestCase): def setUp(self): - self.src = SourceFile(path="", source="\n1234\n6789") + super().setUp() + self.src = SourceCode(path="/notreal/path", source="\n1234\n6789") - def test_building_index(self): - self.assertEqual(self.src._get_index(), [0, 1, 6, 10]) + def test_should_build_offset_index(self): + self.assertEqual(self.src._get_line_offsets(), [0, 1, 6, 10]) - def test_get_source_offset(self): + def test_should_return_offset(self): self.assertEqual(self.src.get_offset(1, 0), 0) self.assertEqual(self.src.get_offset(2, 0), 1) - def test_get_source_lines(self): + def test_should_return_pos(self): self.assertEqual(self.src.get_pos(0), (1, 0)) self.assertEqual(self.src.get_pos(1), (2, 0)) self.assertEqual(self.src.get_pos(5), (2, 4)) self.assertEqual(self.src.get_pos(9), (3, 3)) - + def test_should_return_source_string(self): + self.assertEqual(str(self.src), "\n1234\n6789") + + def test_should_return_source_path(self): + self.assertEqual(self.src.path, Path("/notreal/path")) + + def test_should_load_source_if_not_source_arg(self): + file_name = self.project_file("test.py", "my\nsource\ncode") + src = SourceCode(file_name) + self.assertEqual("my\nsource\ncode", str(src)) + + def test_should_not_load_source_if_source_in_args(self): + file_name = self.project_file("test.py", "my\nsource\nin\file") + not_saved_source_string = "edited\nbut\not\nsaved\source" + src = SourceCode(file_name, not_saved_source_string) + self.assertEqual(not_saved_source_string, str(src)) + + def test_should_fail_on_wrong_pos(self): + with self.assertRaises(ValueError) as cm: + self.src.get_offset(100, 1) + self.assertEqual("Text does not have 100 lines.", str(cm.exception)) + with self.assertRaises(ValueError) as cm: + self.src.get_offset(1, 1) + self.assertEqual( "Line 1 column 1 is not within the text", + str(cm.exception)) + class TestInit(JediBackendTestCase): def test_should_have_jedi_as_name(self): self.assertEqual(self.backend.name, "jedi") @@ -250,44 +277,6 @@ class TestRPCGetNames(RPCGetNamesTests, pass -class TestPosToLinecol(unittest.TestCase): - def test_should_handle_beginning_of_string(self): - self.assertEqual(jedibackend.pos_to_linecol("foo", 0), - (1, 0)) - - def test_should_handle_end_of_line(self): - self.assertEqual(jedibackend.pos_to_linecol("foo\nbar\nbaz\nqux", 9), - (3, 1)) - - def test_should_handle_end_of_string(self): - self.assertEqual(jedibackend.pos_to_linecol("foo\nbar\nbaz\nqux", 14), - (4, 2)) - - -class TestLinecolToPos(unittest.TestCase): - def test_should_handle_beginning_of_string(self): - self.assertEqual(jedibackend.linecol_to_pos("foo", 1, 0), - 0) - - def test_should_handle_end_of_string(self): - self.assertEqual(jedibackend.linecol_to_pos("foo\nbar\nbaz\nqux", - 3, 1), - 9) - - def test_should_return_offset(self): - self.assertEqual(jedibackend.linecol_to_pos("foo\nbar\nbaz\nqux", - 4, 2), - 14) - - def test_should_fail_for_line_past_text(self): - self.assertRaises(ValueError, - jedibackend.linecol_to_pos, "foo\n", 3, 1) - - def test_should_fail_for_column_past_text(self): - self.assertRaises(ValueError, - jedibackend.linecol_to_pos, "foo\n", 1, 10) - - class TestRunWithDebug(unittest.TestCase): @mock.patch('jedi.Script') def test_should_call_method(self, Script): From cee633ac071ead27c76741f6f244b805675c80d5 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 05:31:41 +0700 Subject: [PATCH 09/24] remove fogeted print --- elpy/tests/support.py | 1 - 1 file changed, 1 deletion(-) diff --git a/elpy/tests/support.py b/elpy/tests/support.py index f46939c90..f1c8ffc7a 100644 --- a/elpy/tests/support.py +++ b/elpy/tests/support.py @@ -912,7 +912,6 @@ def test_should_return_variable_extraction_diff(self): new_name, line_beg=3, line_end=3, col_beg=7, col_end=16) - print(diff) self.assertIn("-print(a + 1 + b/2)\n+c = a + 1 + b/2\n+print(c)\n", diff.diff) From a98ea03ff93e88fb4577fd1f2b3b36fddbe858f6 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 06:06:25 +0700 Subject: [PATCH 10/24] refactoring --- elpy/jedibackend.py | 40 +++++++++++++++++----------------- elpy/rpc.py | 14 ++++++------ elpy/tests/support.py | 18 ++++++++++----- elpy/tests/test_jedibackend.py | 3 ++- elpy/tests/test_rpc.py | 6 ++--- 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 996107fe4..99d516a67 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -20,22 +20,22 @@ from elpy import rpc -from elpy.rpc import Fault, ResultMsg +from elpy.rpc import Fault, Result -class NameMsg(ResultMsg): +class NameResult(Result): name: str offset: int filename: str @classmethod def from_name( - cls, x: jedi.api.classes.Name, offset: int) -> NameMsg: + cls, x: jedi.api.classes.Name, offset: int) -> NameResult: if type(x) is not jedi.api.classes.Name: raise TypeError("Must be jedi.api.classes.Name") return cls(name=x.name, filename=str(x.module_path), offset=offset) -class RefactoringMsg(ResultMsg): +class RefactoringResult(Result): success: bool project_path: Path diff: str @@ -50,7 +50,7 @@ def fail(cls, error_msg=""): @classmethod def from_refactoring( - cls, x: jedi.api.refactoring.Refactoring) -> RefactoringMsg: + cls, x: jedi.api.refactoring.Refactoring) -> RefactoringResult: if type(x) is not jedi.api.refactoring.Refactoring: raise TypeError("Must be jedi.api.refactoring.Refactoring type") return cls( @@ -285,7 +285,7 @@ def rpc_get_oneline_docstring(self, filename, source, offset): return {"name": name, "doc": onelinedoc} - def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: + def rpc_get_usages(self, filename, source, offset) -> List[NameResult]: """Return the uses of the symbol at offset. Returns a list of occurrences of the symbol, as dicts with the @@ -309,10 +309,10 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameMsg]: elif name.module_path is not None: other_src = SourceCode(name.module_path) offset = other_src.get_offset(name.line, name.column) - result.append(NameMsg.from_name(name, offset)) + result.append(NameResult.from_name(name, offset)) return result - def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: + def rpc_get_names(self, filename, source, offset) -> Type[Result]: """Return the list of possible names""" src = SourceCode(filename, source) names = run_with_debug(jedi, 'get_names', @@ -329,7 +329,7 @@ def rpc_get_names(self, filename, source, offset) -> Type[ResultMsg]: elif name.module_path is not None: other_src = SourceCode(name.module_path) offset = other_src.get_offset(name.line, name.column) - result.append(NameMsg.from_name(name, offset)) + result.append(NameResult.from_name(name, offset)) return result def rpc_get_rename_diff(self, filename, source, offset, new_name): @@ -343,12 +343,12 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): column=column, new_name=new_name) except Exception as e: - return RefactoringMsg.fail(error_msg=str(e)) - return RefactoringMsg.from_refactoring(ref) + return RefactoringResult.fail(error_msg=str(e)) + return RefactoringResult.from_refactoring(ref) def rpc_get_extract_variable_diff( self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end) -> Type[ResultMsg]: + line_beg, line_end, col_beg, col_end) -> Type[Result]: """Get the diff resulting from extracting the selected code""" ref = run_with_debug(jedi, 'extract_variable', code=source, path=filename, @@ -359,13 +359,13 @@ def rpc_get_extract_variable_diff( 'until_column': col_end, 'new_name': new_name}) if ref is None: - return RefactoringMsg.fail() + return RefactoringResult.fail() else: - return RefactoringMsg.from_refactoring(ref) + return RefactoringResult.from_refactoring(ref) def rpc_get_extract_function_diff( self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end) -> Type[ResultMsg]: + line_beg, line_end, col_beg, col_end) -> Type[Result]: """Get the diff resulting from extracting the selected code""" script = jedi.Script(code=source, path=filename, environment=self.environment) @@ -376,10 +376,10 @@ def rpc_get_extract_function_diff( until_column=col_end, new_name=new_name) except Exception as e: - return RefactoringMsg.fail(error_msg=str(e)) - return RefactoringMsg.from_refactoring(ref) + return RefactoringResult.fail(error_msg=str(e)) + return RefactoringResult.from_refactoring(ref) - def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: + def rpc_get_inline_diff(self, filename, source, offset) -> Type[Result]: """Get the diff resulting from inlining the selected variable""" src = SourceCode(filename, source) line, column = src.get_pos(offset) @@ -389,9 +389,9 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Type[ResultMsg]: fun_kwargs={'line': line, 'column': column}) if ref is None: - return RefactoringMsg.fail() + return RefactoringResult.fail() else: - return RefactoringMsg.from_refactoring(ref) + return RefactoringResult.from_refactoring(ref) # From the Jedi documentation: diff --git a/elpy/rpc.py b/elpy/rpc.py index 778d593fb..30f933146 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -14,18 +14,18 @@ from typing import Union, Optional, List, Type -class ResultMsg(BaseModel): +class Result(BaseModel): pass class ErrorMsg(BaseModel): id: int - error: Union[dict, Type[ResultMsg]] + error: Union[dict, Type[Result]] class ResponceMsg(BaseModel): id: int - result: Union[dict, List, str, Type[ResultMsg]] + result: Union[dict, List, str, Type[Result]] class JSONRPCServer(object): @@ -83,13 +83,13 @@ def read_json(self) -> str: raise EOFError() return json.loads(line) - def send_msg(self, msg: ResultMsg) -> None: + def send_msg(self, msg: Result) -> None: """Write an JSON object on a single line. """ self.stdout.write(msg.json() + '\n') self.stdout.flush() - def handle_request(self) -> Type[ResultMsg]: + def handle_request(self) -> Type[Result]: """Handle a single JSON-RPC request. Read a request, call the appropriate handler method, and @@ -111,7 +111,7 @@ def handle_request(self) -> Type[ResultMsg]: self.send_msg(msg) def _make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[Type[ResultMsg]]: + ) -> Optional[Type[Result]]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -134,7 +134,7 @@ def _make_msg(self, request_id: str, method_name: str, params: List return ErrorMsg(error=error, id=request_id) def handle(self, method_name: str, args: List - ) -> Optional[Type[ResultMsg]]: + ) -> Optional[Type[Result]]: """Handle the call to method_name. You should overwrite this method in a subclass. diff --git a/elpy/tests/support.py b/elpy/tests/support.py index f1c8ffc7a..8ced65022 100644 --- a/elpy/tests/support.py +++ b/elpy/tests/support.py @@ -23,7 +23,8 @@ from elpy.tests import compat from elpy.rpc import Fault from elpy import jedibackend -from elpy.jedibackend import NameMsg +from elpy.jedibackend import NameResult + class BackendTestCase(unittest.TestCase): """Base class for backend tests. @@ -1001,9 +1002,12 @@ def test_should_return_uses_in_same_file(self): offset) self.assertEqual(usages, - [NameMsg(name='x', offset=8, filename=filename), - NameMsg(name='x', offset=23, filename=filename), - NameMsg(name='x', offset=27, filename=filename)]) + [NameResult +(name='x', offset=8, filename=filename), + NameResult +(name='x', offset=23, filename=filename), + NameResult +(name='x', offset=27, filename=filename)]) def test_should_return_uses_in_other_file(self): file1 = self.project_file("file1.py", "") @@ -1016,8 +1020,10 @@ def test_should_return_uses_in_other_file(self): source, offset) - self.assertEqual(usages, [NameMsg(name="x", filename=file1, offset=19), - NameMsg(name="x", filename=file2, offset=5)]) + self.assertEqual(usages, [NameResult +(name="x", filename=file1, offset=19), + NameResult +(name="x", filename=file2, offset=5)]) def test_should_not_fail_without_symbol(self): filename = self.project_file("file.py", "") diff --git a/elpy/tests/test_jedibackend.py b/elpy/tests/test_jedibackend.py index 8cf7765b4..5c8bf3948 100644 --- a/elpy/tests/test_jedibackend.py +++ b/elpy/tests/test_jedibackend.py @@ -78,7 +78,8 @@ def test_should_fail_on_wrong_pos(self): self.src.get_offset(1, 1) self.assertEqual( "Line 1 column 1 is not within the text", str(cm.exception)) - + + class TestInit(JediBackendTestCase): def test_should_have_jedi_as_name(self): self.assertEqual(self.backend.name, "jedi") diff --git a/elpy/tests/test_rpc.py b/elpy/tests/test_rpc.py index 0d3166213..db65bde57 100644 --- a/elpy/tests/test_rpc.py +++ b/elpy/tests/test_rpc.py @@ -5,7 +5,7 @@ import sys from elpy import rpc -from elpy.rpc import ResultMsg, ErrorMsg, ResponceMsg +from elpy.rpc import Result, ErrorMsg, ResponceMsg from elpy.tests.compat import StringIO @@ -79,7 +79,7 @@ def test_should_fail_on_malformed_json(self): class TestWriteJson(TestJSONRPCServer): def test_result_msg_shuld_work_with_msg(self): - class DataMsg(ResultMsg): + class DataMsg(Result): data: str self.rpc.send_msg( ResponceMsg(id=100, result=DataMsg(data="some data"))) @@ -110,7 +110,7 @@ def test_error_msg__shuld_work_with_dict(self): self.read(), '{"id": 100, "error": {"error": "error msg"}}\n') def test_error_msg__shuld_work_with_msg(self): - class MyErrorMsg(ResultMsg): + class MyErrorMsg(Result): error: str self.rpc.send_msg( ErrorMsg(id=100, error=MyErrorMsg(error="error msg"))) From edb6d7ec5fbc083d57f18dbc3c3f964ca23454d8 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 07:00:17 +0700 Subject: [PATCH 11/24] SourceCode now save offsetindex in array.array --- elpy/jedibackend.py | 18 ++++++++++-------- elpy/tests/test_jedibackend.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 99d516a67..5b9e562d4 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -10,7 +10,8 @@ import sys import traceback import re -from functools import reduce + +from array import array from io import StringIO from bisect import bisect_right @@ -41,13 +42,13 @@ class RefactoringResult(Result): diff: str changed_files: List[Path] error_msg: Optional[str] - + @classmethod def fail(cls, error_msg=""): return cls( success=False, project_path=Path(), diff="", changed_files=[], error_msg=error_msg) - + @classmethod def from_refactoring( cls, x: jedi.api.refactoring.Refactoring) -> RefactoringResult: @@ -407,7 +408,7 @@ class Pos(NamedTuple): class SourceCode: _source: Optional[str] _path: Path - _index: List[int] + _line_offsets: array[int] def __init__( self, path: Union[None, str, Path], source: Optional[str] = None, @@ -470,11 +471,12 @@ def get_offset(self, line: int, col: int): def _get_line_offsets(self) -> List[int]: if self._line_offsets is None: - self._line_offsets = [0] - i = 0 + self._line_offsets = array('I') + self._line_offsets.append(0) + curr_line_offset = 0 for line in StringIO(self.get_source()): - i += len(line) - self._line_offsets.append(i) + curr_line_offset += len(line) + self._line_offsets.append(curr_line_offset) return self._line_offsets diff --git a/elpy/tests/test_jedibackend.py b/elpy/tests/test_jedibackend.py index 5c8bf3948..9602bbccd 100644 --- a/elpy/tests/test_jedibackend.py +++ b/elpy/tests/test_jedibackend.py @@ -41,7 +41,7 @@ def setUp(self): self.src = SourceCode(path="/notreal/path", source="\n1234\n6789") def test_should_build_offset_index(self): - self.assertEqual(self.src._get_line_offsets(), [0, 1, 6, 10]) + self.assertEqual(list(self.src._get_line_offsets()), [0, 1, 6, 10]) def test_should_return_offset(self): self.assertEqual(self.src.get_offset(1, 0), 0) From 0262bc98c16b2115971cdaf5af09830f14119895 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 07:47:12 +0700 Subject: [PATCH 12/24] refactoring, fix some mypy errors --- elpy/jedibackend.py | 36 +++++++++++++++++------------------- elpy/rpc.py | 22 ++++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 5b9e562d4..9bd414f09 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -15,7 +15,7 @@ from io import StringIO from bisect import bisect_right -from typing import Type, List, Optional, Union, NamedTuple, Any +from typing import List, Optional, Union, NamedTuple, Any, NoReturn from pathlib import Path import jedi @@ -67,7 +67,7 @@ def from_refactoring( try: from pkg_resources import parse_version except ImportError: # pragma: no cover - def parse_version(*arg, **kwargs): + def parse_version(*arg, **kwargs) -> NoReturn: raise Fault("`pkg_resources` could not be imported, " "please reinstall Elpy RPC virtualenv with" " `M-x elpy-rpc-reinstall-virtualenv`", code=400) @@ -132,15 +132,15 @@ def rpc_get_docstring(self, filename: str, source: str, offset: int 'column': column, 'follow_imports': True, 'follow_builtin_imports': True}) - if not locations: - return None # Filter uninteresting things - if locations[-1].name in ["str", "int", "float", "bool", "tuple", - "list", "dict"]: + uninternsting_to_be_filtered_names = { + "str", "int", "float", "bool", "tuple", "list", "dict"} + if not locations \ + or locations[-1].name in uninternsting_to_be_filtered_names \ + or not locations[-1].docstring(): return None - if locations[-1].docstring(): - return ('Documentation for {0}:\n\n'.format( - locations[-1].full_name) + locations[-1].docstring()) + return ('Documentation for {0}:\n\n'.format( + locations[-1].full_name) + locations[-1].docstring()) def rpc_get_definition(self, filename, source, offset): line, column = pos_to_linecol(source, offset) @@ -301,8 +301,6 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameResult]: environment=self.environment, fun_kwargs={'line': line, 'column': column}) - if names is None: - return None result = [] for name in names: if name.module_path == Path(filename): @@ -313,7 +311,7 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameResult]: result.append(NameResult.from_name(name, offset)) return result - def rpc_get_names(self, filename, source, offset) -> Type[Result]: + def rpc_get_names(self, filename, source, offset) -> List[Result]: """Return the list of possible names""" src = SourceCode(filename, source) names = run_with_debug(jedi, 'get_names', @@ -349,7 +347,7 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): def rpc_get_extract_variable_diff( self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end) -> Type[Result]: + line_beg, line_end, col_beg, col_end) -> Result: """Get the diff resulting from extracting the selected code""" ref = run_with_debug(jedi, 'extract_variable', code=source, path=filename, @@ -366,7 +364,7 @@ def rpc_get_extract_variable_diff( def rpc_get_extract_function_diff( self, filename, source, offset, new_name, - line_beg, line_end, col_beg, col_end) -> Type[Result]: + line_beg, line_end, col_beg, col_end) -> Result: """Get the diff resulting from extracting the selected code""" script = jedi.Script(code=source, path=filename, environment=self.environment) @@ -380,7 +378,7 @@ def rpc_get_extract_function_diff( return RefactoringResult.fail(error_msg=str(e)) return RefactoringResult.from_refactoring(ref) - def rpc_get_inline_diff(self, filename, source, offset) -> Type[Result]: + def rpc_get_inline_diff(self, filename, source, offset) -> Result: """Get the diff resulting from inlining the selected variable""" src = SourceCode(filename, source) line, column = src.get_pos(offset) @@ -407,8 +405,8 @@ class Pos(NamedTuple): class SourceCode: _source: Optional[str] - _path: Path - _line_offsets: array[int] + _path: Optional[Path] + _line_offsets: Optional[array[int]] def __init__( self, path: Union[None, str, Path], source: Optional[str] = None, @@ -433,7 +431,7 @@ def __str__(self): def path(self) -> Optional[Path]: return self._path - def get_pos(self, offset: 0) -> Pos: + def get_pos(self, offset: int) -> Pos: """Return a tuple of line and column for offset pos in text. Lines are one-based, columns zero-based. @@ -469,7 +467,7 @@ def get_offset(self, line: int, col: int): f"Line {line} column {col} is not within the text") return offset - def _get_line_offsets(self) -> List[int]: + def _get_line_offsets(self) -> array[int]: if self._line_offsets is None: self._line_offsets = array('I') self._line_offsets.append(0) diff --git a/elpy/rpc.py b/elpy/rpc.py index 30f933146..1c9255e35 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -11,21 +11,23 @@ import sys import traceback from pydantic import BaseModel -from typing import Union, Optional, List, Type +from typing import Union, Optional, List, Dict class Result(BaseModel): pass +class ServerMsg(BaseModel): + pass -class ErrorMsg(BaseModel): +class ErrorMsg(ServerMsg): id: int - error: Union[dict, Type[Result]] + error: Union[dict, Result] -class ResponceMsg(BaseModel): +class ResponceMsg(ServerMsg): id: int - result: Union[dict, List, str, Type[Result]] + result: Union[dict, List, str, Result] class JSONRPCServer(object): @@ -72,7 +74,7 @@ def __init__(self, stdin=None, stdout=None): else: self.stdout = stdout - def read_json(self) -> str: + def read_json(self) -> Dict: """Read a single line and decode it as JSON. Can raise an EOFError() when the input source was closed. @@ -83,13 +85,13 @@ def read_json(self) -> str: raise EOFError() return json.loads(line) - def send_msg(self, msg: Result) -> None: + def send_msg(self, msg: ServerMsg) -> None: """Write an JSON object on a single line. """ self.stdout.write(msg.json() + '\n') self.stdout.flush() - def handle_request(self) -> Type[Result]: + def handle_request(self) -> None: """Handle a single JSON-RPC request. Read a request, call the appropriate handler method, and @@ -111,7 +113,7 @@ def handle_request(self) -> Type[Result]: self.send_msg(msg) def _make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[Type[Result]]: + ) -> Optional[ServerMsg]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -134,7 +136,7 @@ def _make_msg(self, request_id: str, method_name: str, params: List return ErrorMsg(error=error, id=request_id) def handle(self, method_name: str, args: List - ) -> Optional[Type[Result]]: + ) -> Optional[Result]: """Handle the call to method_name. You should overwrite this method in a subclass. From d67beb371f0777a4826f42e9ab31a65c76beb779 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sat, 9 Jan 2021 07:54:16 +0700 Subject: [PATCH 13/24] update type hints --- elpy/jedibackend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 9bd414f09..e02542880 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -110,7 +110,7 @@ def rpc_get_completions(self, filename, source, offset): 'meta': proposal.description} for proposal in proposals] - def rpc_get_completion_docstring(self, completion) -> Optional[Any]: + def rpc_get_completion_docstring(self, completion) -> Optional[str]: proposal = self.completions.get(completion) if proposal is not None: return proposal.docstring(fast=False) @@ -121,7 +121,7 @@ def rpc_get_completion_location(self, completion): return (str(proposal.module_path), proposal.line) def rpc_get_docstring(self, filename: str, source: str, offset: int - ) -> Optional[Any]: + ) -> Optional[str]: src = SourceCode(filename, source) line, column = src.get_pos(offset) locations = run_with_debug(jedi, 'goto', From e5f22f498cd5f4431d277f1552180c3d7373d46c Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Thu, 14 Jan 2021 03:35:43 +0700 Subject: [PATCH 14/24] refactoring --- elpy/jedibackend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index e02542880..59eb417fc 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -44,7 +44,7 @@ class RefactoringResult(Result): error_msg: Optional[str] @classmethod - def fail(cls, error_msg=""): + def fail(cls, error_msg="") -> RefactoringResult: return cls( success=False, project_path=Path(), diff="", changed_files=[], error_msg=error_msg) @@ -440,9 +440,9 @@ def get_pos(self, offset: int) -> Pos: """ idx = self._get_line_offsets() - line_num = bisect_right(idx, offset) - 1 - line_offset = idx[line_num] - return Pos(line_num + self._line_start, + line_num_from_zero = bisect_right(idx, offset) - self._line_start + line_offset = idx[line_num_from_zero] + return Pos(line_num_from_zero + self._line_start, offset - line_offset + self._col_start) def get_offset(self, line: int, col: int): From bbad917b2b5bda44ed4b07802e29b0fec063ee72 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 31 Jan 2021 20:13:34 +0700 Subject: [PATCH 15/24] Add pydantic package in elpy-rpc--get-package-list for eply-rpc venv builed by emacs. --- elpy-rpc.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elpy-rpc.el b/elpy-rpc.el index 0d46aabb5..ef23b759b 100644 --- a/elpy-rpc.el +++ b/elpy-rpc.el @@ -229,8 +229,8 @@ needed packages from `elpy-rpc--get-package-list'." "Return the list of packages to be installed in the RPC virtualenv." (let ((rpc-python-version (elpy-rpc--get-python-version))) (if (version< rpc-python-version "3.6.0") - '("jedi" "flake8" "autopep8" "yapf" "rope") - '("jedi" "flake8" "autopep8" "yapf" "black" "rope")))) + '("jedi" "flake8" "autopep8" "yapf" "rope" "pydantic") + '("jedi" "flake8" "autopep8" "yapf" "black" "rope" "pydantic")))) (defun elpy-rpc--get-python-version () "Return the RPC python version." From c5037bf99c5cc1c1464c322d77611a21af4cf84e Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 31 Jan 2021 21:18:20 +0700 Subject: [PATCH 16/24] move SourceCode class to utils.py --- elpy/jedibackend.py | 101 ++------------------------------- elpy/tests/test_jedibackend.py | 2 +- elpy/utils.py | 89 +++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 96 deletions(-) create mode 100644 elpy/utils.py diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 59eb417fc..b841187d1 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -11,10 +11,6 @@ import traceback import re -from array import array -from io import StringIO -from bisect import bisect_right - from typing import List, Optional, Union, NamedTuple, Any, NoReturn from pathlib import Path import jedi @@ -22,6 +18,8 @@ from elpy import rpc from elpy.rpc import Fault, Result +from elpy.utils import SourceCode + class NameResult(Result): name: str @@ -63,7 +61,6 @@ def from_refactoring( # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). - try: from pkg_resources import parse_version except ImportError: # pragma: no cover @@ -72,6 +69,7 @@ def parse_version(*arg, **kwargs) -> NoReturn: "please reinstall Elpy RPC virtualenv with" " `M-x elpy-rpc-reinstall-virtualenv`", code=400) + class JediBackend(object): """The Jedi backend class. @@ -83,7 +81,6 @@ class JediBackend(object): name = "jedi" def __init__(self, project_root, environment_binaries_path): - self.project_root = project_root self.environment = None if environment_binaries_path is not None: @@ -94,7 +91,7 @@ def __init__(self, project_root, environment_binaries_path): Fault(message=str(e)) self.completions = {} sys.path.append(project_root) - + def rpc_get_completions(self, filename, source, offset): src = SourceCode(filename, source) line, column = src.get_pos(offset) @@ -177,11 +174,9 @@ def rpc_get_definition(self, filename, source, offset): return None return (str(loc.module_path), offset) - def rpc_get_assignment(self, filename, source, offset): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.") - def rpc_get_calltip(self, filename, source, offset): line, column = pos_to_linecol(source, offset) calls = run_with_debug(jedi, 'get_signatures', @@ -203,6 +198,7 @@ def rpc_get_calltip_or_oneline_docstring(self, filename, source, offset): Return the current function calltip or its oneline docstring. Meant to be used with eldoc. + """ # Try to get a oneline docstring then docs = self.rpc_get_oneline_docstring(filename=filename, @@ -330,7 +326,7 @@ def rpc_get_names(self, filename, source, offset) -> List[Result]: offset = other_src.get_offset(name.line, name.column) result.append(NameResult.from_name(name, offset)) return result - + def rpc_get_rename_diff(self, filename, source, offset, new_name): """Get the diff resulting from renaming the thing at point""" src = SourceCode(filename, source) @@ -393,91 +389,6 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Result: return RefactoringResult.from_refactoring(ref) -# 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. -class Pos(NamedTuple): - line: int - col: int - -class SourceCode: - _source: Optional[str] - _path: Optional[Path] - _line_offsets: Optional[array[int]] - - def __init__( - self, path: Union[None, str, Path], source: Optional[str] = None, - line_start: int = 1, col_start: int = 0): - if path is not None: - self._path = Path(path) - self._source = source - self._line_offsets = None - self._line_start = line_start - self._col_start = col_start - - def get_source(self): - if self._source is None: - with open(self._path, 'r') as fh: - self._source = fh.read() - return self._source - - def __str__(self): - return self.get_source() - - @property - def path(self) -> Optional[Path]: - return self._path - - def get_pos(self, offset: int) -> 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. - - """ - idx = self._get_line_offsets() - line_num_from_zero = bisect_right(idx, offset) - self._line_start - line_offset = idx[line_num_from_zero] - return Pos(line_num_from_zero + self._line_start, - offset - line_offset + self._col_start) - - def get_offset(self, line: int, col: int): - """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. - - """ - line_offsets = self._get_line_offsets() - line_from_zero = line - self._line_start - col_from_zero = col - self._col_start - try: - cur_line_offset = line_offsets[line_from_zero] - except IndexError: - raise ValueError(f"Text does not have {line} lines.") - offset = cur_line_offset + col_from_zero - next_line_offset = line_offsets[line_from_zero + 1] - if not offset < next_line_offset: - raise ValueError( - f"Line {line} column {col} is not within the text") - return offset - - def _get_line_offsets(self) -> array[int]: - if self._line_offsets is None: - self._line_offsets = array('I') - self._line_offsets.append(0) - curr_line_offset = 0 - for line in StringIO(self.get_source()): - curr_line_offset += len(line) - self._line_offsets.append(curr_line_offset) - return self._line_offsets - - def pos_to_linecol(text, pos): """Return a tuple of line and column for offset pos in text. diff --git a/elpy/tests/test_jedibackend.py b/elpy/tests/test_jedibackend.py index 9602bbccd..bca6fe974 100644 --- a/elpy/tests/test_jedibackend.py +++ b/elpy/tests/test_jedibackend.py @@ -9,7 +9,7 @@ import re from elpy import jedibackend -from elpy.jedibackend import SourceCode +from elpy.utils import SourceCode from elpy import rpc from elpy.tests import compat from elpy.tests.support import BackendTestCase diff --git a/elpy/utils.py b/elpy/utils.py new file mode 100644 index 000000000..6b8b8a168 --- /dev/null +++ b/elpy/utils.py @@ -0,0 +1,89 @@ +from typing import Optional, Union, NamedTuple +from pathlib import Path +from array import array +from bisect import bisect_right +from io import StringIO + + +# 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. +class Pos(NamedTuple): + line: int + col: int + + +class SourceCode: + _source: Optional[str] + _path: Optional[Path] + _line_offsets: Optional[array] + + def __init__( + self, path: Union[None, str, Path], source: Optional[str] = None, + line_start: int = 1, col_start: int = 0): + if path is not None: + self._path = Path(path) + self._source = source + self._line_offsets = None + self._line_start = line_start + self._col_start = col_start + + def get_source(self): + if self._source is None: + with open(self._path, 'r') as fh: + self._source = fh.read() + return self._source + + def __str__(self): + return self.get_source() + + @property + def path(self) -> Optional[Path]: + return self._path + + def get_pos(self, offset: int) -> 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. + """ + idx = self._get_line_offsets() + line_num_from_zero = bisect_right(idx, offset) - self._line_start + line_offset = idx[line_num_from_zero] + return Pos(line_num_from_zero + self._line_start, + offset - line_offset + self._col_start) + + def get_offset(self, line: int, col: int): + """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. + """ + line_offsets = self._get_line_offsets() + line_from_zero = line - self._line_start + col_from_zero = col - self._col_start + try: + cur_line_offset = line_offsets[line_from_zero] + except IndexError: + raise ValueError(f"Text does not have {line} lines.") + offset = cur_line_offset + col_from_zero + next_line_offset = line_offsets[line_from_zero + 1] + if not offset < next_line_offset: + raise ValueError( + f"Line {line} column {col} is not within the text") + return offset + + def _get_line_offsets(self) -> array: + if self._line_offsets is None: + self._line_offsets = array('I') + self._line_offsets.append(0) + curr_line_offset = 0 + for line in StringIO(self.get_source()): + curr_line_offset += len(line) + self._line_offsets.append(curr_line_offset) + return self._line_offsets From 2d2383558b473fbd6afa51d3c5483284b8a5d62c Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 7 Feb 2021 17:17:10 +0700 Subject: [PATCH 17/24] make _make_msg private --- elpy/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elpy/rpc.py b/elpy/rpc.py index 1c9255e35..73d520502 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -108,11 +108,11 @@ def handle_request(self) -> None: method_name = request['method'] request_id = request.get('id', None) params = request.get('params') or [] - msg = self._make_msg(request_id, method_name, params) + msg = self.__make_msg(request_id, method_name, params) if msg is not None: self.send_msg(msg) - def _make_msg(self, request_id: str, method_name: str, params: List + def __make_msg(self, request_id: str, method_name: str, params: List ) -> Optional[ServerMsg]: try: method = getattr(self, "rpc_" + method_name, None) From 4695b6085a0307354bb9113870ba4b4ddd0180ca Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 18:02:30 +0700 Subject: [PATCH 18/24] refactoring jedibackend.py --- elpy/jedibackend.py | 71 ++++++++++++++++++++++----------------------- elpy/rpc.py | 5 ++-- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index b841187d1..4fbe04498 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -11,7 +11,7 @@ import traceback import re -from typing import List, Optional, Union, NamedTuple, Any, NoReturn +from typing import List, Optional, Union, Tuple, NamedTuple, Any, NoReturn from pathlib import Path import jedi @@ -26,13 +26,6 @@ class NameResult(Result): offset: int filename: str - @classmethod - def from_name( - cls, x: jedi.api.classes.Name, offset: int) -> NameResult: - if type(x) is not jedi.api.classes.Name: - raise TypeError("Must be jedi.api.classes.Name") - return cls(name=x.name, filename=str(x.module_path), offset=offset) - class RefactoringResult(Result): success: bool @@ -41,23 +34,6 @@ class RefactoringResult(Result): changed_files: List[Path] error_msg: Optional[str] - @classmethod - def fail(cls, error_msg="") -> RefactoringResult: - return cls( - success=False, project_path=Path(), diff="", changed_files=[], - error_msg=error_msg) - - @classmethod - def from_refactoring( - cls, x: jedi.api.refactoring.Refactoring) -> RefactoringResult: - if type(x) is not jedi.api.refactoring.Refactoring: - raise TypeError("Must be jedi.api.refactoring.Refactoring type") - return cls( - success=True, - project_path=x._inference_state.project._path, - diff=x.get_diff(), - changed_files=list(x.get_changed_files().keys())) - # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). @@ -70,7 +46,7 @@ def parse_version(*arg, **kwargs) -> NoReturn: " `M-x elpy-rpc-reinstall-virtualenv`", code=400) -class JediBackend(object): +class JediBackend: """The Jedi backend class. Implements the RPC calls we can pass on to Jedi. @@ -92,6 +68,27 @@ def __init__(self, project_root, environment_binaries_path): self.completions = {} sys.path.append(project_root) + def _name_result(self, x: jedi.api.classes.Name, offset: int) -> NameResult: + if type(x) is not jedi.api.classes.Name: + raise TypeError('Must be jedi.api.classes.Name') + return NameResult( + name=x.name, filename=str(x.module_path), offset=offset) + + def _refactoring_fail(self, error_msg='') -> RefactoringResult: + return RefactoringResult( + success=False, project_path=Path(), diff="", changed_files=[], + error_msg=error_msg) + + def _refactoring_result(self, x: jedi.api.refactoring.Refactoring + ) -> RefactoringResult: + if type(x) is not jedi.api.refactoring.Refactoring: + raise TypeError("Must be jedi.api.refactoring.Refactoring type") + return RefactoringResult( + success=True, + project_path=x._inference_state.project._path, + diff=x.get_diff(), + changed_files=list(x.get_changed_files().keys())) + def rpc_get_completions(self, filename, source, offset): src = SourceCode(filename, source) line, column = src.get_pos(offset) @@ -112,7 +109,7 @@ def rpc_get_completion_docstring(self, completion) -> Optional[str]: if proposal is not None: return proposal.docstring(fast=False) - def rpc_get_completion_location(self, completion): + def rpc_get_completion_location(self, completion) -> Optional[Tuple]: proposal = self.completions.get(completion) if proposal is not None: return (str(proposal.module_path), proposal.line) @@ -304,7 +301,7 @@ def rpc_get_usages(self, filename, source, offset) -> List[NameResult]: elif name.module_path is not None: other_src = SourceCode(name.module_path) offset = other_src.get_offset(name.line, name.column) - result.append(NameResult.from_name(name, offset)) + result.append(self._name_result(name, offset)) return result def rpc_get_names(self, filename, source, offset) -> List[Result]: @@ -324,7 +321,7 @@ def rpc_get_names(self, filename, source, offset) -> List[Result]: elif name.module_path is not None: other_src = SourceCode(name.module_path) offset = other_src.get_offset(name.line, name.column) - result.append(NameResult.from_name(name, offset)) + result.append(self._name_result(name, offset)) return result def rpc_get_rename_diff(self, filename, source, offset, new_name): @@ -338,8 +335,8 @@ def rpc_get_rename_diff(self, filename, source, offset, new_name): column=column, new_name=new_name) except Exception as e: - return RefactoringResult.fail(error_msg=str(e)) - return RefactoringResult.from_refactoring(ref) + return self._refactoring_fail(error_msg=str(e)) + return self._refactoring_result(ref) def rpc_get_extract_variable_diff( self, filename, source, offset, new_name, @@ -354,9 +351,9 @@ def rpc_get_extract_variable_diff( 'until_column': col_end, 'new_name': new_name}) if ref is None: - return RefactoringResult.fail() + return self._refactoring_fail() else: - return RefactoringResult.from_refactoring(ref) + return self._refactoring_result(ref) def rpc_get_extract_function_diff( self, filename, source, offset, new_name, @@ -371,8 +368,8 @@ def rpc_get_extract_function_diff( until_column=col_end, new_name=new_name) except Exception as e: - return RefactoringResult.fail(error_msg=str(e)) - return RefactoringResult.from_refactoring(ref) + return self._refactoring_fail(error_msg=str(e)) + return self._refactoring_result(ref) def rpc_get_inline_diff(self, filename, source, offset) -> Result: """Get the diff resulting from inlining the selected variable""" @@ -384,9 +381,9 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Result: fun_kwargs={'line': line, 'column': column}) if ref is None: - return RefactoringResult.fail() + return self._refactoring_fail() else: - return RefactoringResult.from_refactoring(ref) + return self._refactoring_result(ref) def pos_to_linecol(text, pos): diff --git a/elpy/rpc.py b/elpy/rpc.py index 73d520502..37bdc9d99 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -10,8 +10,9 @@ import json import sys import traceback +from typing import Dict, List, Optional, Union + from pydantic import BaseModel -from typing import Union, Optional, List, Dict class Result(BaseModel): @@ -113,7 +114,7 @@ def handle_request(self) -> None: self.send_msg(msg) def __make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[ServerMsg]: + ) -> Optional[ServerMsg]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: From 7c0bfeb1ef3c40f742c13e85be65dea0247faeb1 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 21:23:21 +0700 Subject: [PATCH 19/24] fix docstring --- elpy/jedibackend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index 4fbe04498..be3fea2db 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -51,7 +51,7 @@ class JediBackend: Implements the RPC calls we can pass on to Jedi. - Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html + Documentation: https://jedi.readthedocs.io/en/latest/docs/api.html """ name = "jedi" From 63a894160b5dfe535fb0fd2b2fc27f465cdb68de Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 21:52:34 +0700 Subject: [PATCH 20/24] create api package --- elpy/api.py | 36 ++++++++++++++++++++++++++++++++++++ elpy/jedibackend.py | 17 ++--------------- elpy/rpc.py | 21 +++------------------ elpy/tests/test_rpc.py | 2 +- 4 files changed, 42 insertions(+), 34 deletions(-) create mode 100644 elpy/api.py diff --git a/elpy/api.py b/elpy/api.py new file mode 100644 index 000000000..70993e44d --- /dev/null +++ b/elpy/api.py @@ -0,0 +1,36 @@ +from pathlib import Path +from typing import Any, List, NamedTuple, NoReturn, Optional, Tuple, Union + +from pydantic import BaseModel + + +class Result(BaseModel): + pass + + +class ServerMsg(BaseModel): + pass + + +class NameResult(Result): + name: str + offset: int + filename: str + + +class RefactoringResult(Result): + success: bool + project_path: Path + diff: str + changed_files: List[Path] + error_msg: Optional[str] + + +class ErrorMsg(ServerMsg): + id: int + error: Union[dict, Result] + + +class ResponceMsg(ServerMsg): + id: int + result: Union[dict, List, str, Result] diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index be3fea2db..f244e47bb 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -17,24 +17,11 @@ from elpy import rpc -from elpy.rpc import Fault, Result +from elpy.rpc import Fault +from elpy.api import NameResult, RefactoringResult, Result from elpy.utils import SourceCode -class NameResult(Result): - name: str - offset: int - filename: str - - -class RefactoringResult(Result): - success: bool - project_path: Path - diff: str - changed_files: List[Path] - error_msg: Optional[str] - - # in case pkg_resources is not properly installed # (see https://github.com/jorgenschaefer/elpy/issues/1674). try: diff --git a/elpy/rpc.py b/elpy/rpc.py index 37bdc9d99..000f9001d 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -12,24 +12,9 @@ import traceback from typing import Dict, List, Optional, Union -from pydantic import BaseModel - - -class Result(BaseModel): - pass - -class ServerMsg(BaseModel): - pass - -class ErrorMsg(ServerMsg): - id: int - error: Union[dict, Result] - - -class ResponceMsg(ServerMsg): - id: int - result: Union[dict, List, str, Result] - +from elpy.api import ServerMsg +from elpy.api import ErrorMsg, ResponceMsg +from elpy.api import Result class JSONRPCServer(object): """Simple JSON-RPC-like server. diff --git a/elpy/tests/test_rpc.py b/elpy/tests/test_rpc.py index db65bde57..400d807e8 100644 --- a/elpy/tests/test_rpc.py +++ b/elpy/tests/test_rpc.py @@ -5,7 +5,7 @@ import sys from elpy import rpc -from elpy.rpc import Result, ErrorMsg, ResponceMsg +from elpy.api import Result, ErrorMsg, ResponceMsg from elpy.tests.compat import StringIO From b73003adf1d5ee2d8df3f5a91a521793aa1b6907 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 22:29:16 +0700 Subject: [PATCH 21/24] refactoring rpc.py --- elpy/rpc.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/elpy/rpc.py b/elpy/rpc.py index 000f9001d..112dafadc 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -94,12 +94,12 @@ def handle_request(self) -> None: method_name = request['method'] request_id = request.get('id', None) params = request.get('params') or [] - msg = self.__make_msg(request_id, method_name, params) + msg = self._make_msg(request_id, method_name, params) if msg is not None: self.send_msg(msg) - def __make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[ServerMsg]: + def _make_msg(self, request_id: str, method_name: str, params: List + ) -> Optional[ServerMsg]: try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -109,18 +109,21 @@ def __make_msg(self, request_id: str, method_name: str, params: List if request_id is not None and result is not None: return ResponceMsg(result=result, id=request_id) except Fault as fault: - error = {"message": fault.message, - "code": fault.code} - if fault.data is not None: - error["data"] = fault.data - return ErrorMsg(error=error, id=request_id) + return self._make_error_msg(id=request_id, msg=fault.message, + code=fault.code, data=fault.data) except Exception as e: - error = {"message": str(e), - "code": 500, - "data": {"traceback": traceback.format_exc()} - } - return ErrorMsg(error=error, id=request_id) - + return self._make_error_msg( + id=request_id, msg=str(e), code=500, + data={"traceback": traceback.format_exc()}) + + def _make_error_msg(self, id: int, msg: str, code: int, data=Optional[dict] + ) -> ErrorMsg: + error = {"message": msg, + "code": code, + "data": '' if data is None else data, + } + return ErrorMsg(error=error, id=id) + def handle(self, method_name: str, args: List ) -> Optional[Result]: """Handle the call to method_name. From 837247102ab36e87c92d5e5a0b664ecd42f1bc54 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 23:26:42 +0700 Subject: [PATCH 22/24] refactoring rpc.py --- elpy/rpc.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/elpy/rpc.py b/elpy/rpc.py index 112dafadc..7c508ca20 100644 --- a/elpy/rpc.py +++ b/elpy/rpc.py @@ -94,12 +94,7 @@ def handle_request(self) -> None: method_name = request['method'] request_id = request.get('id', None) params = request.get('params') or [] - msg = self._make_msg(request_id, method_name, params) - if msg is not None: - self.send_msg(msg) - - def _make_msg(self, request_id: str, method_name: str, params: List - ) -> Optional[ServerMsg]: + msg = None try: method = getattr(self, "rpc_" + method_name, None) if method is not None: @@ -107,14 +102,16 @@ def _make_msg(self, request_id: str, method_name: str, params: List else: result = self.handle(method_name, params) if request_id is not None and result is not None: - return ResponceMsg(result=result, id=request_id) + msg = ResponceMsg(result=result, id=request_id) except Fault as fault: - return self._make_error_msg(id=request_id, msg=fault.message, - code=fault.code, data=fault.data) + msg = self._make_error_msg(id=request_id, msg=fault.message, + code=fault.code, data=fault.data) except Exception as e: - return self._make_error_msg( + msg = self._make_error_msg( id=request_id, msg=str(e), code=500, data={"traceback": traceback.format_exc()}) + if msg is not None: + self.send_msg(msg) def _make_error_msg(self, id: int, msg: str, code: int, data=Optional[dict] ) -> ErrorMsg: From 94a6f6f44096c3666fa75ac0f324827df54eed2a Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Sun, 28 Feb 2021 23:43:52 +0700 Subject: [PATCH 23/24] remove jedibackend.pos_to_linecol --- elpy/jedibackend.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/elpy/jedibackend.py b/elpy/jedibackend.py index f244e47bb..20f683d6f 100644 --- a/elpy/jedibackend.py +++ b/elpy/jedibackend.py @@ -124,7 +124,6 @@ def rpc_get_docstring(self, filename: str, source: str, offset: int locations[-1].full_name) + locations[-1].docstring()) def rpc_get_definition(self, filename, source, offset): - line, column = pos_to_linecol(source, offset) src = SourceCode(filename, source) line, column = src.get_pos(offset) locations = run_with_debug(jedi, 'goto', @@ -162,7 +161,8 @@ def rpc_get_assignment(self, filename, source, offset): raise Fault("Obsolete since jedi 17.0. Please use 'get_definition'.") def rpc_get_calltip(self, filename, source, offset): - line, column = pos_to_linecol(source, offset) + src = SourceCode(filename, source) + line, column = src.get_pos(offset) calls = run_with_debug(jedi, 'get_signatures', code=source, path=filename, @@ -373,20 +373,6 @@ def rpc_get_inline_diff(self, filename, source, offset) -> Result: return self._refactoring_result(ref) -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 run_with_debug(jedi, name, fun_kwargs={}, *args, **kwargs): re_raise = kwargs.pop('re_raise', ()) try: From ec96294706650268499f8bef9e8d7d0a85b6f435 Mon Sep 17 00:00:00 2001 From: "Fedor P. Goncharov" Date: Mon, 1 Mar 2021 00:08:55 +0700 Subject: [PATCH 24/24] dropped unnecessary changes in elpy-refactor.el --- elpy-refactor.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/elpy-refactor.el b/elpy-refactor.el index c64b7c2cc..19193b159 100644 --- a/elpy-refactor.el +++ b/elpy-refactor.el @@ -202,11 +202,11 @@ do not display the diff before applying." (diff (elpy-rpc-get-rename-diff new-name)) (proj-path (alist-get 'project_path diff)) (success (alist-get 'success diff)) - (error-msg (alist-get 'error_msg diff)) - (error-type (alist-get 'error_type diff)) (diff (alist-get 'diff diff))) (cond ((not success) - (error "Elpy-RPC Error (%s): %s" error-type error-msg)) + (error "Refactoring failed for some reason")) + ((string= success "Not available") + (error "This functionnality needs jedi > 0.17.0, please update")) ((or dontask current-prefix-arg) (message "Replacing '%s' with '%s'..." (thing-at-point 'symbol)