Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/move.py
1415 views
"""A module containing classes for move refactoring12`create_move()` is a factory for creating move refactoring objects3based on inputs.45"""6from rope.base import pyobjects, codeanalyze, exceptions, pynames, taskhandle, evaluate, worder7from rope.base.change import ChangeSet, ChangeContents, MoveResource8from rope.refactor import importutils, rename, occurrences, sourceutils, functionutils91011def create_move(project, resource, offset=None):12"""A factory for creating Move objects1314Based on `resource` and `offset`, return one of `MoveModule`,15`MoveGlobal` or `MoveMethod` for performing move refactoring.1617"""18if offset is None:19return MoveModule(project, resource)20this_pymodule = project.pycore.resource_to_pyobject(resource)21pyname = evaluate.eval_location(this_pymodule, offset)22if pyname is None:23raise exceptions.RefactoringError(24'Move only works on classes, functions, modules and methods.')25pyobject = pyname.get_object()26if isinstance(pyobject, pyobjects.PyModule) or \27isinstance(pyobject, pyobjects.PyPackage):28return MoveModule(project, pyobject.get_resource())29if isinstance(pyobject, pyobjects.PyFunction) and \30isinstance(pyobject.parent, pyobjects.PyClass):31return MoveMethod(project, resource, offset)32if isinstance(pyobject, pyobjects.PyDefinedObject) and \33isinstance(pyobject.parent, pyobjects.PyModule):34return MoveGlobal(project, resource, offset)35raise exceptions.RefactoringError(36'Move only works on global classes/functions, modules and methods.')373839class MoveMethod(object):40"""For moving methods4142It makes a new method in the destination class and changes43the body of the old method to call the new method. You can44inline the old method to change all of its occurrences.4546"""4748def __init__(self, project, resource, offset):49self.project = project50self.pycore = project.pycore51this_pymodule = self.pycore.resource_to_pyobject(resource)52pyname = evaluate.eval_location(this_pymodule, offset)53self.method_name = worder.get_name_at(resource, offset)54self.pyfunction = pyname.get_object()55if self.pyfunction.get_kind() != 'method':56raise exceptions.RefactoringError('Only normal methods'57' can be moved.')5859def get_changes(self, dest_attr, new_name=None, resources=None,60task_handle=taskhandle.NullTaskHandle()):61"""Return the changes needed for this refactoring6263Parameters:6465- `dest_attr`: the name of the destination attribute66- `new_name`: the name of the new method; if `None` uses67the old name68- `resources` can be a list of `rope.base.resources.File`\s to69apply this refactoring on. If `None`, the restructuring70will be applied to all python files.7172"""73changes = ChangeSet('Moving method <%s>' % self.method_name)74if resources is None:75resources = self.pycore.get_python_files()76if new_name is None:77new_name = self.get_method_name()78resource1, start1, end1, new_content1 = \79self._get_changes_made_by_old_class(dest_attr, new_name)80collector1 = codeanalyze.ChangeCollector(resource1.read())81collector1.add_change(start1, end1, new_content1)8283resource2, start2, end2, new_content2 = \84self._get_changes_made_by_new_class(dest_attr, new_name)85if resource1 == resource2:86collector1.add_change(start2, end2, new_content2)87else:88collector2 = codeanalyze.ChangeCollector(resource2.read())89collector2.add_change(start2, end2, new_content2)90result = collector2.get_changed()91import_tools = importutils.ImportTools(self.pycore)92new_imports = self._get_used_imports(import_tools)93if new_imports:94goal_pymodule = self.pycore.get_string_module(result,95resource2)96result = _add_imports_to_module(97import_tools, goal_pymodule, new_imports)98if resource2 in resources:99changes.add_change(ChangeContents(resource2, result))100101if resource1 in resources:102changes.add_change(ChangeContents(resource1,103collector1.get_changed()))104return changes105106def get_method_name(self):107return self.method_name108109def _get_used_imports(self, import_tools):110return importutils.get_imports(self.pycore, self.pyfunction)111112def _get_changes_made_by_old_class(self, dest_attr, new_name):113pymodule = self.pyfunction.get_module()114indents = self._get_scope_indents(self.pyfunction)115body = 'return self.%s.%s(%s)\n' % (dest_attr, new_name,116self._get_passed_arguments_string())117region = sourceutils.get_body_region(self.pyfunction)118return (pymodule.get_resource(), region[0], region[1],119sourceutils.fix_indentation(body, indents))120121def _get_scope_indents(self, pyobject):122pymodule = pyobject.get_module()123return sourceutils.get_indents(124pymodule.lines, pyobject.get_scope().get_start()) + \125sourceutils.get_indent(self.pycore)126127def _get_changes_made_by_new_class(self, dest_attr, new_name):128old_pyclass = self.pyfunction.parent129if dest_attr not in old_pyclass:130raise exceptions.RefactoringError(131'Destination attribute <%s> not found' % dest_attr)132pyclass = old_pyclass[dest_attr].get_object().get_type()133if not isinstance(pyclass, pyobjects.PyClass):134raise exceptions.RefactoringError(135'Unknown class type for attribute <%s>' % dest_attr)136pymodule = pyclass.get_module()137resource = pyclass.get_module().get_resource()138start, end = sourceutils.get_body_region(pyclass)139pre_blanks = '\n'140if pymodule.source_code[start:end].strip() != 'pass':141pre_blanks = '\n\n'142start = end143indents = self._get_scope_indents(pyclass)144body = pre_blanks + sourceutils.fix_indentation(145self.get_new_method(new_name), indents)146return resource, start, end, body147148def get_new_method(self, name):149return '%s\n%s' % (150self._get_new_header(name),151sourceutils.fix_indentation(self._get_body(),152sourceutils.get_indent(self.pycore)))153154def _get_unchanged_body(self):155return sourceutils.get_body(self.pyfunction)156157def _get_body(self, host='host'):158self_name = self._get_self_name()159body = self_name + ' = None\n' + self._get_unchanged_body()160pymodule = self.pycore.get_string_module(body)161finder = occurrences.create_finder(162self.pycore, self_name, pymodule[self_name])163result = rename.rename_in_module(finder, host, pymodule=pymodule)164if result is None:165result = body166return result[result.index('\n') + 1:]167168def _get_self_name(self):169return self.pyfunction.get_param_names()[0]170171def _get_new_header(self, name):172header = 'def %s(self' % name173if self._is_host_used():174header += ', host'175definition_info = functionutils.DefinitionInfo.read(self.pyfunction)176others = definition_info.arguments_to_string(1)177if others:178header += ', ' + others179return header + '):'180181def _get_passed_arguments_string(self):182result = ''183if self._is_host_used():184result = 'self'185definition_info = functionutils.DefinitionInfo.read(self.pyfunction)186others = definition_info.arguments_to_string(1)187if others:188if result:189result += ', '190result += others191return result192193def _is_host_used(self):194return self._get_body('__old_self') != self._get_unchanged_body()195196197class MoveGlobal(object):198"""For moving global function and classes"""199200def __init__(self, project, resource, offset):201self.pycore = project.pycore202this_pymodule = self.pycore.resource_to_pyobject(resource)203self.old_pyname = evaluate.eval_location(this_pymodule, offset)204self.old_name = self.old_pyname.get_object().get_name()205pymodule = self.old_pyname.get_object().get_module()206self.source = pymodule.get_resource()207self.tools = _MoveTools(self.pycore, self.source,208self.old_pyname, self.old_name)209self.import_tools = self.tools.import_tools210self._check_exceptional_conditions()211212def _check_exceptional_conditions(self):213if self.old_pyname is None or \214not isinstance(self.old_pyname.get_object(), pyobjects.PyDefinedObject):215raise exceptions.RefactoringError(216'Move refactoring should be performed on a class/function.')217moving_pyobject = self.old_pyname.get_object()218if not self._is_global(moving_pyobject):219raise exceptions.RefactoringError(220'Move refactoring should be performed on a global class/function.')221222def _is_global(self, pyobject):223return pyobject.get_scope().parent == pyobject.get_module().get_scope()224225def get_changes(self, dest, resources=None,226task_handle=taskhandle.NullTaskHandle()):227if resources is None:228resources = self.pycore.get_python_files()229if dest is None or not dest.exists():230raise exceptions.RefactoringError(231'Move destination does not exist.')232if dest.is_folder() and dest.has_child('__init__.py'):233dest = dest.get_child('__init__.py')234if dest.is_folder():235raise exceptions.RefactoringError(236'Move destination for non-modules should not be folders.')237if self.source == dest:238raise exceptions.RefactoringError(239'Moving global elements to the same module.')240return self._calculate_changes(dest, resources, task_handle)241242def _calculate_changes(self, dest, resources, task_handle):243changes = ChangeSet('Moving global <%s>' % self.old_name)244job_set = task_handle.create_jobset('Collecting Changes',245len(resources))246for file_ in resources:247job_set.started_job(file_.path)248if file_ == self.source:249changes.add_change(self._source_module_changes(dest))250elif file_ == dest:251changes.add_change(self._dest_module_changes(dest))252elif self.tools.occurs_in_module(resource=file_):253pymodule = self.pycore.resource_to_pyobject(file_)254# Changing occurrences255placeholder = '__rope_renaming_%s_' % self.old_name256source = self.tools.rename_in_module(placeholder,257resource=file_)258should_import = source is not None259# Removing out of date imports260pymodule = self.tools.new_pymodule(pymodule, source)261source = self.tools.remove_old_imports(pymodule)262# Adding new import263if should_import:264pymodule = self.tools.new_pymodule(pymodule, source)265source, imported = importutils.add_import(266self.pycore, pymodule, self._new_modname(dest), self.old_name)267source = source.replace(placeholder, imported)268source = self.tools.new_source(pymodule, source)269if source != file_.read():270changes.add_change(ChangeContents(file_, source))271job_set.finished_job()272return changes273274def _source_module_changes(self, dest):275placeholder = '__rope_moving_%s_' % self.old_name276handle = _ChangeMoveOccurrencesHandle(placeholder)277occurrence_finder = occurrences.create_finder(278self.pycore, self.old_name, self.old_pyname)279start, end = self._get_moving_region()280renamer = ModuleSkipRenamer(occurrence_finder, self.source,281handle, start, end)282source = renamer.get_changed_module()283if handle.occurred:284pymodule = self.pycore.get_string_module(source, self.source)285# Adding new import286source, imported = importutils.add_import(287self.pycore, pymodule, self._new_modname(dest), self.old_name)288source = source.replace(placeholder, imported)289return ChangeContents(self.source, source)290291def _new_modname(self, dest):292return self.pycore.modname(dest)293294def _dest_module_changes(self, dest):295# Changing occurrences296pymodule = self.pycore.resource_to_pyobject(dest)297source = self.tools.rename_in_module(self.old_name, pymodule)298pymodule = self.tools.new_pymodule(pymodule, source)299300moving, imports = self._get_moving_element_with_imports()301source = self.tools.remove_old_imports(pymodule)302pymodule = self.tools.new_pymodule(pymodule, source)303pymodule, has_changed = self._add_imports2(pymodule, imports)304305module_with_imports = self.import_tools.module_imports(pymodule)306source = pymodule.source_code307if module_with_imports.imports:308start = pymodule.lines.get_line_end(309module_with_imports.imports[-1].end_line - 1)310result = source[:start + 1] + '\n\n'311else:312result = ''313start = -1314result += moving + source[start + 1:]315316# Organizing imports317source = result318pymodule = self.pycore.get_string_module(source, dest)319source = self.import_tools.organize_imports(pymodule, sort=False,320unused=False)321return ChangeContents(dest, source)322323def _get_moving_element_with_imports(self):324return moving_code_with_imports(325self.pycore, self.source, self._get_moving_element())326327def _get_module_with_imports(self, source_code, resource):328pymodule = self.pycore.get_string_module(source_code, resource)329return self.import_tools.module_imports(pymodule)330331def _get_moving_element(self):332start, end = self._get_moving_region()333moving = self.source.read()[start:end]334return moving.rstrip() + '\n'335336def _get_moving_region(self):337pymodule = self.pycore.resource_to_pyobject(self.source)338lines = pymodule.lines339scope = self.old_pyname.get_object().get_scope()340start = lines.get_line_start(scope.get_start())341end_line = scope.get_end()342while end_line < lines.length() and \343lines.get_line(end_line + 1).strip() == '':344end_line += 1345end = min(lines.get_line_end(end_line) + 1, len(pymodule.source_code))346return start, end347348def _add_imports2(self, pymodule, new_imports):349source = self.tools.add_imports(pymodule, new_imports)350if source is None:351return pymodule, False352else:353resource = pymodule.get_resource()354pymodule = self.pycore.get_string_module(source, resource)355return pymodule, True356357358class MoveModule(object):359"""For moving modules and packages"""360361def __init__(self, project, resource):362self.project = project363self.pycore = project.pycore364if not resource.is_folder() and resource.name == '__init__.py':365resource = resource.parent366if resource.is_folder() and not resource.has_child('__init__.py'):367raise exceptions.RefactoringError(368'Cannot move non-package folder.')369dummy_pymodule = self.pycore.get_string_module('')370self.old_pyname = pynames.ImportedModule(dummy_pymodule,371resource=resource)372self.source = self.old_pyname.get_object().get_resource()373if self.source.is_folder():374self.old_name = self.source.name375else:376self.old_name = self.source.name[:-3]377self.tools = _MoveTools(self.pycore, self.source,378self.old_pyname, self.old_name)379self.import_tools = self.tools.import_tools380381def get_changes(self, dest, resources=None,382task_handle=taskhandle.NullTaskHandle()):383moving_pyobject = self.old_pyname.get_object()384if resources is None:385resources = self.pycore.get_python_files()386if dest is None or not dest.is_folder():387raise exceptions.RefactoringError(388'Move destination for modules should be packages.')389return self._calculate_changes(dest, resources, task_handle)390391def _calculate_changes(self, dest, resources, task_handle):392changes = ChangeSet('Moving module <%s>' % self.old_name)393job_set = task_handle.create_jobset('Collecting changes',394len(resources))395for module in resources:396job_set.started_job(module.path)397if module == self.source:398self._change_moving_module(changes, dest)399else:400source = self._change_occurrences_in_module(dest,401resource=module)402if source is not None:403changes.add_change(ChangeContents(module, source))404job_set.finished_job()405if self.project == self.source.project:406changes.add_change(MoveResource(self.source, dest.path))407return changes408409def _new_modname(self, dest):410destname = self.pycore.modname(dest)411if destname:412return destname + '.' + self.old_name413return self.old_name414415def _new_import(self, dest):416return importutils.NormalImport([(self._new_modname(dest), None)])417418def _change_moving_module(self, changes, dest):419if not self.source.is_folder():420pymodule = self.pycore.resource_to_pyobject(self.source)421source = self.import_tools.relatives_to_absolutes(pymodule)422pymodule = self.tools.new_pymodule(pymodule, source)423source = self._change_occurrences_in_module(dest, pymodule)424source = self.tools.new_source(pymodule, source)425if source != self.source.read():426changes.add_change(ChangeContents(self.source, source))427428def _change_occurrences_in_module(self, dest, pymodule=None,429resource=None):430if not self.tools.occurs_in_module(pymodule=pymodule,431resource=resource):432return433if pymodule is None:434pymodule = self.pycore.resource_to_pyobject(resource)435new_name = self._new_modname(dest)436new_import = self._new_import(dest)437source = self.tools.rename_in_module(438new_name, imports=True, pymodule=pymodule, resource=resource)439should_import = self.tools.occurs_in_module(440pymodule=pymodule, resource=resource, imports=False)441pymodule = self.tools.new_pymodule(pymodule, source)442source = self.tools.remove_old_imports(pymodule)443if should_import:444pymodule = self.tools.new_pymodule(pymodule, source)445source = self.tools.add_imports(pymodule, [new_import])446source = self.tools.new_source(pymodule, source)447if source != pymodule.resource.read():448return source449450451class _ChangeMoveOccurrencesHandle(object):452453def __init__(self, new_name):454self.new_name = new_name455self.occurred = False456457def occurred_inside_skip(self, change_collector, occurrence):458pass459460def occurred_outside_skip(self, change_collector, occurrence):461start, end = occurrence.get_primary_range()462change_collector.add_change(start, end, self.new_name)463self.occurred = True464465466class _MoveTools(object):467468def __init__(self, pycore, source, pyname, old_name):469self.pycore = pycore470self.source = source471self.old_pyname = pyname472self.old_name = old_name473self.import_tools = importutils.ImportTools(self.pycore)474475def remove_old_imports(self, pymodule):476old_source = pymodule.source_code477module_with_imports = self.import_tools.module_imports(pymodule)478class CanSelect(object):479changed = False480old_name = self.old_name481old_pyname = self.old_pyname482def __call__(self, name):483try:484if name == self.old_name and \485pymodule[name].get_object() == \486self.old_pyname.get_object():487self.changed = True488return False489except exceptions.AttributeNotFoundError:490pass491return True492can_select = CanSelect()493module_with_imports.filter_names(can_select)494new_source = module_with_imports.get_changed_source()495if old_source != new_source:496return new_source497498def rename_in_module(self, new_name, pymodule=None,499imports=False, resource=None):500occurrence_finder = self._create_finder(imports)501source = rename.rename_in_module(502occurrence_finder, new_name, replace_primary=True,503pymodule=pymodule, resource=resource)504return source505506def occurs_in_module(self, pymodule=None, resource=None, imports=True):507finder = self._create_finder(imports)508for occurrence in finder.find_occurrences(pymodule=pymodule,509resource=resource):510return True511return False512513def _create_finder(self, imports):514return occurrences.create_finder(self.pycore, self.old_name,515self.old_pyname, imports=imports)516517def new_pymodule(self, pymodule, source):518if source is not None:519return self.pycore.get_string_module(520source, pymodule.get_resource())521return pymodule522523def new_source(self, pymodule, source):524if source is None:525return pymodule.source_code526return source527528def add_imports(self, pymodule, new_imports):529return _add_imports_to_module(self.import_tools, pymodule, new_imports)530531532def _add_imports_to_module(import_tools, pymodule, new_imports):533module_with_imports = import_tools.module_imports(pymodule)534for new_import in new_imports:535module_with_imports.add_import(new_import)536return module_with_imports.get_changed_source()537538539def moving_code_with_imports(pycore, resource, source):540import_tools = importutils.ImportTools(pycore)541pymodule = pycore.get_string_module(source, resource)542origin = pycore.resource_to_pyobject(resource)543544imports = []545for stmt in import_tools.module_imports(origin).imports:546imports.append(stmt.import_info)547548back_names = []549for name in origin:550if name not in pymodule:551back_names.append(name)552imports.append(import_tools.get_from_import(resource, back_names))553554source = _add_imports_to_module(import_tools, pymodule, imports)555pymodule = pycore.get_string_module(source, resource)556557source = import_tools.relatives_to_absolutes(pymodule)558pymodule = pycore.get_string_module(source, resource)559source = import_tools.organize_imports(pymodule, selfs=False)560pymodule = pycore.get_string_module(source, resource)561562# extracting imports after changes563module_imports = import_tools.module_imports(pymodule)564imports = [import_stmt.import_info565for import_stmt in module_imports.imports]566start = 1567if module_imports.imports:568start = module_imports.imports[-1].end_line569lines = codeanalyze.SourceLinesAdapter(source)570while start < lines.length() and not lines.get_line(start).strip():571start += 1572moving = source[lines.get_line_start(start):]573return moving, imports574575576class ModuleSkipRenamerHandle(object):577578def occurred_outside_skip(self, change_collector, occurrence):579pass580581def occurred_inside_skip(self, change_collector, occurrence):582pass583584585class ModuleSkipRenamer(object):586"""Rename occurrences in a module587588This class can be used when you want to treat a region in a file589separately from other parts when renaming.590591"""592593def __init__(self, occurrence_finder, resource, handle=None,594skip_start=0, skip_end=0, replacement=''):595"""Constructor596597if replacement is `None` the region is not changed. Otherwise598it is replaced with `replacement`.599600"""601self.occurrence_finder = occurrence_finder602self.resource = resource603self.skip_start = skip_start604self.skip_end = skip_end605self.replacement = replacement606self.handle = handle607if self.handle is None:608self.handle = ModuleSkipHandle()609610def get_changed_module(self):611source = self.resource.read()612change_collector = codeanalyze.ChangeCollector(source)613if self.replacement is not None:614change_collector.add_change(self.skip_start, self.skip_end,615self.replacement)616for occurrence in self.occurrence_finder.find_occurrences(self.resource):617start, end = occurrence.get_primary_range()618if self.skip_start <= start < self.skip_end:619self.handle.occurred_inside_skip(change_collector, occurrence)620else:621self.handle.occurred_outside_skip(change_collector, occurrence)622result = change_collector.get_changed()623if result is not None and result != source:624return result625626627