From 13b393a5e32ae4bbff84729ea8882ac79aa55360 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 10 Feb 2020 17:42:23 +0100 Subject: [PATCH] Get the first rename test passing --- jedi/api/__init__.py | 31 +++++++++++++++++++ jedi/api/exceptions.py | 4 +++ jedi/api/refactoring.py | 64 ++++++++++++++++++++++++++++++++++++++++ test/refactor.py | 42 ++++++++------------------ test/refactor/rename.py | 20 ++++++++----- test/test_integration.py | 4 +-- 6 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 jedi/api/refactoring.py diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 6630ead5..8574ee5f 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -523,6 +523,37 @@ class Script(object): ] return sorted(filter(def_ref_filter, defs), key=lambda x: (x.line, x.column)) + def rename(self, line, column, new_name): + """ + Returns an object that you can use to rename the variable under the + cursor and its references to a different name. + """ + definitions = self.get_references(line, column, include_builtins=False) + file_renames = [] + file_tree_name_map = {} + for d in definitions: + if d.type == 'module': + file_renames.append((d.module_path, new)) + else: + # This private access is ok in a way. It's not public to + # protect Jedi users from seeing it. + tree_name = d._name.tree_name + if tree_name is not None: + fmap = file_tree_name_map.setdefault(d.module_path, {}) + fmap[tree_name] = tree_name.prefix + new_name + from jedi.api.refactoring import Refactoring + return Refactoring(self._grammar, file_tree_name_map, file_renames) + + def extract_variable(self): + """ + """ + raise NotImplementedError + + def extract_method(self): + """ + """ + raise NotImplementedError + class Interpreter(Script): """ diff --git a/jedi/api/exceptions.py b/jedi/api/exceptions.py index 99cebdb7..e0507888 100644 --- a/jedi/api/exceptions.py +++ b/jedi/api/exceptions.py @@ -8,3 +8,7 @@ class InternalError(_JediError): class WrongVersion(_JediError): pass + + +class RefactoringError(_JediError): + pass diff --git a/jedi/api/refactoring.py b/jedi/api/refactoring.py new file mode 100644 index 00000000..b88b193b --- /dev/null +++ b/jedi/api/refactoring.py @@ -0,0 +1,64 @@ +import difflib + +from parso import split_lines + + +class ChangedFile(object): + def __init__(self, grammar, path, module_node, node_to_str_map): + self._grammar = grammar + self._path = path + self._module_node = module_node + self._node_to_str_map = node_to_str_map + + def get_diff(self): + old_lines = split_lines(self._module_node.get_code(), keepends=True) + new_lines = split_lines(self.get_code(), keepends=True) + diff = difflib.unified_diff( + old_lines, new_lines, + fromfile=self._path, + tofile=self._path + ) + # Apparently there's a space at the end of the diff - for whatever + # reason. + return ''.join(diff).rstrip(' ') + + def get_code(self): + return self._grammar.refactor(self._module_node, self._node_to_str_map) + + def apply(self): + with open(self._path, 'w') as f: + f.write(self.get_code()) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self._path) + + +class Refactoring(object): + def __init__(self, grammar, file_to_node_changes, renames=()): + self._grammar = grammar + self._renames = renames + self._file_to_node_changes = file_to_node_changes + + def get_changed_files(self): + return [ + ChangedFile(self._grammar, path, next(iter(map_)).get_root_node(), map_) + for path, map_ in self._file_to_node_changes.items() + ] + + def get_renames(self): + """ + Files can be renamed in a refactoring. + + Returns ``Iterable[Tuple[str, str]]``. + """ + return self._renames + + def get_diff(self): + return ''.join(f.get_diff() for f in self.get_changed_files()) + + def apply(self): + for old, new in self._renames: + rename(old, new) + + for f in self.get_changed_files(): + f.apply() diff --git a/test/refactor.py b/test/refactor.py index 6d26020b..9185ca8b 100755 --- a/test/refactor.py +++ b/test/refactor.py @@ -16,14 +16,13 @@ import jedi class RefactoringCase(object): def __init__(self, name, code, line_nr, index, path, - new_name, start_line_test, desired): + args, desired): self.name = name self.code = code self.line_nr = line_nr self.index = index self.path = path - self.new_name = new_name - self.start_line_test = start_line_test + self._args = args self.desired = desired @property @@ -32,29 +31,13 @@ class RefactoringCase(object): return f_name.replace('.py', '') def refactor(self): - script = jedi.Script(self.code, self.line_nr, self.index, self.path) + script = jedi.Script(self.code, path=self.path) refactor_func = getattr(script, self.refactor_type) - args = (self.new_name,) if self.new_name else () - return refactor_func(script, *args) + return refactor_func(self.line_nr, self.index, *self._args) def run(self): refactor_object = self.refactor() - - # try to get the right excerpt of the newfile - f = refactor_object.new_files()[self.path] - lines = f.splitlines()[self.start_line_test:] - - end = self.start_line_test + len(lines) - pop_start = None - for i, l in enumerate(lines): - if l.startswith('# +++'): - end = i - break - elif '#? ' in l: - pop_start = i - lines.pop(pop_start) - self.result = '\n'.join(lines[:end - 1]).strip() - return self.result + return refactor_object.get_diff() def check(self): return self.run() == self.desired @@ -65,13 +48,12 @@ class RefactoringCase(object): def collect_file_tests(code, path, lines_to_execute): - r = r'^# --- ?([^\n]*)\n((?:(?!\n# \+\+\+).)*)' \ - r'\n# \+\+\+((?:(?!\n# ---).)*)' + r = r'^# -{5} ?([^\n]*)\n((?:(?!\n# \+{5}).)*\n)' \ + r'# \+{5}\n((?:(?!\n# -{5}).)*\n)' for match in re.finditer(r, code, re.DOTALL | re.MULTILINE): name = match.group(1).strip() - first = match.group(2).strip() - second = match.group(3).strip() - start_line_test = code[:match.start()].count('\n') + 1 + first = match.group(2) + second = match.group(3) # get the line with the position of the operation p = re.match(r'((?:(?!#\?).)*)#\? (\d*) ?([^\n]*)', first, re.DOTALL) @@ -81,13 +63,13 @@ def collect_file_tests(code, path, lines_to_execute): until = p.group(1) index = int(p.group(2)) new_name = p.group(3) + args = (new_name,) if new_name else () - line_nr = start_line_test + until.count('\n') + 2 + line_nr = until.count('\n') + 2 if lines_to_execute and line_nr - 1 not in lines_to_execute: continue - yield RefactoringCase(name, code, line_nr, index, path, - new_name, start_line_test, second) + yield RefactoringCase(name, first, line_nr, index, path, args, second) def collect_dir_tests(base_dir, test_files): diff --git a/test/refactor/rename.py b/test/refactor/rename.py index c717eb04..a79699ee 100644 --- a/test/refactor/rename.py +++ b/test/refactor/rename.py @@ -3,15 +3,21 @@ Test coverage for renaming is mostly being done by testing `Script.get_references`. """ -# --- simple +# ----- simple def test1(): #? 7 blabla test1() AssertionError return test1, test1.not_existing -# +++ -def blabla(): - blabla() - AssertionError - return blabla, blabla.not_existing - +# +++++ +--- /home/dave/source/jedi/test/refactor/rename.py ++++ /home/dave/source/jedi/test/refactor/rename.py +@@ -1,6 +1,6 @@ +-def test1(): ++def blabla(): + #? 7 blabla +- test1() ++ blabla() + AssertionError +- return test1, test1.not_existing ++ return blabla, blabla.not_existing diff --git a/test/test_integration.py b/test/test_integration.py index 6652ef2f..c61f944b 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -58,5 +58,5 @@ def test_refactor(refactor_case): :type refactor_case: :class:`.refactor.RefactoringCase` """ - refactor_case.run() - assert_case_equal(refactor_case, refactor_case.result, refactor_case.desired) + diff = refactor_case.run() + assert_case_equal(refactor_case, diff, refactor_case.desired)