1import copy
2
3import rope.base.exceptions
4from rope.base import codeanalyze
5from rope.base import evaluate
6from rope.base import pyobjects
7from rope.base import taskhandle
8from rope.base import utils
9from rope.base import worder
10from rope.base.change import ChangeContents, ChangeSet
11from rope.refactor import occurrences, functionutils
12
13
14class ChangeSignature(object):
15
16    def __init__(self, project, resource, offset):
17        self.project = project
18        self.resource = resource
19        self.offset = offset
20        self._set_name_and_pyname()
21        if self.pyname is None or self.pyname.get_object() is None or \
22           not isinstance(self.pyname.get_object(), pyobjects.PyFunction):
23            raise rope.base.exceptions.RefactoringError(
24                'Change method signature should be performed on functions')
25
26    def _set_name_and_pyname(self):
27        self.name = worder.get_name_at(self.resource, self.offset)
28        this_pymodule = self.project.get_pymodule(self.resource)
29        self.primary, self.pyname = evaluate.eval_location2(
30            this_pymodule, self.offset)
31        if self.pyname is None:
32            return
33        pyobject = self.pyname.get_object()
34        if isinstance(pyobject, pyobjects.PyClass) and \
35           '__init__' in pyobject:
36            self.pyname = pyobject['__init__']
37            self.name = '__init__'
38        pyobject = self.pyname.get_object()
39        self.others = None
40        if self.name == '__init__' and \
41           isinstance(pyobject, pyobjects.PyFunction) and \
42           isinstance(pyobject.parent, pyobjects.PyClass):
43            pyclass = pyobject.parent
44            self.others = (pyclass.get_name(),
45                           pyclass.parent[pyclass.get_name()])
46
47    def _change_calls(self, call_changer, in_hierarchy=None, resources=None,
48                      handle=taskhandle.NullTaskHandle()):
49        if resources is None:
50            resources = self.project.get_python_files()
51        changes = ChangeSet('Changing signature of <%s>' % self.name)
52        job_set = handle.create_jobset('Collecting Changes', len(resources))
53        finder = occurrences.create_finder(
54            self.project, self.name, self.pyname, instance=self.primary,
55            in_hierarchy=in_hierarchy and self.is_method())
56        if self.others:
57            name, pyname = self.others
58            constructor_finder = occurrences.create_finder(
59                self.project, name, pyname, only_calls=True)
60            finder = _MultipleFinders([finder, constructor_finder])
61        for file in resources:
62            job_set.started_job(file.path)
63            change_calls = _ChangeCallsInModule(
64                self.project, finder, file, call_changer)
65            changed_file = change_calls.get_changed_module()
66            if changed_file is not None:
67                changes.add_change(ChangeContents(file, changed_file))
68            job_set.finished_job()
69        return changes
70
71    def get_args(self):
72        """Get function arguments.
73
74        Return a list of ``(name, default)`` tuples for all but star
75        and double star arguments.  For arguments that don't have a
76        default, `None` will be used.
77        """
78        return self._definfo().args_with_defaults
79
80    def is_method(self):
81        pyfunction = self.pyname.get_object()
82        return isinstance(pyfunction.parent, pyobjects.PyClass)
83
84    @utils.deprecated('Use `ChangeSignature.get_args()` instead')
85    def get_definition_info(self):
86        return self._definfo()
87
88    def _definfo(self):
89        return functionutils.DefinitionInfo.read(self.pyname.get_object())
90
91    @utils.deprecated()
92    def normalize(self):
93        changer = _FunctionChangers(
94            self.pyname.get_object(), self.get_definition_info(),
95            [ArgumentNormalizer()])
96        return self._change_calls(changer)
97
98    @utils.deprecated()
99    def remove(self, index):
100        changer = _FunctionChangers(
101            self.pyname.get_object(), self.get_definition_info(),
102            [ArgumentRemover(index)])
103        return self._change_calls(changer)
104
105    @utils.deprecated()
106    def add(self, index, name, default=None, value=None):
107        changer = _FunctionChangers(
108            self.pyname.get_object(), self.get_definition_info(),
109            [ArgumentAdder(index, name, default, value)])
110        return self._change_calls(changer)
111
112    @utils.deprecated()
113    def inline_default(self, index):
114        changer = _FunctionChangers(
115            self.pyname.get_object(), self.get_definition_info(),
116            [ArgumentDefaultInliner(index)])
117        return self._change_calls(changer)
118
119    @utils.deprecated()
120    def reorder(self, new_ordering):
121        changer = _FunctionChangers(
122            self.pyname.get_object(), self.get_definition_info(),
123            [ArgumentReorderer(new_ordering)])
124        return self._change_calls(changer)
125
126    def get_changes(self, changers, in_hierarchy=False, resources=None,
127                    task_handle=taskhandle.NullTaskHandle()):
128        """Get changes caused by this refactoring
129
130        `changers` is a list of `_ArgumentChanger`.  If `in_hierarchy`
131        is `True` the changers are applyed to all matching methods in
132        the class hierarchy.
133        `resources` can be a list of `rope.base.resource.File` that
134        should be searched for occurrences; if `None` all python files
135        in the project are searched.
136
137        """
138        function_changer = _FunctionChangers(self.pyname.get_object(),
139                                             self._definfo(), changers)
140        return self._change_calls(function_changer, in_hierarchy,
141                                  resources, task_handle)
142
143
144class _FunctionChangers(object):
145
146    def __init__(self, pyfunction, definition_info, changers=None):
147        self.pyfunction = pyfunction
148        self.definition_info = definition_info
149        self.changers = changers
150        self.changed_definition_infos = self._get_changed_definition_infos()
151
152    def _get_changed_definition_infos(self):
153        result = []
154        definition_info = self.definition_info
155        result.append(definition_info)
156        for changer in self.changers:
157            definition_info = copy.deepcopy(definition_info)
158            changer.change_definition_info(definition_info)
159            result.append(definition_info)
160        return result
161
162    def change_definition(self, call):
163        return self.changed_definition_infos[-1].to_string()
164
165    def change_call(self, primary, pyname, call):
166        call_info = functionutils.CallInfo.read(
167            primary, pyname, self.definition_info, call)
168        mapping = functionutils.ArgumentMapping(self.definition_info,
169                                                call_info)
170
171        for definition_info, changer in zip(self.changed_definition_infos,
172                                            self.changers):
173            changer.change_argument_mapping(definition_info, mapping)
174
175        return mapping.to_call_info(
176            self.changed_definition_infos[-1]).to_string()
177
178
179class _ArgumentChanger(object):
180
181    def change_definition_info(self, definition_info):
182        pass
183
184    def change_argument_mapping(self, definition_info, argument_mapping):
185        pass
186
187
188class ArgumentNormalizer(_ArgumentChanger):
189    pass
190
191
192class ArgumentRemover(_ArgumentChanger):
193
194    def __init__(self, index):
195        self.index = index
196
197    def change_definition_info(self, call_info):
198        if self.index < len(call_info.args_with_defaults):
199            del call_info.args_with_defaults[self.index]
200        elif self.index == len(call_info.args_with_defaults) and \
201                call_info.args_arg is not None:
202            call_info.args_arg = None
203        elif (self.index == len(call_info.args_with_defaults) and
204              call_info.args_arg is None and
205              call_info.keywords_arg is not None) or \
206                (self.index == len(call_info.args_with_defaults) + 1 and
207                 call_info.args_arg is not None and
208                 call_info.keywords_arg is not None):
209            call_info.keywords_arg = None
210
211    def change_argument_mapping(self, definition_info, mapping):
212        if self.index < len(definition_info.args_with_defaults):
213            name = definition_info.args_with_defaults[0]
214            if name in mapping.param_dict:
215                del mapping.param_dict[name]
216
217
218class ArgumentAdder(_ArgumentChanger):
219
220    def __init__(self, index, name, default=None, value=None):
221        self.index = index
222        self.name = name
223        self.default = default
224        self.value = value
225
226    def change_definition_info(self, definition_info):
227        for pair in definition_info.args_with_defaults:
228            if pair[0] == self.name:
229                raise rope.base.exceptions.RefactoringError(
230                    'Adding duplicate parameter: <%s>.' % self.name)
231        definition_info.args_with_defaults.insert(self.index,
232                                                  (self.name, self.default))
233
234    def change_argument_mapping(self, definition_info, mapping):
235        if self.value is not None:
236            mapping.param_dict[self.name] = self.value
237
238
239class ArgumentDefaultInliner(_ArgumentChanger):
240
241    def __init__(self, index):
242        self.index = index
243        self.remove = False
244
245    def change_definition_info(self, definition_info):
246        if self.remove:
247            definition_info.args_with_defaults[self.index] = \
248                (definition_info.args_with_defaults[self.index][0], None)
249
250    def change_argument_mapping(self, definition_info, mapping):
251        default = definition_info.args_with_defaults[self.index][1]
252        name = definition_info.args_with_defaults[self.index][0]
253        if default is not None and name not in mapping.param_dict:
254            mapping.param_dict[name] = default
255
256
257class ArgumentReorderer(_ArgumentChanger):
258
259    def __init__(self, new_order, autodef=None):
260        """Construct an `ArgumentReorderer`
261
262        Note that the `new_order` is a list containing the new
263        position of parameters; not the position each parameter
264        is going to be moved to. (changed in ``0.5m4``)
265
266        For example changing ``f(a, b, c)`` to ``f(c, a, b)``
267        requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``.
268
269        The `autodef` (automatic default) argument, forces rope to use
270        it as a default if a default is needed after the change.  That
271        happens when an argument without default is moved after
272        another that has a default value.  Note that `autodef` should
273        be a string or `None`; the latter disables adding automatic
274        default.
275
276        """
277        self.new_order = new_order
278        self.autodef = autodef
279
280    def change_definition_info(self, definition_info):
281        new_args = list(definition_info.args_with_defaults)
282        for new_index, index in enumerate(self.new_order):
283            new_args[new_index] = definition_info.args_with_defaults[index]
284        seen_default = False
285        for index, (arg, default) in enumerate(list(new_args)):
286            if default is not None:
287                seen_default = True
288            if seen_default and default is None and self.autodef is not None:
289                new_args[index] = (arg, self.autodef)
290        definition_info.args_with_defaults = new_args
291
292
293class _ChangeCallsInModule(object):
294
295    def __init__(self, project, occurrence_finder, resource, call_changer):
296        self.project = project
297        self.occurrence_finder = occurrence_finder
298        self.resource = resource
299        self.call_changer = call_changer
300
301    def get_changed_module(self):
302        word_finder = worder.Worder(self.source)
303        change_collector = codeanalyze.ChangeCollector(self.source)
304        for occurrence in self.occurrence_finder.find_occurrences(
305                self.resource):
306            if not occurrence.is_called() and not occurrence.is_defined():
307                continue
308            start, end = occurrence.get_primary_range()
309            begin_parens, end_parens = word_finder.\
310                get_word_parens_range(end - 1)
311            if occurrence.is_called():
312                primary, pyname = occurrence.get_primary_and_pyname()
313                changed_call = self.call_changer.change_call(
314                    primary, pyname, self.source[start:end_parens])
315            else:
316                changed_call = self.call_changer.change_definition(
317                    self.source[start:end_parens])
318            if changed_call is not None:
319                change_collector.add_change(start, end_parens, changed_call)
320        return change_collector.get_changed()
321
322    @property
323    @utils.saveit
324    def pymodule(self):
325        return self.project.get_pymodule(self.resource)
326
327    @property
328    @utils.saveit
329    def source(self):
330        if self.resource is not None:
331            return self.resource.read()
332        else:
333            return self.pymodule.source_code
334
335    @property
336    @utils.saveit
337    def lines(self):
338        return self.pymodule.lines
339
340
341class _MultipleFinders(object):
342
343    def __init__(self, finders):
344        self.finders = finders
345
346    def find_occurrences(self, resource=None, pymodule=None):
347        all_occurrences = []
348        for finder in self.finders:
349            all_occurrences.extend(finder.find_occurrences(resource, pymodule))
350        all_occurrences.sort(key=lambda x: x.get_primary_range())
351        return all_occurrences
352
353