Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/change_signature.py
1494 views
import copy12import rope.base.exceptions3from rope.base import pyobjects, taskhandle, evaluate, worder, codeanalyze, utils4from rope.base.change import ChangeContents, ChangeSet5from rope.refactor import occurrences, functionutils678class ChangeSignature(object):910def __init__(self, project, resource, offset):11self.pycore = project.pycore12self.resource = resource13self.offset = offset14self._set_name_and_pyname()15if self.pyname is None or self.pyname.get_object() is None or \16not isinstance(self.pyname.get_object(), pyobjects.PyFunction):17raise rope.base.exceptions.RefactoringError(18'Change method signature should be performed on functions')1920def _set_name_and_pyname(self):21self.name = worder.get_name_at(self.resource, self.offset)22this_pymodule = self.pycore.resource_to_pyobject(self.resource)23self.primary, self.pyname = evaluate.eval_location2(24this_pymodule, self.offset)25if self.pyname is None:26return27pyobject = self.pyname.get_object()28if isinstance(pyobject, pyobjects.PyClass) and \29'__init__' in pyobject:30self.pyname = pyobject['__init__']31self.name = '__init__'32pyobject = self.pyname.get_object()33self.others = None34if self.name == '__init__' and \35isinstance(pyobject, pyobjects.PyFunction) and \36isinstance(pyobject.parent, pyobjects.PyClass):37pyclass = pyobject.parent38self.others = (pyclass.get_name(),39pyclass.parent[pyclass.get_name()])4041def _change_calls(self, call_changer, in_hierarchy=None, resources=None,42handle=taskhandle.NullTaskHandle()):43if resources is None:44resources = self.pycore.get_python_files()45changes = ChangeSet('Changing signature of <%s>' % self.name)46job_set = handle.create_jobset('Collecting Changes', len(resources))47finder = occurrences.create_finder(48self.pycore, self.name, self.pyname, instance=self.primary,49in_hierarchy=in_hierarchy and self.is_method())50if self.others:51name, pyname = self.others52constructor_finder = occurrences.create_finder(53self.pycore, name, pyname, only_calls=True)54finder = _MultipleFinders([finder, constructor_finder])55for file in resources:56job_set.started_job(file.path)57change_calls = _ChangeCallsInModule(58self.pycore, finder, file, call_changer)59changed_file = change_calls.get_changed_module()60if changed_file is not None:61changes.add_change(ChangeContents(file, changed_file))62job_set.finished_job()63return changes6465def get_args(self):66"""Get function arguments.6768Return a list of ``(name, default)`` tuples for all but star69and double star arguments. For arguments that don't have a70default, `None` will be used.71"""72return self._definfo().args_with_defaults7374def is_method(self):75pyfunction = self.pyname.get_object()76return isinstance(pyfunction.parent, pyobjects.PyClass)7778@utils.deprecated('Use `ChangeSignature.get_args()` instead')79def get_definition_info(self):80return self._definfo()8182def _definfo(self):83return functionutils.DefinitionInfo.read(self.pyname.get_object())8485@utils.deprecated()86def normalize(self):87changer = _FunctionChangers(88self.pyname.get_object(), self.get_definition_info(),89[ArgumentNormalizer()])90return self._change_calls(changer)9192@utils.deprecated()93def remove(self, index):94changer = _FunctionChangers(95self.pyname.get_object(), self.get_definition_info(),96[ArgumentRemover(index)])97return self._change_calls(changer)9899@utils.deprecated()100def add(self, index, name, default=None, value=None):101changer = _FunctionChangers(102self.pyname.get_object(), self.get_definition_info(),103[ArgumentAdder(index, name, default, value)])104return self._change_calls(changer)105106@utils.deprecated()107def inline_default(self, index):108changer = _FunctionChangers(109self.pyname.get_object(), self.get_definition_info(),110[ArgumentDefaultInliner(index)])111return self._change_calls(changer)112113@utils.deprecated()114def reorder(self, new_ordering):115changer = _FunctionChangers(116self.pyname.get_object(), self.get_definition_info(),117[ArgumentReorderer(new_ordering)])118return self._change_calls(changer)119120def get_changes(self, changers, in_hierarchy=False, resources=None,121task_handle=taskhandle.NullTaskHandle()):122"""Get changes caused by this refactoring123124`changers` is a list of `_ArgumentChanger`\s. If `in_hierarchy`125is `True` the changers are applyed to all matching methods in126the class hierarchy.127`resources` can be a list of `rope.base.resource.File`\s that128should be searched for occurrences; if `None` all python files129in the project are searched.130131"""132function_changer = _FunctionChangers(self.pyname.get_object(),133self._definfo(), changers)134return self._change_calls(function_changer, in_hierarchy,135resources, task_handle)136137138class _FunctionChangers(object):139140def __init__(self, pyfunction, definition_info, changers=None):141self.pyfunction = pyfunction142self.definition_info = definition_info143self.changers = changers144self.changed_definition_infos = self._get_changed_definition_infos()145146def _get_changed_definition_infos(self):147result = []148definition_info = self.definition_info149result.append(definition_info)150for changer in self.changers:151definition_info = copy.deepcopy(definition_info)152changer.change_definition_info(definition_info)153result.append(definition_info)154return result155156def change_definition(self, call):157return self.changed_definition_infos[-1].to_string()158159def change_call(self, primary, pyname, call):160call_info = functionutils.CallInfo.read(161primary, pyname, self.definition_info, call)162mapping = functionutils.ArgumentMapping(self.definition_info, call_info)163164for definition_info, changer in zip(self.changed_definition_infos, self.changers):165changer.change_argument_mapping(definition_info, mapping)166167return mapping.to_call_info(self.changed_definition_infos[-1]).to_string()168169170class _ArgumentChanger(object):171172def change_definition_info(self, definition_info):173pass174175def change_argument_mapping(self, definition_info, argument_mapping):176pass177178179class ArgumentNormalizer(_ArgumentChanger):180pass181182183class ArgumentRemover(_ArgumentChanger):184185def __init__(self, index):186self.index = index187188def change_definition_info(self, call_info):189if self.index < len(call_info.args_with_defaults):190del call_info.args_with_defaults[self.index]191elif self.index == len(call_info.args_with_defaults) and \192call_info.args_arg is not None:193call_info.args_arg = None194elif (self.index == len(call_info.args_with_defaults) and195call_info.args_arg is None and call_info.keywords_arg is not None) or \196(self.index == len(call_info.args_with_defaults) + 1 and197call_info.args_arg is not None and call_info.keywords_arg is not None):198call_info.keywords_arg = None199200def change_argument_mapping(self, definition_info, mapping):201if self.index < len(definition_info.args_with_defaults):202name = definition_info.args_with_defaults[0]203if name in mapping.param_dict:204del mapping.param_dict[name]205206207class ArgumentAdder(_ArgumentChanger):208209def __init__(self, index, name, default=None, value=None):210self.index = index211self.name = name212self.default = default213self.value = value214215def change_definition_info(self, definition_info):216for pair in definition_info.args_with_defaults:217if pair[0] == self.name:218raise rope.base.exceptions.RefactoringError(219'Adding duplicate parameter: <%s>.' % self.name)220definition_info.args_with_defaults.insert(self.index,221(self.name, self.default))222223def change_argument_mapping(self, definition_info, mapping):224if self.value is not None:225mapping.param_dict[self.name] = self.value226227228class ArgumentDefaultInliner(_ArgumentChanger):229230def __init__(self, index):231self.index = index232self.remove = False233234def change_definition_info(self, definition_info):235if self.remove:236definition_info.args_with_defaults[self.index] = \237(definition_info.args_with_defaults[self.index][0], None)238239def change_argument_mapping(self, definition_info, mapping):240default = definition_info.args_with_defaults[self.index][1]241name = definition_info.args_with_defaults[self.index][0]242if default is not None and name not in mapping.param_dict:243mapping.param_dict[name] = default244245246class ArgumentReorderer(_ArgumentChanger):247248def __init__(self, new_order, autodef=None):249"""Construct an `ArgumentReorderer`250251Note that the `new_order` is a list containing the new252position of parameters; not the position each parameter253is going to be moved to. (changed in ``0.5m4``)254255For example changing ``f(a, b, c)`` to ``f(c, a, b)``256requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``.257258The `autodef` (automatic default) argument, forces rope to use259it as a default if a default is needed after the change. That260happens when an argument without default is moved after261another that has a default value. Note that `autodef` should262be a string or `None`; the latter disables adding automatic263default.264265"""266self.new_order = new_order267self.autodef = autodef268269def change_definition_info(self, definition_info):270new_args = list(definition_info.args_with_defaults)271for new_index, index in enumerate(self.new_order):272new_args[new_index] = definition_info.args_with_defaults[index]273seen_default = False274for index, (arg, default) in enumerate(list(new_args)):275if default is not None:276seen_default = True277if seen_default and default is None and self.autodef is not None:278new_args[index] = (arg, self.autodef)279definition_info.args_with_defaults = new_args280281282class _ChangeCallsInModule(object):283284def __init__(self, pycore, occurrence_finder, resource, call_changer):285self.pycore = pycore286self.occurrence_finder = occurrence_finder287self.resource = resource288self.call_changer = call_changer289290def get_changed_module(self):291word_finder = worder.Worder(self.source)292change_collector = codeanalyze.ChangeCollector(self.source)293for occurrence in self.occurrence_finder.find_occurrences(self.resource):294if not occurrence.is_called() and not occurrence.is_defined():295continue296start, end = occurrence.get_primary_range()297begin_parens, end_parens = word_finder.get_word_parens_range(end - 1)298if occurrence.is_called():299primary, pyname = occurrence.get_primary_and_pyname()300changed_call = self.call_changer.change_call(301primary, pyname, self.source[start:end_parens])302else:303changed_call = self.call_changer.change_definition(304self.source[start:end_parens])305if changed_call is not None:306change_collector.add_change(start, end_parens, changed_call)307return change_collector.get_changed()308309@property310@utils.saveit311def pymodule(self):312return self.pycore.resource_to_pyobject(self.resource)313314@property315@utils.saveit316def source(self):317if self.resource is not None:318return self.resource.read()319else:320return self.pymodule.source_code321322@property323@utils.saveit324def lines(self):325return self.pymodule.lines326327328class _MultipleFinders(object):329330def __init__(self, finders):331self.finders = finders332333def find_occurrences(self, resource=None, pymodule=None):334all_occurrences = []335for finder in self.finders:336all_occurrences.extend(finder.find_occurrences(resource, pymodule))337all_occurrences.sort(self._cmp_occurrences)338return all_occurrences339340def _cmp_occurrences(self, o1, o2):341return cmp(o1.get_primary_range(), o2.get_primary_range())342343344