import warnings from rope.base import exceptions, pyobjects, pynames, taskhandle, evaluate, worder, codeanalyze from rope.base.change import ChangeSet, ChangeContents, MoveResource from rope.refactor import occurrences, sourceutils class Rename(object): """A class for performing rename refactoring It can rename everything: classes, functions, modules, packages, methods, variables and keyword arguments. """ def __init__(self, project, resource, offset=None): """If `offset` is None, the `resource` itself will be renamed""" self.project = project self.pycore = project.pycore self.resource = resource if offset is not None: self.old_name = worder.get_name_at(self.resource, offset) this_pymodule = self.pycore.resource_to_pyobject(self.resource) self.old_instance, self.old_pyname = \ evaluate.eval_location2(this_pymodule, offset) if self.old_pyname is None: raise exceptions.RefactoringError( 'Rename refactoring should be performed' ' on resolvable python identifiers.') else: if not resource.is_folder() and resource.name == '__init__.py': resource = resource.parent dummy_pymodule = self.pycore.get_string_module('') self.old_instance = None self.old_pyname = pynames.ImportedModule(dummy_pymodule, resource=resource) if resource.is_folder(): self.old_name = resource.name else: self.old_name = resource.name[:-3] def get_old_name(self): return self.old_name def get_changes(self, new_name, in_file=None, in_hierarchy=False, unsure=None, docs=False, resources=None, task_handle=taskhandle.NullTaskHandle()): """Get the changes needed for this refactoring Parameters: - `in_hierarchy`: when renaming a method this keyword forces to rename all matching methods in the hierarchy - `docs`: when `True` rename refactoring will rename occurrences in comments and strings where the name is visible. Setting it will make renames faster, too. - `unsure`: decides what to do about unsure occurrences. If `None`, they are ignored. Otherwise `unsure` is called with an instance of `occurrence.Occurrence` as parameter. If it returns `True`, the occurrence is considered to be a match. - `resources` can be a list of `rope.base.resources.File`\s to apply this refactoring on. If `None`, the restructuring will be applied to all python files. - `in_file`: this argument has been deprecated; use `resources` instead. """ if unsure in (True, False): warnings.warn( 'unsure parameter should be a function that returns ' 'True or False', DeprecationWarning, stacklevel=2) def unsure_func(value=unsure): return value unsure = unsure_func if in_file is not None: warnings.warn( '`in_file` argument has been deprecated; use `resources` ' 'instead. ', DeprecationWarning, stacklevel=2) if in_file: resources = [self.resource] if _is_local(self.old_pyname): resources = [self.resource] if resources is None: resources = self.pycore.get_python_files() changes = ChangeSet('Renaming <%s> to <%s>' % (self.old_name, new_name)) finder = occurrences.create_finder( self.pycore, self.old_name, self.old_pyname, unsure=unsure, docs=docs, instance=self.old_instance, in_hierarchy=in_hierarchy and self.is_method()) job_set = task_handle.create_jobset('Collecting Changes', len(resources)) for file_ in resources: job_set.started_job(file_.path) new_content = rename_in_module(finder, new_name, resource=file_) if new_content is not None: changes.add_change(ChangeContents(file_, new_content)) job_set.finished_job() if self._is_renaming_a_module(): resource = self.old_pyname.get_object().get_resource() if self._is_allowed_to_move(resources, resource): self._rename_module(resource, new_name, changes) return changes def _is_allowed_to_move(self, resources, resource): if resource.is_folder(): try: return resource.get_child('__init__.py') in resources except exceptions.ResourceNotFoundError: return False else: return resource in resources def _is_renaming_a_module(self): if isinstance(self.old_pyname.get_object(), pyobjects.AbstractModule): return True return False def is_method(self): pyname = self.old_pyname return isinstance(pyname, pynames.DefinedName) and \ isinstance(pyname.get_object(), pyobjects.PyFunction) and \ isinstance(pyname.get_object().parent, pyobjects.PyClass) def _rename_module(self, resource, new_name, changes): if not resource.is_folder(): new_name = new_name + '.py' parent_path = resource.parent.path if parent_path == '': new_location = new_name else: new_location = parent_path + '/' + new_name changes.add_change(MoveResource(resource, new_location)) class ChangeOccurrences(object): """A class for changing the occurrences of a name in a scope This class replaces the occurrences of a name. Note that it only changes the scope containing the offset passed to the constructor. What's more it does not have any side-effects. That is for example changing occurrences of a module does not rename the module; it merely replaces the occurrences of that module in a scope with the given expression. This class is useful for performing many custom refactorings. """ def __init__(self, project, resource, offset): self.pycore = project.pycore self.resource = resource self.offset = offset self.old_name = worder.get_name_at(resource, offset) self.pymodule = self.pycore.resource_to_pyobject(self.resource) self.old_pyname = evaluate.eval_location(self.pymodule, offset) def get_old_name(self): word_finder = worder.Worder(self.resource.read()) return word_finder.get_primary_at(self.offset) def _get_scope_offset(self): lines = self.pymodule.lines scope = self.pymodule.get_scope().\ get_inner_scope_for_line(lines.get_line_number(self.offset)) start = lines.get_line_start(scope.get_start()) end = lines.get_line_end(scope.get_end()) return start, end def get_changes(self, new_name, only_calls=False, reads=True, writes=True): changes = ChangeSet('Changing <%s> occurrences to <%s>' % (self.old_name, new_name)) scope_start, scope_end = self._get_scope_offset() finder = occurrences.create_finder( self.pycore, self.old_name, self.old_pyname, imports=False, only_calls=only_calls) new_contents = rename_in_module( finder, new_name, pymodule=self.pymodule, replace_primary=True, region=(scope_start, scope_end), reads=reads, writes=writes) if new_contents is not None: changes.add_change(ChangeContents(self.resource, new_contents)) return changes def rename_in_module(occurrences_finder, new_name, resource=None, pymodule=None, replace_primary=False, region=None, reads=True, writes=True): """Returns the changed source or `None` if there is no changes""" if resource is not None: source_code = resource.read() else: source_code = pymodule.source_code change_collector = codeanalyze.ChangeCollector(source_code) for occurrence in occurrences_finder.find_occurrences(resource, pymodule): if replace_primary and occurrence.is_a_fixed_primary(): continue if replace_primary: start, end = occurrence.get_primary_range() else: start, end = occurrence.get_word_range() if (not reads and not occurrence.is_written()) or \ (not writes and occurrence.is_written()): continue if region is None or region[0] <= start < region[1]: change_collector.add_change(start, end, new_name) return change_collector.get_changed() def _is_local(pyname): module, lineno = pyname.get_definition_location() if lineno is None: return False scope = module.get_scope().get_inner_scope_for_line(lineno) if isinstance(pyname, pynames.DefinedName) and \ scope.get_kind() in ('Function', 'Class'): scope = scope.parent return scope.get_kind() == 'Function' and \ pyname in scope.get_names().values() and \ isinstance(pyname, pynames.AssignedName)