rename.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import warnings
  2. from rope.base import exceptions, pyobjects, pynames, taskhandle, evaluate, worder, codeanalyze
  3. from rope.base.change import ChangeSet, ChangeContents, MoveResource
  4. from rope.refactor import occurrences, sourceutils
  5. class Rename(object):
  6. """A class for performing rename refactoring
  7. It can rename everything: classes, functions, modules, packages,
  8. methods, variables and keyword arguments.
  9. """
  10. def __init__(self, project, resource, offset=None):
  11. """If `offset` is None, the `resource` itself will be renamed"""
  12. self.project = project
  13. self.pycore = project.pycore
  14. self.resource = resource
  15. if offset is not None:
  16. self.old_name = worder.get_name_at(self.resource, offset)
  17. this_pymodule = self.pycore.resource_to_pyobject(self.resource)
  18. self.old_instance, self.old_pyname = \
  19. evaluate.eval_location2(this_pymodule, offset)
  20. if self.old_pyname is None:
  21. raise exceptions.RefactoringError(
  22. 'Rename refactoring should be performed'
  23. ' on resolvable python identifiers.')
  24. else:
  25. if not resource.is_folder() and resource.name == '__init__.py':
  26. resource = resource.parent
  27. dummy_pymodule = self.pycore.get_string_module('')
  28. self.old_instance = None
  29. self.old_pyname = pynames.ImportedModule(dummy_pymodule,
  30. resource=resource)
  31. if resource.is_folder():
  32. self.old_name = resource.name
  33. else:
  34. self.old_name = resource.name[:-3]
  35. def get_old_name(self):
  36. return self.old_name
  37. def get_changes(self, new_name, in_file=None, in_hierarchy=False,
  38. unsure=None, docs=False, resources=None,
  39. task_handle=taskhandle.NullTaskHandle()):
  40. """Get the changes needed for this refactoring
  41. Parameters:
  42. - `in_hierarchy`: when renaming a method this keyword forces
  43. to rename all matching methods in the hierarchy
  44. - `docs`: when `True` rename refactoring will rename
  45. occurrences in comments and strings where the name is
  46. visible. Setting it will make renames faster, too.
  47. - `unsure`: decides what to do about unsure occurrences.
  48. If `None`, they are ignored. Otherwise `unsure` is
  49. called with an instance of `occurrence.Occurrence` as
  50. parameter. If it returns `True`, the occurrence is
  51. considered to be a match.
  52. - `resources` can be a list of `rope.base.resources.File`\s to
  53. apply this refactoring on. If `None`, the restructuring
  54. will be applied to all python files.
  55. - `in_file`: this argument has been deprecated; use
  56. `resources` instead.
  57. """
  58. if unsure in (True, False):
  59. warnings.warn(
  60. 'unsure parameter should be a function that returns '
  61. 'True or False', DeprecationWarning, stacklevel=2)
  62. def unsure_func(value=unsure):
  63. return value
  64. unsure = unsure_func
  65. if in_file is not None:
  66. warnings.warn(
  67. '`in_file` argument has been deprecated; use `resources` '
  68. 'instead. ', DeprecationWarning, stacklevel=2)
  69. if in_file:
  70. resources = [self.resource]
  71. if _is_local(self.old_pyname):
  72. resources = [self.resource]
  73. if resources is None:
  74. resources = self.pycore.get_python_files()
  75. changes = ChangeSet('Renaming <%s> to <%s>' %
  76. (self.old_name, new_name))
  77. finder = occurrences.create_finder(
  78. self.pycore, self.old_name, self.old_pyname, unsure=unsure,
  79. docs=docs, instance=self.old_instance,
  80. in_hierarchy=in_hierarchy and self.is_method())
  81. job_set = task_handle.create_jobset('Collecting Changes', len(resources))
  82. for file_ in resources:
  83. job_set.started_job(file_.path)
  84. new_content = rename_in_module(finder, new_name, resource=file_)
  85. if new_content is not None:
  86. changes.add_change(ChangeContents(file_, new_content))
  87. job_set.finished_job()
  88. if self._is_renaming_a_module():
  89. resource = self.old_pyname.get_object().get_resource()
  90. if self._is_allowed_to_move(resources, resource):
  91. self._rename_module(resource, new_name, changes)
  92. return changes
  93. def _is_allowed_to_move(self, resources, resource):
  94. if resource.is_folder():
  95. try:
  96. return resource.get_child('__init__.py') in resources
  97. except exceptions.ResourceNotFoundError:
  98. return False
  99. else:
  100. return resource in resources
  101. def _is_renaming_a_module(self):
  102. if isinstance(self.old_pyname.get_object(), pyobjects.AbstractModule):
  103. return True
  104. return False
  105. def is_method(self):
  106. pyname = self.old_pyname
  107. return isinstance(pyname, pynames.DefinedName) and \
  108. isinstance(pyname.get_object(), pyobjects.PyFunction) and \
  109. isinstance(pyname.get_object().parent, pyobjects.PyClass)
  110. def _rename_module(self, resource, new_name, changes):
  111. if not resource.is_folder():
  112. new_name = new_name + '.py'
  113. parent_path = resource.parent.path
  114. if parent_path == '':
  115. new_location = new_name
  116. else:
  117. new_location = parent_path + '/' + new_name
  118. changes.add_change(MoveResource(resource, new_location))
  119. class ChangeOccurrences(object):
  120. """A class for changing the occurrences of a name in a scope
  121. This class replaces the occurrences of a name. Note that it only
  122. changes the scope containing the offset passed to the constructor.
  123. What's more it does not have any side-effects. That is for
  124. example changing occurrences of a module does not rename the
  125. module; it merely replaces the occurrences of that module in a
  126. scope with the given expression. This class is useful for
  127. performing many custom refactorings.
  128. """
  129. def __init__(self, project, resource, offset):
  130. self.pycore = project.pycore
  131. self.resource = resource
  132. self.offset = offset
  133. self.old_name = worder.get_name_at(resource, offset)
  134. self.pymodule = self.pycore.resource_to_pyobject(self.resource)
  135. self.old_pyname = evaluate.eval_location(self.pymodule, offset)
  136. def get_old_name(self):
  137. word_finder = worder.Worder(self.resource.read())
  138. return word_finder.get_primary_at(self.offset)
  139. def _get_scope_offset(self):
  140. lines = self.pymodule.lines
  141. scope = self.pymodule.get_scope().\
  142. get_inner_scope_for_line(lines.get_line_number(self.offset))
  143. start = lines.get_line_start(scope.get_start())
  144. end = lines.get_line_end(scope.get_end())
  145. return start, end
  146. def get_changes(self, new_name, only_calls=False, reads=True, writes=True):
  147. changes = ChangeSet('Changing <%s> occurrences to <%s>' %
  148. (self.old_name, new_name))
  149. scope_start, scope_end = self._get_scope_offset()
  150. finder = occurrences.create_finder(
  151. self.pycore, self.old_name, self.old_pyname,
  152. imports=False, only_calls=only_calls)
  153. new_contents = rename_in_module(
  154. finder, new_name, pymodule=self.pymodule, replace_primary=True,
  155. region=(scope_start, scope_end), reads=reads, writes=writes)
  156. if new_contents is not None:
  157. changes.add_change(ChangeContents(self.resource, new_contents))
  158. return changes
  159. def rename_in_module(occurrences_finder, new_name, resource=None, pymodule=None,
  160. replace_primary=False, region=None, reads=True, writes=True):
  161. """Returns the changed source or `None` if there is no changes"""
  162. if resource is not None:
  163. source_code = resource.read()
  164. else:
  165. source_code = pymodule.source_code
  166. change_collector = codeanalyze.ChangeCollector(source_code)
  167. for occurrence in occurrences_finder.find_occurrences(resource, pymodule):
  168. if replace_primary and occurrence.is_a_fixed_primary():
  169. continue
  170. if replace_primary:
  171. start, end = occurrence.get_primary_range()
  172. else:
  173. start, end = occurrence.get_word_range()
  174. if (not reads and not occurrence.is_written()) or \
  175. (not writes and occurrence.is_written()):
  176. continue
  177. if region is None or region[0] <= start < region[1]:
  178. change_collector.add_change(start, end, new_name)
  179. return change_collector.get_changed()
  180. def _is_local(pyname):
  181. module, lineno = pyname.get_definition_location()
  182. if lineno is None:
  183. return False
  184. scope = module.get_scope().get_inner_scope_for_line(lineno)
  185. if isinstance(pyname, pynames.DefinedName) and \
  186. scope.get_kind() in ('Function', 'Class'):
  187. scope = scope.parent
  188. return scope.get_kind() == 'Function' and \
  189. pyname in scope.get_names().values() and \
  190. isinstance(pyname, pynames.AssignedName)