change_signature.py 13 KB

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