import rope.base.exceptions import rope.base.pyobjects from rope.base import taskhandle, evaluate from rope.base.change import (ChangeSet, ChangeContents) from rope.refactor import rename, occurrences, sourceutils, importutils class IntroduceFactory(object): def __init__(self, project, resource, offset): self.pycore = project.pycore self.offset = offset this_pymodule = self.pycore.resource_to_pyobject(resource) self.old_pyname = evaluate.eval_location(this_pymodule, offset) if self.old_pyname is None or not isinstance(self.old_pyname.get_object(), rope.base.pyobjects.PyClass): raise rope.base.exceptions.RefactoringError( 'Introduce factory should be performed on a class.') self.old_name = self.old_pyname.get_object().get_name() self.pymodule = self.old_pyname.get_object().get_module() self.resource = self.pymodule.get_resource() def get_changes(self, factory_name, global_factory=False, resources=None, task_handle=taskhandle.NullTaskHandle()): """Get the changes this refactoring makes `factory_name` indicates the name of the factory function to be added. If `global_factory` is `True` the factory will be global otherwise a static method is added to the class. `resources` can be a list of `rope.base.resource.File`\s that this refactoring should be applied on; if `None` all python files in the project are searched. """ if resources is None: resources = self.pycore.get_python_files() changes = ChangeSet('Introduce factory method <%s>' % factory_name) job_set = task_handle.create_jobset('Collecting Changes', len(resources)) self._change_module(resources, changes, factory_name, global_factory, job_set) return changes def get_name(self): """Return the name of the class""" return self.old_name def _change_module(self, resources, changes, factory_name, global_, job_set): if global_: replacement = '__rope_factory_%s_' % factory_name else: replacement = self._new_function_name(factory_name, global_) for file_ in resources: job_set.started_job(file_.path) if file_ == self.resource: self._change_resource(changes, factory_name, global_) job_set.finished_job() continue changed_code = self._rename_occurrences(file_, replacement, global_) if changed_code is not None: if global_: new_pymodule = self.pycore.get_string_module(changed_code, self.resource) modname = self.pycore.modname(self.resource) changed_code, imported = importutils.add_import( self.pycore, new_pymodule, modname, factory_name) changed_code = changed_code.replace(replacement, imported) changes.add_change(ChangeContents(file_, changed_code)) job_set.finished_job() def _change_resource(self, changes, factory_name, global_): class_scope = self.old_pyname.get_object().get_scope() source_code = self._rename_occurrences( self.resource, self._new_function_name(factory_name, global_), global_) if source_code is None: source_code = self.pymodule.source_code else: self.pymodule = self.pycore.get_string_module( source_code, resource=self.resource) lines = self.pymodule.lines start = self._get_insertion_offset(class_scope, lines) result = source_code[:start] result += self._get_factory_method(lines, class_scope, factory_name, global_) result += source_code[start:] changes.add_change(ChangeContents(self.resource, result)) def _get_insertion_offset(self, class_scope, lines): start_line = class_scope.get_end() if class_scope.get_scopes(): start_line = class_scope.get_scopes()[-1].get_end() start = lines.get_line_end(start_line) + 1 return start def _get_factory_method(self, lines, class_scope, factory_name, global_): unit_indents = ' ' * sourceutils.get_indent(self.pycore) if global_: if self._get_scope_indents(lines, class_scope) > 0: raise rope.base.exceptions.RefactoringError( 'Cannot make global factory method for nested classes.') return ('\ndef %s(*args, **kwds):\n%sreturn %s(*args, **kwds)\n' % (factory_name, unit_indents, self.old_name)) unindented_factory = \ ('@staticmethod\ndef %s(*args, **kwds):\n' % factory_name + '%sreturn %s(*args, **kwds)\n' % (unit_indents, self.old_name)) indents = self._get_scope_indents(lines, class_scope) + \ sourceutils.get_indent(self.pycore) return '\n' + sourceutils.indent_lines(unindented_factory, indents) def _get_scope_indents(self, lines, scope): return sourceutils.get_indents(lines, scope.get_start()) def _new_function_name(self, factory_name, global_): if global_: return factory_name else: return self.old_name + '.' + factory_name def _rename_occurrences(self, file_, changed_name, global_factory): finder = occurrences.create_finder(self.pycore, self.old_name, self.old_pyname, only_calls=True) result = rename.rename_in_module(finder, changed_name, resource=file_, replace_primary=global_factory) return result IntroduceFactoryRefactoring = IntroduceFactory