restructure.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import warnings
  2. from rope.base import change, taskhandle, builtins, ast, codeanalyze
  3. from rope.refactor import patchedast, similarfinder, sourceutils
  4. from rope.refactor.importutils import module_imports
  5. class Restructure(object):
  6. """A class to perform python restructurings
  7. A restructuring transforms pieces of code matching `pattern` to
  8. `goal`. In the `pattern` wildcards can appear. Wildcards match
  9. some piece of code based on their kind and arguments that are
  10. passed to them through `args`.
  11. `args` is a dictionary of wildcard names to wildcard arguments.
  12. If the argument is a tuple, the first item of the tuple is
  13. considered to be the name of the wildcard to use; otherwise the
  14. "default" wildcard is used. For getting the list arguments a
  15. wildcard supports, see the pydoc of the wildcard. (see
  16. `rope.refactor.wildcard.DefaultWildcard` for the default
  17. wildcard.)
  18. `wildcards` is the list of wildcard types that can appear in
  19. `pattern`. See `rope.refactor.wildcards`. If a wildcard does not
  20. specify its kind (by using a tuple in args), the wildcard named
  21. "default" is used. So there should be a wildcard with "default"
  22. name in `wildcards`.
  23. `imports` is the list of imports that changed modules should
  24. import. Note that rope handles duplicate imports and does not add
  25. the import if it already appears.
  26. Example #1::
  27. pattern ${pyobject}.get_attribute(${name})
  28. goal ${pyobject}[${name}]
  29. args pyobject: instance=rope.base.pyobjects.PyObject
  30. Example #2::
  31. pattern ${name} in ${pyobject}.get_attributes()
  32. goal ${name} in {pyobject}
  33. args pyobject: instance=rope.base.pyobjects.PyObject
  34. Example #3::
  35. pattern ${pycore}.create_module(${project}.root, ${name})
  36. goal generate.create_module(${project}, ${name})
  37. imports
  38. from rope.contrib import generate
  39. args
  40. pycore: type=rope.base.pycore.PyCore
  41. project: type=rope.base.project.Project
  42. Example #4::
  43. pattern ${pow}(${param1}, ${param2})
  44. goal ${param1} ** ${param2}
  45. args pow: name=mod.pow, exact
  46. Example #5::
  47. pattern ${inst}.longtask(${p1}, ${p2})
  48. goal
  49. ${inst}.subtask1(${p1})
  50. ${inst}.subtask2(${p2})
  51. args
  52. inst: type=mod.A,unsure
  53. """
  54. def __init__(self, project, pattern, goal, args=None,
  55. imports=None, wildcards=None):
  56. """Construct a restructuring
  57. See class pydoc for more info about the arguments.
  58. """
  59. self.pycore = project.pycore
  60. self.pattern = pattern
  61. self.goal = goal
  62. self.args = args
  63. if self.args is None:
  64. self.args = {}
  65. self.imports = imports
  66. if self.imports is None:
  67. self.imports = []
  68. self.wildcards = wildcards
  69. self.template = similarfinder.CodeTemplate(self.goal)
  70. def get_changes(self, checks=None, imports=None, resources=None,
  71. task_handle=taskhandle.NullTaskHandle()):
  72. """Get the changes needed by this restructuring
  73. `resources` can be a list of `rope.base.resources.File`\s to
  74. apply the restructuring on. If `None`, the restructuring will
  75. be applied to all python files.
  76. `checks` argument has been deprecated. Use the `args` argument
  77. of the constructor. The usage of::
  78. strchecks = {'obj1.type': 'mod.A', 'obj2': 'mod.B',
  79. 'obj3.object': 'mod.C'}
  80. checks = restructuring.make_checks(strchecks)
  81. can be replaced with::
  82. args = {'obj1': 'type=mod.A', 'obj2': 'name=mod.B',
  83. 'obj3': 'object=mod.C'}
  84. where obj1, obj2 and obj3 are wildcard names that appear
  85. in restructuring pattern.
  86. """
  87. if checks is not None:
  88. warnings.warn(
  89. 'The use of checks parameter is deprecated; '
  90. 'use the args parameter of the constructor instead.',
  91. DeprecationWarning, stacklevel=2)
  92. for name, value in checks.items():
  93. self.args[name] = similarfinder._pydefined_to_str(value)
  94. if imports is not None:
  95. warnings.warn(
  96. 'The use of imports parameter is deprecated; '
  97. 'use imports parameter of the constructor, instead.',
  98. DeprecationWarning, stacklevel=2)
  99. self.imports = imports
  100. changes = change.ChangeSet('Restructuring <%s> to <%s>' %
  101. (self.pattern, self.goal))
  102. if resources is not None:
  103. files = [resource for resource in resources
  104. if self.pycore.is_python_file(resource)]
  105. else:
  106. files = self.pycore.get_python_files()
  107. job_set = task_handle.create_jobset('Collecting Changes', len(files))
  108. for resource in files:
  109. job_set.started_job(resource.path)
  110. pymodule = self.pycore.resource_to_pyobject(resource)
  111. finder = similarfinder.SimilarFinder(pymodule,
  112. wildcards=self.wildcards)
  113. matches = list(finder.get_matches(self.pattern, self.args))
  114. computer = self._compute_changes(matches, pymodule)
  115. result = computer.get_changed()
  116. if result is not None:
  117. imported_source = self._add_imports(resource, result,
  118. self.imports)
  119. changes.add_change(change.ChangeContents(resource,
  120. imported_source))
  121. job_set.finished_job()
  122. return changes
  123. def _compute_changes(self, matches, pymodule):
  124. return _ChangeComputer(
  125. pymodule.source_code, pymodule.get_ast(),
  126. pymodule.lines, self.template, matches)
  127. def _add_imports(self, resource, source, imports):
  128. if not imports:
  129. return source
  130. import_infos = self._get_import_infos(resource, imports)
  131. pymodule = self.pycore.get_string_module(source, resource)
  132. imports = module_imports.ModuleImports(self.pycore, pymodule)
  133. for import_info in import_infos:
  134. imports.add_import(import_info)
  135. return imports.get_changed_source()
  136. def _get_import_infos(self, resource, imports):
  137. pymodule = self.pycore.get_string_module('\n'.join(imports),
  138. resource)
  139. imports = module_imports.ModuleImports(self.pycore, pymodule)
  140. return [imports.import_info
  141. for imports in imports.imports]
  142. def make_checks(self, string_checks):
  143. """Convert str to str dicts to str to PyObject dicts
  144. This function is here to ease writing a UI.
  145. """
  146. checks = {}
  147. for key, value in string_checks.items():
  148. is_pyname = not key.endswith('.object') and \
  149. not key.endswith('.type')
  150. evaluated = self._evaluate(value, is_pyname=is_pyname)
  151. if evaluated is not None:
  152. checks[key] = evaluated
  153. return checks
  154. def _evaluate(self, code, is_pyname=True):
  155. attributes = code.split('.')
  156. pyname = None
  157. if attributes[0] in ('__builtin__', '__builtins__'):
  158. class _BuiltinsStub(object):
  159. def get_attribute(self, name):
  160. return builtins.builtins[name]
  161. pyobject = _BuiltinsStub()
  162. else:
  163. pyobject = self.pycore.get_module(attributes[0])
  164. for attribute in attributes[1:]:
  165. pyname = pyobject[attribute]
  166. if pyname is None:
  167. return None
  168. pyobject = pyname.get_object()
  169. return pyname if is_pyname else pyobject
  170. def replace(code, pattern, goal):
  171. """used by other refactorings"""
  172. finder = similarfinder.RawSimilarFinder(code)
  173. matches = list(finder.get_matches(pattern))
  174. ast = patchedast.get_patched_ast(code)
  175. lines = codeanalyze.SourceLinesAdapter(code)
  176. template = similarfinder.CodeTemplate(goal)
  177. computer = _ChangeComputer(code, ast, lines, template, matches)
  178. result = computer.get_changed()
  179. if result is None:
  180. return code
  181. return result
  182. class _ChangeComputer(object):
  183. def __init__(self, code, ast, lines, goal, matches):
  184. self.source = code
  185. self.goal = goal
  186. self.matches = matches
  187. self.ast = ast
  188. self.lines = lines
  189. self.matched_asts = {}
  190. self._nearest_roots = {}
  191. if self._is_expression():
  192. for match in self.matches:
  193. self.matched_asts[match.ast] = match
  194. def get_changed(self):
  195. if self._is_expression():
  196. result = self._get_node_text(self.ast)
  197. if result == self.source:
  198. return None
  199. return result
  200. else:
  201. collector = codeanalyze.ChangeCollector(self.source)
  202. last_end = -1
  203. for match in self.matches:
  204. start, end = match.get_region()
  205. if start < last_end:
  206. if not self._is_expression():
  207. continue
  208. last_end = end
  209. replacement = self._get_matched_text(match)
  210. collector.add_change(start, end, replacement)
  211. return collector.get_changed()
  212. def _is_expression(self):
  213. return self.matches and isinstance(self.matches[0],
  214. similarfinder.ExpressionMatch)
  215. def _get_matched_text(self, match):
  216. mapping = {}
  217. for name in self.goal.get_names():
  218. node = match.get_ast(name)
  219. if node is None:
  220. raise similarfinder.BadNameInCheckError(
  221. 'Unknown name <%s>' % name)
  222. force = self._is_expression() and match.ast == node
  223. mapping[name] = self._get_node_text(node, force)
  224. unindented = self.goal.substitute(mapping)
  225. return self._auto_indent(match.get_region()[0], unindented)
  226. def _get_node_text(self, node, force=False):
  227. if not force and node in self.matched_asts:
  228. return self._get_matched_text(self.matched_asts[node])
  229. start, end = patchedast.node_region(node)
  230. main_text = self.source[start:end]
  231. collector = codeanalyze.ChangeCollector(main_text)
  232. for node in self._get_nearest_roots(node):
  233. sub_start, sub_end = patchedast.node_region(node)
  234. collector.add_change(sub_start - start, sub_end - start,
  235. self._get_node_text(node))
  236. result = collector.get_changed()
  237. if result is None:
  238. return main_text
  239. return result
  240. def _auto_indent(self, offset, text):
  241. lineno = self.lines.get_line_number(offset)
  242. indents = sourceutils.get_indents(self.lines, lineno)
  243. result = []
  244. for index, line in enumerate(text.splitlines(True)):
  245. if index != 0 and line.strip():
  246. result.append(' ' * indents)
  247. result.append(line)
  248. return ''.join(result)
  249. def _get_nearest_roots(self, node):
  250. if node not in self._nearest_roots:
  251. result = []
  252. for child in ast.get_child_nodes(node):
  253. if child in self.matched_asts:
  254. result.append(child)
  255. else:
  256. result.extend(self._get_nearest_roots(child))
  257. self._nearest_roots[node] = result
  258. return self._nearest_roots[node]