/
introduce_factory.py
145 lines (127 loc) · 5.85 KB
/
introduce_factory.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import rope.base.exceptions
import rope.base.pyobjects
from rope.base import libutils
from rope.base import taskhandle, evaluate
from rope.base.change import ChangeSet, ChangeContents
from rope.refactor import rename, occurrences, sourceutils, importutils
class IntroduceFactory:
def __init__(self, project, resource, offset):
self.project = project
self.offset = offset
this_pymodule = self.project.get_pymodule(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` that
this refactoring should be applied on; if `None` all python
files in the project are searched.
"""
if resources is None:
resources = self.project.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 = libutils.get_string_module(
self.project, changed_code, self.resource
)
modname = libutils.modname(self.resource)
changed_code, imported = importutils.add_import(
self.project, 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 = libutils.get_string_module(
self.project, 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.project)
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 {}(*args, **kwds):\n{}return {}(*args, **kwds)\n".format(
factory_name,
unit_indents,
self.old_name,
)
unindented_factory = (
"@staticmethod\ndef %s(*args, **kwds):\n" % factory_name
+ f"{unit_indents}return {self.old_name}(*args, **kwds)\n"
)
indents = self._get_scope_indents(lines, class_scope) + sourceutils.get_indent(
self.project
)
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.project, self.old_name, self.old_pyname, only_calls=True
)
return rename.rename_in_module(
finder, changed_name, resource=file_, replace_primary=global_factory
)
IntroduceFactoryRefactoring = IntroduceFactory