Path: blob/master/elisp/emacs-for-python/rope-dist/rope/refactor/rename.py
1439 views
import warnings12from rope.base import exceptions, pyobjects, pynames, taskhandle, evaluate, worder, codeanalyze3from rope.base.change import ChangeSet, ChangeContents, MoveResource4from rope.refactor import occurrences, sourceutils567class Rename(object):8"""A class for performing rename refactoring910It can rename everything: classes, functions, modules, packages,11methods, variables and keyword arguments.1213"""1415def __init__(self, project, resource, offset=None):16"""If `offset` is None, the `resource` itself will be renamed"""17self.project = project18self.pycore = project.pycore19self.resource = resource20if offset is not None:21self.old_name = worder.get_name_at(self.resource, offset)22this_pymodule = self.pycore.resource_to_pyobject(self.resource)23self.old_instance, self.old_pyname = \24evaluate.eval_location2(this_pymodule, offset)25if self.old_pyname is None:26raise exceptions.RefactoringError(27'Rename refactoring should be performed'28' on resolvable python identifiers.')29else:30if not resource.is_folder() and resource.name == '__init__.py':31resource = resource.parent32dummy_pymodule = self.pycore.get_string_module('')33self.old_instance = None34self.old_pyname = pynames.ImportedModule(dummy_pymodule,35resource=resource)36if resource.is_folder():37self.old_name = resource.name38else:39self.old_name = resource.name[:-3]4041def get_old_name(self):42return self.old_name4344def get_changes(self, new_name, in_file=None, in_hierarchy=False,45unsure=None, docs=False, resources=None,46task_handle=taskhandle.NullTaskHandle()):47"""Get the changes needed for this refactoring4849Parameters:5051- `in_hierarchy`: when renaming a method this keyword forces52to rename all matching methods in the hierarchy53- `docs`: when `True` rename refactoring will rename54occurrences in comments and strings where the name is55visible. Setting it will make renames faster, too.56- `unsure`: decides what to do about unsure occurrences.57If `None`, they are ignored. Otherwise `unsure` is58called with an instance of `occurrence.Occurrence` as59parameter. If it returns `True`, the occurrence is60considered to be a match.61- `resources` can be a list of `rope.base.resources.File`\s to62apply this refactoring on. If `None`, the restructuring63will be applied to all python files.64- `in_file`: this argument has been deprecated; use65`resources` instead.6667"""68if unsure in (True, False):69warnings.warn(70'unsure parameter should be a function that returns '71'True or False', DeprecationWarning, stacklevel=2)72def unsure_func(value=unsure):73return value74unsure = unsure_func75if in_file is not None:76warnings.warn(77'`in_file` argument has been deprecated; use `resources` '78'instead. ', DeprecationWarning, stacklevel=2)79if in_file:80resources = [self.resource]81if _is_local(self.old_pyname):82resources = [self.resource]83if resources is None:84resources = self.pycore.get_python_files()85changes = ChangeSet('Renaming <%s> to <%s>' %86(self.old_name, new_name))87finder = occurrences.create_finder(88self.pycore, self.old_name, self.old_pyname, unsure=unsure,89docs=docs, instance=self.old_instance,90in_hierarchy=in_hierarchy and self.is_method())91job_set = task_handle.create_jobset('Collecting Changes', len(resources))92for file_ in resources:93job_set.started_job(file_.path)94new_content = rename_in_module(finder, new_name, resource=file_)95if new_content is not None:96changes.add_change(ChangeContents(file_, new_content))97job_set.finished_job()98if self._is_renaming_a_module():99resource = self.old_pyname.get_object().get_resource()100if self._is_allowed_to_move(resources, resource):101self._rename_module(resource, new_name, changes)102return changes103104def _is_allowed_to_move(self, resources, resource):105if resource.is_folder():106try:107return resource.get_child('__init__.py') in resources108except exceptions.ResourceNotFoundError:109return False110else:111return resource in resources112113def _is_renaming_a_module(self):114if isinstance(self.old_pyname.get_object(), pyobjects.AbstractModule):115return True116return False117118def is_method(self):119pyname = self.old_pyname120return isinstance(pyname, pynames.DefinedName) and \121isinstance(pyname.get_object(), pyobjects.PyFunction) and \122isinstance(pyname.get_object().parent, pyobjects.PyClass)123124def _rename_module(self, resource, new_name, changes):125if not resource.is_folder():126new_name = new_name + '.py'127parent_path = resource.parent.path128if parent_path == '':129new_location = new_name130else:131new_location = parent_path + '/' + new_name132changes.add_change(MoveResource(resource, new_location))133134135class ChangeOccurrences(object):136"""A class for changing the occurrences of a name in a scope137138This class replaces the occurrences of a name. Note that it only139changes the scope containing the offset passed to the constructor.140What's more it does not have any side-effects. That is for141example changing occurrences of a module does not rename the142module; it merely replaces the occurrences of that module in a143scope with the given expression. This class is useful for144performing many custom refactorings.145146"""147148def __init__(self, project, resource, offset):149self.pycore = project.pycore150self.resource = resource151self.offset = offset152self.old_name = worder.get_name_at(resource, offset)153self.pymodule = self.pycore.resource_to_pyobject(self.resource)154self.old_pyname = evaluate.eval_location(self.pymodule, offset)155156def get_old_name(self):157word_finder = worder.Worder(self.resource.read())158return word_finder.get_primary_at(self.offset)159160def _get_scope_offset(self):161lines = self.pymodule.lines162scope = self.pymodule.get_scope().\163get_inner_scope_for_line(lines.get_line_number(self.offset))164start = lines.get_line_start(scope.get_start())165end = lines.get_line_end(scope.get_end())166return start, end167168def get_changes(self, new_name, only_calls=False, reads=True, writes=True):169changes = ChangeSet('Changing <%s> occurrences to <%s>' %170(self.old_name, new_name))171scope_start, scope_end = self._get_scope_offset()172finder = occurrences.create_finder(173self.pycore, self.old_name, self.old_pyname,174imports=False, only_calls=only_calls)175new_contents = rename_in_module(176finder, new_name, pymodule=self.pymodule, replace_primary=True,177region=(scope_start, scope_end), reads=reads, writes=writes)178if new_contents is not None:179changes.add_change(ChangeContents(self.resource, new_contents))180return changes181182183def rename_in_module(occurrences_finder, new_name, resource=None, pymodule=None,184replace_primary=False, region=None, reads=True, writes=True):185"""Returns the changed source or `None` if there is no changes"""186if resource is not None:187source_code = resource.read()188else:189source_code = pymodule.source_code190change_collector = codeanalyze.ChangeCollector(source_code)191for occurrence in occurrences_finder.find_occurrences(resource, pymodule):192if replace_primary and occurrence.is_a_fixed_primary():193continue194if replace_primary:195start, end = occurrence.get_primary_range()196else:197start, end = occurrence.get_word_range()198if (not reads and not occurrence.is_written()) or \199(not writes and occurrence.is_written()):200continue201if region is None or region[0] <= start < region[1]:202change_collector.add_change(start, end, new_name)203return change_collector.get_changed()204205def _is_local(pyname):206module, lineno = pyname.get_definition_location()207if lineno is None:208return False209scope = module.get_scope().get_inner_scope_for_line(lineno)210if isinstance(pyname, pynames.DefinedName) and \211scope.get_kind() in ('Function', 'Class'):212scope = scope.parent213return scope.get_kind() == 'Function' and \214pyname in scope.get_names().values() and \215isinstance(pyname, pynames.AssignedName)216217218