1import copy 2 3import rope.base.exceptions 4from rope.base import codeanalyze 5from rope.base import evaluate 6from rope.base import pyobjects 7from rope.base import taskhandle 8from rope.base import utils 9from rope.base import worder 10from rope.base.change import ChangeContents, ChangeSet 11from rope.refactor import occurrences, functionutils 12 13 14class ChangeSignature(object): 15 16 def __init__(self, project, resource, offset): 17 self.project = project 18 self.resource = resource 19 self.offset = offset 20 self._set_name_and_pyname() 21 if self.pyname is None or self.pyname.get_object() is None or \ 22 not isinstance(self.pyname.get_object(), pyobjects.PyFunction): 23 raise rope.base.exceptions.RefactoringError( 24 'Change method signature should be performed on functions') 25 26 def _set_name_and_pyname(self): 27 self.name = worder.get_name_at(self.resource, self.offset) 28 this_pymodule = self.project.get_pymodule(self.resource) 29 self.primary, self.pyname = evaluate.eval_location2( 30 this_pymodule, self.offset) 31 if self.pyname is None: 32 return 33 pyobject = self.pyname.get_object() 34 if isinstance(pyobject, pyobjects.PyClass) and \ 35 '__init__' in pyobject: 36 self.pyname = pyobject['__init__'] 37 self.name = '__init__' 38 pyobject = self.pyname.get_object() 39 self.others = None 40 if self.name == '__init__' and \ 41 isinstance(pyobject, pyobjects.PyFunction) and \ 42 isinstance(pyobject.parent, pyobjects.PyClass): 43 pyclass = pyobject.parent 44 self.others = (pyclass.get_name(), 45 pyclass.parent[pyclass.get_name()]) 46 47 def _change_calls(self, call_changer, in_hierarchy=None, resources=None, 48 handle=taskhandle.NullTaskHandle()): 49 if resources is None: 50 resources = self.project.get_python_files() 51 changes = ChangeSet('Changing signature of <%s>' % self.name) 52 job_set = handle.create_jobset('Collecting Changes', len(resources)) 53 finder = occurrences.create_finder( 54 self.project, self.name, self.pyname, instance=self.primary, 55 in_hierarchy=in_hierarchy and self.is_method()) 56 if self.others: 57 name, pyname = self.others 58 constructor_finder = occurrences.create_finder( 59 self.project, name, pyname, only_calls=True) 60 finder = _MultipleFinders([finder, constructor_finder]) 61 for file in resources: 62 job_set.started_job(file.path) 63 change_calls = _ChangeCallsInModule( 64 self.project, finder, file, call_changer) 65 changed_file = change_calls.get_changed_module() 66 if changed_file is not None: 67 changes.add_change(ChangeContents(file, changed_file)) 68 job_set.finished_job() 69 return changes 70 71 def get_args(self): 72 """Get function arguments. 73 74 Return a list of ``(name, default)`` tuples for all but star 75 and double star arguments. For arguments that don't have a 76 default, `None` will be used. 77 """ 78 return self._definfo().args_with_defaults 79 80 def is_method(self): 81 pyfunction = self.pyname.get_object() 82 return isinstance(pyfunction.parent, pyobjects.PyClass) 83 84 @utils.deprecated('Use `ChangeSignature.get_args()` instead') 85 def get_definition_info(self): 86 return self._definfo() 87 88 def _definfo(self): 89 return functionutils.DefinitionInfo.read(self.pyname.get_object()) 90 91 @utils.deprecated() 92 def normalize(self): 93 changer = _FunctionChangers( 94 self.pyname.get_object(), self.get_definition_info(), 95 [ArgumentNormalizer()]) 96 return self._change_calls(changer) 97 98 @utils.deprecated() 99 def remove(self, index): 100 changer = _FunctionChangers( 101 self.pyname.get_object(), self.get_definition_info(), 102 [ArgumentRemover(index)]) 103 return self._change_calls(changer) 104 105 @utils.deprecated() 106 def add(self, index, name, default=None, value=None): 107 changer = _FunctionChangers( 108 self.pyname.get_object(), self.get_definition_info(), 109 [ArgumentAdder(index, name, default, value)]) 110 return self._change_calls(changer) 111 112 @utils.deprecated() 113 def inline_default(self, index): 114 changer = _FunctionChangers( 115 self.pyname.get_object(), self.get_definition_info(), 116 [ArgumentDefaultInliner(index)]) 117 return self._change_calls(changer) 118 119 @utils.deprecated() 120 def reorder(self, new_ordering): 121 changer = _FunctionChangers( 122 self.pyname.get_object(), self.get_definition_info(), 123 [ArgumentReorderer(new_ordering)]) 124 return self._change_calls(changer) 125 126 def get_changes(self, changers, in_hierarchy=False, resources=None, 127 task_handle=taskhandle.NullTaskHandle()): 128 """Get changes caused by this refactoring 129 130 `changers` is a list of `_ArgumentChanger`. If `in_hierarchy` 131 is `True` the changers are applyed to all matching methods in 132 the class hierarchy. 133 `resources` can be a list of `rope.base.resource.File` that 134 should be searched for occurrences; if `None` all python files 135 in the project are searched. 136 137 """ 138 function_changer = _FunctionChangers(self.pyname.get_object(), 139 self._definfo(), changers) 140 return self._change_calls(function_changer, in_hierarchy, 141 resources, task_handle) 142 143 144class _FunctionChangers(object): 145 146 def __init__(self, pyfunction, definition_info, changers=None): 147 self.pyfunction = pyfunction 148 self.definition_info = definition_info 149 self.changers = changers 150 self.changed_definition_infos = self._get_changed_definition_infos() 151 152 def _get_changed_definition_infos(self): 153 result = [] 154 definition_info = self.definition_info 155 result.append(definition_info) 156 for changer in self.changers: 157 definition_info = copy.deepcopy(definition_info) 158 changer.change_definition_info(definition_info) 159 result.append(definition_info) 160 return result 161 162 def change_definition(self, call): 163 return self.changed_definition_infos[-1].to_string() 164 165 def change_call(self, primary, pyname, call): 166 call_info = functionutils.CallInfo.read( 167 primary, pyname, self.definition_info, call) 168 mapping = functionutils.ArgumentMapping(self.definition_info, 169 call_info) 170 171 for definition_info, changer in zip(self.changed_definition_infos, 172 self.changers): 173 changer.change_argument_mapping(definition_info, mapping) 174 175 return mapping.to_call_info( 176 self.changed_definition_infos[-1]).to_string() 177 178 179class _ArgumentChanger(object): 180 181 def change_definition_info(self, definition_info): 182 pass 183 184 def change_argument_mapping(self, definition_info, argument_mapping): 185 pass 186 187 188class ArgumentNormalizer(_ArgumentChanger): 189 pass 190 191 192class ArgumentRemover(_ArgumentChanger): 193 194 def __init__(self, index): 195 self.index = index 196 197 def change_definition_info(self, call_info): 198 if self.index < len(call_info.args_with_defaults): 199 del call_info.args_with_defaults[self.index] 200 elif self.index == len(call_info.args_with_defaults) and \ 201 call_info.args_arg is not None: 202 call_info.args_arg = None 203 elif (self.index == len(call_info.args_with_defaults) and 204 call_info.args_arg is None and 205 call_info.keywords_arg is not None) or \ 206 (self.index == len(call_info.args_with_defaults) + 1 and 207 call_info.args_arg is not None and 208 call_info.keywords_arg is not None): 209 call_info.keywords_arg = None 210 211 def change_argument_mapping(self, definition_info, mapping): 212 if self.index < len(definition_info.args_with_defaults): 213 name = definition_info.args_with_defaults[0] 214 if name in mapping.param_dict: 215 del mapping.param_dict[name] 216 217 218class ArgumentAdder(_ArgumentChanger): 219 220 def __init__(self, index, name, default=None, value=None): 221 self.index = index 222 self.name = name 223 self.default = default 224 self.value = value 225 226 def change_definition_info(self, definition_info): 227 for pair in definition_info.args_with_defaults: 228 if pair[0] == self.name: 229 raise rope.base.exceptions.RefactoringError( 230 'Adding duplicate parameter: <%s>.' % self.name) 231 definition_info.args_with_defaults.insert(self.index, 232 (self.name, self.default)) 233 234 def change_argument_mapping(self, definition_info, mapping): 235 if self.value is not None: 236 mapping.param_dict[self.name] = self.value 237 238 239class ArgumentDefaultInliner(_ArgumentChanger): 240 241 def __init__(self, index): 242 self.index = index 243 self.remove = False 244 245 def change_definition_info(self, definition_info): 246 if self.remove: 247 definition_info.args_with_defaults[self.index] = \ 248 (definition_info.args_with_defaults[self.index][0], None) 249 250 def change_argument_mapping(self, definition_info, mapping): 251 default = definition_info.args_with_defaults[self.index][1] 252 name = definition_info.args_with_defaults[self.index][0] 253 if default is not None and name not in mapping.param_dict: 254 mapping.param_dict[name] = default 255 256 257class ArgumentReorderer(_ArgumentChanger): 258 259 def __init__(self, new_order, autodef=None): 260 """Construct an `ArgumentReorderer` 261 262 Note that the `new_order` is a list containing the new 263 position of parameters; not the position each parameter 264 is going to be moved to. (changed in ``0.5m4``) 265 266 For example changing ``f(a, b, c)`` to ``f(c, a, b)`` 267 requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``. 268 269 The `autodef` (automatic default) argument, forces rope to use 270 it as a default if a default is needed after the change. That 271 happens when an argument without default is moved after 272 another that has a default value. Note that `autodef` should 273 be a string or `None`; the latter disables adding automatic 274 default. 275 276 """ 277 self.new_order = new_order 278 self.autodef = autodef 279 280 def change_definition_info(self, definition_info): 281 new_args = list(definition_info.args_with_defaults) 282 for new_index, index in enumerate(self.new_order): 283 new_args[new_index] = definition_info.args_with_defaults[index] 284 seen_default = False 285 for index, (arg, default) in enumerate(list(new_args)): 286 if default is not None: 287 seen_default = True 288 if seen_default and default is None and self.autodef is not None: 289 new_args[index] = (arg, self.autodef) 290 definition_info.args_with_defaults = new_args 291 292 293class _ChangeCallsInModule(object): 294 295 def __init__(self, project, occurrence_finder, resource, call_changer): 296 self.project = project 297 self.occurrence_finder = occurrence_finder 298 self.resource = resource 299 self.call_changer = call_changer 300 301 def get_changed_module(self): 302 word_finder = worder.Worder(self.source) 303 change_collector = codeanalyze.ChangeCollector(self.source) 304 for occurrence in self.occurrence_finder.find_occurrences( 305 self.resource): 306 if not occurrence.is_called() and not occurrence.is_defined(): 307 continue 308 start, end = occurrence.get_primary_range() 309 begin_parens, end_parens = word_finder.\ 310 get_word_parens_range(end - 1) 311 if occurrence.is_called(): 312 primary, pyname = occurrence.get_primary_and_pyname() 313 changed_call = self.call_changer.change_call( 314 primary, pyname, self.source[start:end_parens]) 315 else: 316 changed_call = self.call_changer.change_definition( 317 self.source[start:end_parens]) 318 if changed_call is not None: 319 change_collector.add_change(start, end_parens, changed_call) 320 return change_collector.get_changed() 321 322 @property 323 @utils.saveit 324 def pymodule(self): 325 return self.project.get_pymodule(self.resource) 326 327 @property 328 @utils.saveit 329 def source(self): 330 if self.resource is not None: 331 return self.resource.read() 332 else: 333 return self.pymodule.source_code 334 335 @property 336 @utils.saveit 337 def lines(self): 338 return self.pymodule.lines 339 340 341class _MultipleFinders(object): 342 343 def __init__(self, finders): 344 self.finders = finders 345 346 def find_occurrences(self, resource=None, pymodule=None): 347 all_occurrences = [] 348 for finder in self.finders: 349 all_occurrences.extend(finder.find_occurrences(resource, pymodule)) 350 all_occurrences.sort(key=lambda x: x.get_primary_range()) 351 return all_occurrences 352 353