import copy import rope.base.exceptions from rope.base import pyobjects, taskhandle, evaluate, worder, codeanalyze, utils from rope.base.change import ChangeContents, ChangeSet from rope.refactor import occurrences, functionutils class ChangeSignature(object): def __init__(self, project, resource, offset): self.pycore = project.pycore self.resource = resource self.offset = offset self._set_name_and_pyname() if self.pyname is None or self.pyname.get_object() is None or \ not isinstance(self.pyname.get_object(), pyobjects.PyFunction): raise rope.base.exceptions.RefactoringError( 'Change method signature should be performed on functions') def _set_name_and_pyname(self): self.name = worder.get_name_at(self.resource, self.offset) this_pymodule = self.pycore.resource_to_pyobject(self.resource) self.primary, self.pyname = evaluate.eval_location2( this_pymodule, self.offset) if self.pyname is None: return pyobject = self.pyname.get_object() if isinstance(pyobject, pyobjects.PyClass) and \ '__init__' in pyobject: self.pyname = pyobject['__init__'] self.name = '__init__' pyobject = self.pyname.get_object() self.others = None if self.name == '__init__' and \ isinstance(pyobject, pyobjects.PyFunction) and \ isinstance(pyobject.parent, pyobjects.PyClass): pyclass = pyobject.parent self.others = (pyclass.get_name(), pyclass.parent[pyclass.get_name()]) def _change_calls(self, call_changer, in_hierarchy=None, resources=None, handle=taskhandle.NullTaskHandle()): if resources is None: resources = self.pycore.get_python_files() changes = ChangeSet('Changing signature of <%s>' % self.name) job_set = handle.create_jobset('Collecting Changes', len(resources)) finder = occurrences.create_finder( self.pycore, self.name, self.pyname, instance=self.primary, in_hierarchy=in_hierarchy and self.is_method()) if self.others: name, pyname = self.others constructor_finder = occurrences.create_finder( self.pycore, name, pyname, only_calls=True) finder = _MultipleFinders([finder, constructor_finder]) for file in resources: job_set.started_job(file.path) change_calls = _ChangeCallsInModule( self.pycore, finder, file, call_changer) changed_file = change_calls.get_changed_module() if changed_file is not None: changes.add_change(ChangeContents(file, changed_file)) job_set.finished_job() return changes def get_args(self): """Get function arguments. Return a list of ``(name, default)`` tuples for all but star and double star arguments. For arguments that don't have a default, `None` will be used. """ return self._definfo().args_with_defaults def is_method(self): pyfunction = self.pyname.get_object() return isinstance(pyfunction.parent, pyobjects.PyClass) @utils.deprecated('Use `ChangeSignature.get_args()` instead') def get_definition_info(self): return self._definfo() def _definfo(self): return functionutils.DefinitionInfo.read(self.pyname.get_object()) @utils.deprecated() def normalize(self): changer = _FunctionChangers( self.pyname.get_object(), self.get_definition_info(), [ArgumentNormalizer()]) return self._change_calls(changer) @utils.deprecated() def remove(self, index): changer = _FunctionChangers( self.pyname.get_object(), self.get_definition_info(), [ArgumentRemover(index)]) return self._change_calls(changer) @utils.deprecated() def add(self, index, name, default=None, value=None): changer = _FunctionChangers( self.pyname.get_object(), self.get_definition_info(), [ArgumentAdder(index, name, default, value)]) return self._change_calls(changer) @utils.deprecated() def inline_default(self, index): changer = _FunctionChangers( self.pyname.get_object(), self.get_definition_info(), [ArgumentDefaultInliner(index)]) return self._change_calls(changer) @utils.deprecated() def reorder(self, new_ordering): changer = _FunctionChangers( self.pyname.get_object(), self.get_definition_info(), [ArgumentReorderer(new_ordering)]) return self._change_calls(changer) def get_changes(self, changers, in_hierarchy=False, resources=None, task_handle=taskhandle.NullTaskHandle()): """Get changes caused by this refactoring `changers` is a list of `_ArgumentChanger`\s. If `in_hierarchy` is `True` the changers are applyed to all matching methods in the class hierarchy. `resources` can be a list of `rope.base.resource.File`\s that should be searched for occurrences; if `None` all python files in the project are searched. """ function_changer = _FunctionChangers(self.pyname.get_object(), self._definfo(), changers) return self._change_calls(function_changer, in_hierarchy, resources, task_handle) class _FunctionChangers(object): def __init__(self, pyfunction, definition_info, changers=None): self.pyfunction = pyfunction self.definition_info = definition_info self.changers = changers self.changed_definition_infos = self._get_changed_definition_infos() def _get_changed_definition_infos(self): result = [] definition_info = self.definition_info result.append(definition_info) for changer in self.changers: definition_info = copy.deepcopy(definition_info) changer.change_definition_info(definition_info) result.append(definition_info) return result def change_definition(self, call): return self.changed_definition_infos[-1].to_string() def change_call(self, primary, pyname, call): call_info = functionutils.CallInfo.read( primary, pyname, self.definition_info, call) mapping = functionutils.ArgumentMapping(self.definition_info, call_info) for definition_info, changer in zip(self.changed_definition_infos, self.changers): changer.change_argument_mapping(definition_info, mapping) return mapping.to_call_info(self.changed_definition_infos[-1]).to_string() class _ArgumentChanger(object): def change_definition_info(self, definition_info): pass def change_argument_mapping(self, definition_info, argument_mapping): pass class ArgumentNormalizer(_ArgumentChanger): pass class ArgumentRemover(_ArgumentChanger): def __init__(self, index): self.index = index def change_definition_info(self, call_info): if self.index < len(call_info.args_with_defaults): del call_info.args_with_defaults[self.index] elif self.index == len(call_info.args_with_defaults) and \ call_info.args_arg is not None: call_info.args_arg = None elif (self.index == len(call_info.args_with_defaults) and call_info.args_arg is None and call_info.keywords_arg is not None) or \ (self.index == len(call_info.args_with_defaults) + 1 and call_info.args_arg is not None and call_info.keywords_arg is not None): call_info.keywords_arg = None def change_argument_mapping(self, definition_info, mapping): if self.index < len(definition_info.args_with_defaults): name = definition_info.args_with_defaults[0] if name in mapping.param_dict: del mapping.param_dict[name] class ArgumentAdder(_ArgumentChanger): def __init__(self, index, name, default=None, value=None): self.index = index self.name = name self.default = default self.value = value def change_definition_info(self, definition_info): for pair in definition_info.args_with_defaults: if pair[0] == self.name: raise rope.base.exceptions.RefactoringError( 'Adding duplicate parameter: <%s>.' % self.name) definition_info.args_with_defaults.insert(self.index, (self.name, self.default)) def change_argument_mapping(self, definition_info, mapping): if self.value is not None: mapping.param_dict[self.name] = self.value class ArgumentDefaultInliner(_ArgumentChanger): def __init__(self, index): self.index = index self.remove = False def change_definition_info(self, definition_info): if self.remove: definition_info.args_with_defaults[self.index] = \ (definition_info.args_with_defaults[self.index][0], None) def change_argument_mapping(self, definition_info, mapping): default = definition_info.args_with_defaults[self.index][1] name = definition_info.args_with_defaults[self.index][0] if default is not None and name not in mapping.param_dict: mapping.param_dict[name] = default class ArgumentReorderer(_ArgumentChanger): def __init__(self, new_order, autodef=None): """Construct an `ArgumentReorderer` Note that the `new_order` is a list containing the new position of parameters; not the position each parameter is going to be moved to. (changed in ``0.5m4``) For example changing ``f(a, b, c)`` to ``f(c, a, b)`` requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``. The `autodef` (automatic default) argument, forces rope to use it as a default if a default is needed after the change. That happens when an argument without default is moved after another that has a default value. Note that `autodef` should be a string or `None`; the latter disables adding automatic default. """ self.new_order = new_order self.autodef = autodef def change_definition_info(self, definition_info): new_args = list(definition_info.args_with_defaults) for new_index, index in enumerate(self.new_order): new_args[new_index] = definition_info.args_with_defaults[index] seen_default = False for index, (arg, default) in enumerate(list(new_args)): if default is not None: seen_default = True if seen_default and default is None and self.autodef is not None: new_args[index] = (arg, self.autodef) definition_info.args_with_defaults = new_args class _ChangeCallsInModule(object): def __init__(self, pycore, occurrence_finder, resource, call_changer): self.pycore = pycore self.occurrence_finder = occurrence_finder self.resource = resource self.call_changer = call_changer def get_changed_module(self): word_finder = worder.Worder(self.source) change_collector = codeanalyze.ChangeCollector(self.source) for occurrence in self.occurrence_finder.find_occurrences(self.resource): if not occurrence.is_called() and not occurrence.is_defined(): continue start, end = occurrence.get_primary_range() begin_parens, end_parens = word_finder.get_word_parens_range(end - 1) if occurrence.is_called(): primary, pyname = occurrence.get_primary_and_pyname() changed_call = self.call_changer.change_call( primary, pyname, self.source[start:end_parens]) else: changed_call = self.call_changer.change_definition( self.source[start:end_parens]) if changed_call is not None: change_collector.add_change(start, end_parens, changed_call) return change_collector.get_changed() @property @utils.saveit def pymodule(self): return self.pycore.resource_to_pyobject(self.resource) @property @utils.saveit def source(self): if self.resource is not None: return self.resource.read() else: return self.pymodule.source_code @property @utils.saveit def lines(self): return self.pymodule.lines class _MultipleFinders(object): def __init__(self, finders): self.finders = finders def find_occurrences(self, resource=None, pymodule=None): all_occurrences = [] for finder in self.finders: all_occurrences.extend(finder.find_occurrences(resource, pymodule)) all_occurrences.sort(self._cmp_occurrences) return all_occurrences def _cmp_occurrences(self, o1, o2): return cmp(o1.get_primary_range(), o2.get_primary_range())