import warnings
from rope.base import change,taskhandle,builtins,ast,codeanalyze
from rope.refactor import patchedast,similarfinder,sourceutils
from rope.refactor.importutils import module_imports
class Restructure(object):
"""A class to perform python restructurings
A restructuring transforms pieces of code matching `pattern` to
`goal`. In the `pattern` wildcards can appear. Wildcards match
some piece of code based on their kind and arguments that are
passed to them through `args`.
`args` is a dictionary of wildcard names to wildcard arguments.
If the argument is a tuple, the first item of the tuple is
considered to be the name of the wildcard to use; otherwise the
"default" wildcard is used. For getting the list arguments a
wildcard supports, see the pydoc of the wildcard. (see
`rope.refactor.wildcard.DefaultWildcard` for the default
wildcard.)
`wildcards` is the list of wildcard types that can appear in
`pattern`. See `rope.refactor.wildcards`. If a wildcard does not
specify its kind (by using a tuple in args), the wildcard named
"default" is used. So there should be a wildcard with "default"
name in `wildcards`.
`imports` is the list of imports that changed modules should
import. Note that rope handles duplicate imports and does not add
the import if it already appears.
Example #1::
pattern ${pyobject}.get_attribute(${name})
goal ${pyobject}[${name}]
args pyobject: instance=rope.base.pyobjects.PyObject
Example #2::
pattern ${name} in ${pyobject}.get_attributes()
goal ${name} in {pyobject}
args pyobject: instance=rope.base.pyobjects.PyObject
Example #3::
pattern ${pycore}.create_module(${project}.root, ${name})
goal generate.create_module(${project}, ${name})
imports
from rope.contrib import generate
args
pycore: type=rope.base.pycore.PyCore
project: type=rope.base.project.Project
Example #4::
pattern ${pow}(${param1}, ${param2})
goal ${param1} ** ${param2}
args pow: name=mod.pow, exact
Example #5::
pattern ${inst}.longtask(${p1}, ${p2})
goal
${inst}.subtask1(${p1})
${inst}.subtask2(${p2})
args
inst: type=mod.A,unsure
"""
def __init__(self, project, pattern, goal, args=None,
imports=None, wildcards=None):
"""Construct a restructuring
See class pydoc for more info about the arguments.
"""
self.pycore = project.pycore
self.pattern = pattern
self.goal = goal
self.args = args
if self.args is None:
self.args = {}
self.imports = imports
if self.imports is None:
self.imports = []
self.wildcards = wildcards
self.template = similarfinder.CodeTemplate(self.goal)
def get_changes(self, checks=None, imports=None, resources=None,
task_handle=taskhandle.NullTaskHandle()):
"""Get the changes needed by this restructuring
`resources` can be a list of `rope.base.resources.File`\s to
apply the restructuring on. If `None`, the restructuring will
be applied to all python files.
`checks` argument has been deprecated. Use the `args` argument
of the constructor. The usage of::
strchecks = {'obj1.type': 'mod.A', 'obj2': 'mod.B',
'obj3.object': 'mod.C'}
checks = restructuring.make_checks(strchecks)
can be replaced with::
args = {'obj1': 'type=mod.A', 'obj2': 'name=mod.B',
'obj3': 'object=mod.C'}
where obj1, obj2 and obj3 are wildcard names that appear
in restructuring pattern.
"""
if checks is not None:
warnings.warn(
'The use of checks parameter is deprecated; '
'use the args parameter of the constructor instead.',
DeprecationWarning, stacklevel=2)
for name, value in checks.items():
self.args[name] = similarfinder._pydefined_to_str(value)
if imports is not None:
warnings.warn(
'The use of imports parameter is deprecated; '
'use imports parameter of the constructor, instead.',
DeprecationWarning, stacklevel=2)
self.imports = imports
changes = change.ChangeSet('Restructuring <%s> to <%s>' %
(self.pattern, self.goal))
if resources is not None:
files = [resource for resource in resources
if self.pycore.is_python_file(resource)]
else:
files = self.pycore.get_python_files()
job_set = task_handle.create_jobset('Collecting Changes', len(files))
for resource in files:
job_set.started_job(resource.path)
pymodule = self.pycore.resource_to_pyobject(resource)
finder = similarfinder.SimilarFinder(pymodule,
wildcards=self.wildcards)
matches = list(finder.get_matches(self.pattern, self.args))
computer = self._compute_changes(matches, pymodule)
result = computer.get_changed()
if result is not None:
imported_source = self._add_imports(resource, result,
self.imports)
changes.add_change(change.ChangeContents(resource,
imported_source))
job_set.finished_job()
return changes
def _compute_changes(self, matches, pymodule):
return _ChangeComputer(
pymodule.source_code, pymodule.get_ast(),
pymodule.lines, self.template, matches)
def _add_imports(self, resource, source, imports):
if not imports:
return source
import_infos = self._get_import_infos(resource, imports)
pymodule = self.pycore.get_string_module(source, resource)
imports = module_imports.ModuleImports(self.pycore, pymodule)
for import_info in import_infos:
imports.add_import(import_info)
return imports.get_changed_source()
def _get_import_infos(self, resource, imports):
pymodule = self.pycore.get_string_module('\n'.join(imports),
resource)
imports = module_imports.ModuleImports(self.pycore, pymodule)
return [imports.import_info
for imports in imports.imports]
def make_checks(self, string_checks):
"""Convert str to str dicts to str to PyObject dicts
This function is here to ease writing a UI.
"""
checks = {}
for key, value in string_checks.items():
is_pyname = not key.endswith('.object') and \
not key.endswith('.type')
evaluated = self._evaluate(value, is_pyname=is_pyname)
if evaluated is not None:
checks[key] = evaluated
return checks
def _evaluate(self, code, is_pyname=True):
attributes = code.split('.')
pyname = None
if attributes[0] in ('__builtin__', '__builtins__'):
class _BuiltinsStub(object):
def get_attribute(self, name):
return builtins.builtins[name]
pyobject = _BuiltinsStub()
else:
pyobject = self.pycore.get_module(attributes[0])
for attribute in attributes[1:]:
pyname = pyobject[attribute]
if pyname is None:
return None
pyobject = pyname.get_object()
return pyname if is_pyname else pyobject
def replace(code, pattern, goal):
"""used by other refactorings"""
finder = similarfinder.RawSimilarFinder(code)
matches = list(finder.get_matches(pattern))
ast = patchedast.get_patched_ast(code)
lines = codeanalyze.SourceLinesAdapter(code)
template = similarfinder.CodeTemplate(goal)
computer = _ChangeComputer(code, ast, lines, template, matches)
result = computer.get_changed()
if result is None:
return code
return result
class _ChangeComputer(object):
def __init__(self, code, ast, lines, goal, matches):
self.source = code
self.goal = goal
self.matches = matches
self.ast = ast
self.lines = lines
self.matched_asts = {}
self._nearest_roots = {}
if self._is_expression():
for match in self.matches:
self.matched_asts[match.ast] = match
def get_changed(self):
if self._is_expression():
result = self._get_node_text(self.ast)
if result == self.source:
return None
return result
else:
collector = codeanalyze.ChangeCollector(self.source)
last_end = -1
for match in self.matches:
start, end = match.get_region()
if start < last_end:
if not self._is_expression():
continue
last_end = end
replacement = self._get_matched_text(match)
collector.add_change(start, end, replacement)
return collector.get_changed()
def _is_expression(self):
return self.matches and isinstance(self.matches[0],
similarfinder.ExpressionMatch)
def _get_matched_text(self, match):
mapping = {}
for name in self.goal.get_names():
node = match.get_ast(name)
if node is None:
raise similarfinder.BadNameInCheckError(
'Unknown name <%s>' % name)
force = self._is_expression() and match.ast == node
mapping[name] = self._get_node_text(node, force)
unindented = self.goal.substitute(mapping)
return self._auto_indent(match.get_region()[0], unindented)
def _get_node_text(self, node, force=False):
if not force and node in self.matched_asts:
return self._get_matched_text(self.matched_asts[node])
start, end = patchedast.node_region(node)
main_text = self.source[start:end]
collector = codeanalyze.ChangeCollector(main_text)
for node in self._get_nearest_roots(node):
sub_start, sub_end = patchedast.node_region(node)
collector.add_change(sub_start - start, sub_end - start,
self._get_node_text(node))
result = collector.get_changed()
if result is None:
return main_text
return result
def _auto_indent(self, offset, text):
lineno = self.lines.get_line_number(offset)
indents = sourceutils.get_indents(self.lines, lineno)
result = []
for index, line in enumerate(text.splitlines(True)):
if index != 0 and line.strip():
result.append(' ' * indents)
result.append(line)
return ''.join(result)
def _get_nearest_roots(self, node):
if node not in self._nearest_roots:
result = []
for child in ast.get_child_nodes(node):
if child in self.matched_asts:
result.append(child)
else:
result.extend(self._get_nearest_roots(child))
self._nearest_roots[node] = result
return self._nearest_roots[node]
|