Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/restructure.py
1415 views
import warnings12from rope.base import change, taskhandle, builtins, ast, codeanalyze3from rope.refactor import patchedast, similarfinder, sourceutils4from rope.refactor.importutils import module_imports567class Restructure(object):8"""A class to perform python restructurings910A restructuring transforms pieces of code matching `pattern` to11`goal`. In the `pattern` wildcards can appear. Wildcards match12some piece of code based on their kind and arguments that are13passed to them through `args`.1415`args` is a dictionary of wildcard names to wildcard arguments.16If the argument is a tuple, the first item of the tuple is17considered to be the name of the wildcard to use; otherwise the18"default" wildcard is used. For getting the list arguments a19wildcard supports, see the pydoc of the wildcard. (see20`rope.refactor.wildcard.DefaultWildcard` for the default21wildcard.)2223`wildcards` is the list of wildcard types that can appear in24`pattern`. See `rope.refactor.wildcards`. If a wildcard does not25specify its kind (by using a tuple in args), the wildcard named26"default" is used. So there should be a wildcard with "default"27name in `wildcards`.2829`imports` is the list of imports that changed modules should30import. Note that rope handles duplicate imports and does not add31the import if it already appears.3233Example #1::3435pattern ${pyobject}.get_attribute(${name})36goal ${pyobject}[${name}]37args pyobject: instance=rope.base.pyobjects.PyObject3839Example #2::4041pattern ${name} in ${pyobject}.get_attributes()42goal ${name} in {pyobject}43args pyobject: instance=rope.base.pyobjects.PyObject4445Example #3::4647pattern ${pycore}.create_module(${project}.root, ${name})48goal generate.create_module(${project}, ${name})4950imports51from rope.contrib import generate5253args54pycore: type=rope.base.pycore.PyCore55project: type=rope.base.project.Project5657Example #4::5859pattern ${pow}(${param1}, ${param2})60goal ${param1} ** ${param2}61args pow: name=mod.pow, exact6263Example #5::6465pattern ${inst}.longtask(${p1}, ${p2})66goal67${inst}.subtask1(${p1})68${inst}.subtask2(${p2})69args70inst: type=mod.A,unsure7172"""7374def __init__(self, project, pattern, goal, args=None,75imports=None, wildcards=None):76"""Construct a restructuring7778See class pydoc for more info about the arguments.7980"""81self.pycore = project.pycore82self.pattern = pattern83self.goal = goal84self.args = args85if self.args is None:86self.args = {}87self.imports = imports88if self.imports is None:89self.imports = []90self.wildcards = wildcards91self.template = similarfinder.CodeTemplate(self.goal)9293def get_changes(self, checks=None, imports=None, resources=None,94task_handle=taskhandle.NullTaskHandle()):95"""Get the changes needed by this restructuring9697`resources` can be a list of `rope.base.resources.File`\s to98apply the restructuring on. If `None`, the restructuring will99be applied to all python files.100101`checks` argument has been deprecated. Use the `args` argument102of the constructor. The usage of::103104strchecks = {'obj1.type': 'mod.A', 'obj2': 'mod.B',105'obj3.object': 'mod.C'}106checks = restructuring.make_checks(strchecks)107108can be replaced with::109110args = {'obj1': 'type=mod.A', 'obj2': 'name=mod.B',111'obj3': 'object=mod.C'}112113where obj1, obj2 and obj3 are wildcard names that appear114in restructuring pattern.115116"""117if checks is not None:118warnings.warn(119'The use of checks parameter is deprecated; '120'use the args parameter of the constructor instead.',121DeprecationWarning, stacklevel=2)122for name, value in checks.items():123self.args[name] = similarfinder._pydefined_to_str(value)124if imports is not None:125warnings.warn(126'The use of imports parameter is deprecated; '127'use imports parameter of the constructor, instead.',128DeprecationWarning, stacklevel=2)129self.imports = imports130changes = change.ChangeSet('Restructuring <%s> to <%s>' %131(self.pattern, self.goal))132if resources is not None:133files = [resource for resource in resources134if self.pycore.is_python_file(resource)]135else:136files = self.pycore.get_python_files()137job_set = task_handle.create_jobset('Collecting Changes', len(files))138for resource in files:139job_set.started_job(resource.path)140pymodule = self.pycore.resource_to_pyobject(resource)141finder = similarfinder.SimilarFinder(pymodule,142wildcards=self.wildcards)143matches = list(finder.get_matches(self.pattern, self.args))144computer = self._compute_changes(matches, pymodule)145result = computer.get_changed()146if result is not None:147imported_source = self._add_imports(resource, result,148self.imports)149changes.add_change(change.ChangeContents(resource,150imported_source))151job_set.finished_job()152return changes153154def _compute_changes(self, matches, pymodule):155return _ChangeComputer(156pymodule.source_code, pymodule.get_ast(),157pymodule.lines, self.template, matches)158159def _add_imports(self, resource, source, imports):160if not imports:161return source162import_infos = self._get_import_infos(resource, imports)163pymodule = self.pycore.get_string_module(source, resource)164imports = module_imports.ModuleImports(self.pycore, pymodule)165for import_info in import_infos:166imports.add_import(import_info)167return imports.get_changed_source()168169def _get_import_infos(self, resource, imports):170pymodule = self.pycore.get_string_module('\n'.join(imports),171resource)172imports = module_imports.ModuleImports(self.pycore, pymodule)173return [imports.import_info174for imports in imports.imports]175176def make_checks(self, string_checks):177"""Convert str to str dicts to str to PyObject dicts178179This function is here to ease writing a UI.180181"""182checks = {}183for key, value in string_checks.items():184is_pyname = not key.endswith('.object') and \185not key.endswith('.type')186evaluated = self._evaluate(value, is_pyname=is_pyname)187if evaluated is not None:188checks[key] = evaluated189return checks190191def _evaluate(self, code, is_pyname=True):192attributes = code.split('.')193pyname = None194if attributes[0] in ('__builtin__', '__builtins__'):195class _BuiltinsStub(object):196def get_attribute(self, name):197return builtins.builtins[name]198pyobject = _BuiltinsStub()199else:200pyobject = self.pycore.get_module(attributes[0])201for attribute in attributes[1:]:202pyname = pyobject[attribute]203if pyname is None:204return None205pyobject = pyname.get_object()206return pyname if is_pyname else pyobject207208209def replace(code, pattern, goal):210"""used by other refactorings"""211finder = similarfinder.RawSimilarFinder(code)212matches = list(finder.get_matches(pattern))213ast = patchedast.get_patched_ast(code)214lines = codeanalyze.SourceLinesAdapter(code)215template = similarfinder.CodeTemplate(goal)216computer = _ChangeComputer(code, ast, lines, template, matches)217result = computer.get_changed()218if result is None:219return code220return result221222223class _ChangeComputer(object):224225def __init__(self, code, ast, lines, goal, matches):226self.source = code227self.goal = goal228self.matches = matches229self.ast = ast230self.lines = lines231self.matched_asts = {}232self._nearest_roots = {}233if self._is_expression():234for match in self.matches:235self.matched_asts[match.ast] = match236237def get_changed(self):238if self._is_expression():239result = self._get_node_text(self.ast)240if result == self.source:241return None242return result243else:244collector = codeanalyze.ChangeCollector(self.source)245last_end = -1246for match in self.matches:247start, end = match.get_region()248if start < last_end:249if not self._is_expression():250continue251last_end = end252replacement = self._get_matched_text(match)253collector.add_change(start, end, replacement)254return collector.get_changed()255256def _is_expression(self):257return self.matches and isinstance(self.matches[0],258similarfinder.ExpressionMatch)259260def _get_matched_text(self, match):261mapping = {}262for name in self.goal.get_names():263node = match.get_ast(name)264if node is None:265raise similarfinder.BadNameInCheckError(266'Unknown name <%s>' % name)267force = self._is_expression() and match.ast == node268mapping[name] = self._get_node_text(node, force)269unindented = self.goal.substitute(mapping)270return self._auto_indent(match.get_region()[0], unindented)271272def _get_node_text(self, node, force=False):273if not force and node in self.matched_asts:274return self._get_matched_text(self.matched_asts[node])275start, end = patchedast.node_region(node)276main_text = self.source[start:end]277collector = codeanalyze.ChangeCollector(main_text)278for node in self._get_nearest_roots(node):279sub_start, sub_end = patchedast.node_region(node)280collector.add_change(sub_start - start, sub_end - start,281self._get_node_text(node))282result = collector.get_changed()283if result is None:284return main_text285return result286287def _auto_indent(self, offset, text):288lineno = self.lines.get_line_number(offset)289indents = sourceutils.get_indents(self.lines, lineno)290result = []291for index, line in enumerate(text.splitlines(True)):292if index != 0 and line.strip():293result.append(' ' * indents)294result.append(line)295return ''.join(result)296297def _get_nearest_roots(self, node):298if node not in self._nearest_roots:299result = []300for child in ast.get_child_nodes(node):301if child in self.matched_asts:302result.append(child)303else:304result.extend(self._get_nearest_roots(child))305self._nearest_roots[node] = result306return self._nearest_roots[node]307308309