1import datetime
2import difflib
3import os
4import time
5
6import rope.base.fscommands
7from rope.base import taskhandle, exceptions, utils
8
9
10class Change(object):
11    """The base class for changes
12
13    Rope refactorings return `Change` objects.  They can be previewed,
14    committed or undone.
15    """
16
17    def do(self, job_set=None):
18        """Perform the change
19
20        .. note:: Do use this directly.  Use `Project.do()` instead.
21        """
22
23    def undo(self, job_set=None):
24        """Perform the change
25
26        .. note:: Do use this directly.  Use `History.undo()` instead.
27        """
28
29    def get_description(self):
30        """Return the description of this change
31
32        This can be used for previewing the changes.
33        """
34        return str(self)
35
36    def get_changed_resources(self):
37        """Return the list of resources that will be changed"""
38        return []
39
40    @property
41    @utils.saveit
42    def _operations(self):
43        return _ResourceOperations(self.resource.project)
44
45
46class ChangeSet(Change):
47    """A collection of `Change` objects
48
49    This class holds a collection of changes.  This class provides
50    these fields:
51
52    * `changes`: the list of changes
53    * `description`: the goal of these changes
54    """
55
56    def __init__(self, description, timestamp=None):
57        self.changes = []
58        self.description = description
59        self.time = timestamp
60
61    def do(self, job_set=taskhandle.NullJobSet()):
62        try:
63            done = []
64            for change in self.changes:
65                change.do(job_set)
66                done.append(change)
67            self.time = time.time()
68        except Exception:
69            for change in done:
70                change.undo()
71            raise
72
73    def undo(self, job_set=taskhandle.NullJobSet()):
74        try:
75            done = []
76            for change in reversed(self.changes):
77                change.undo(job_set)
78                done.append(change)
79        except Exception:
80            for change in done:
81                change.do()
82            raise
83
84    def add_change(self, change):
85        self.changes.append(change)
86
87    def get_description(self):
88        result = [str(self) + ':\n\n\n']
89        for change in self.changes:
90            result.append(change.get_description())
91            result.append('\n')
92        return ''.join(result)
93
94    def __str__(self):
95        if self.time is not None:
96            date = datetime.datetime.fromtimestamp(self.time)
97            if date.date() == datetime.date.today():
98                string_date = 'today'
99            elif date.date() == (datetime.date.today() -
100                                 datetime.timedelta(1)):
101                string_date = 'yesterday'
102            elif date.year == datetime.date.today().year:
103                string_date = date.strftime('%b %d')
104            else:
105                string_date = date.strftime('%d %b, %Y')
106            string_time = date.strftime('%H:%M:%S')
107            string_time = '%s %s ' % (string_date, string_time)
108            return self.description + ' - ' + string_time
109        return self.description
110
111    def get_changed_resources(self):
112        result = set()
113        for change in self.changes:
114            result.update(change.get_changed_resources())
115        return result
116
117
118def _handle_job_set(function):
119    """A decorator for handling `taskhandle.JobSet`
120
121    A decorator for handling `taskhandle.JobSet` for `do` and `undo`
122    methods of `Change`.
123    """
124    def call(self, job_set=taskhandle.NullJobSet()):
125        job_set.started_job(str(self))
126        function(self)
127        job_set.finished_job()
128    return call
129
130
131class ChangeContents(Change):
132    """A class to change the contents of a file
133
134    Fields:
135
136    * `resource`: The `rope.base.resources.File` to change
137    * `new_contents`: What to write in the file
138    """
139
140    def __init__(self, resource, new_contents, old_contents=None):
141        self.resource = resource
142        # IDEA: Only saving diffs; possible problems when undo/redoing
143        self.new_contents = new_contents
144        self.old_contents = old_contents
145
146    @_handle_job_set
147    def do(self):
148        if self.old_contents is None:
149            self.old_contents = self.resource.read()
150        self._operations.write_file(self.resource, self.new_contents)
151
152    @_handle_job_set
153    def undo(self):
154        if self.old_contents is None:
155            raise exceptions.HistoryError(
156                'Undoing a change that is not performed yet!')
157        self._operations.write_file(self.resource, self.old_contents)
158
159    def __str__(self):
160        return 'Change <%s>' % self.resource.path
161
162    def get_description(self):
163        new = self.new_contents
164        old = self.old_contents
165        if old is None:
166            if self.resource.exists():
167                old = self.resource.read()
168            else:
169                old = ''
170        result = difflib.unified_diff(
171            old.splitlines(True), new.splitlines(True),
172            'a/' + self.resource.path, 'b/' + self.resource.path)
173        return ''.join(list(result))
174
175    def get_changed_resources(self):
176        return [self.resource]
177
178
179class MoveResource(Change):
180    """Move a resource to a new location
181
182    Fields:
183
184    * `resource`: The `rope.base.resources.Resource` to move
185    * `new_resource`: The destination for move; It is the moved
186      resource not the folder containing that resource.
187    """
188
189    def __init__(self, resource, new_location, exact=False):
190        self.project = resource.project
191        self.resource = resource
192        if not exact:
193            new_location = _get_destination_for_move(resource, new_location)
194        if resource.is_folder():
195            self.new_resource = self.project.get_folder(new_location)
196        else:
197            self.new_resource = self.project.get_file(new_location)
198
199    @_handle_job_set
200    def do(self):
201        self._operations.move(self.resource, self.new_resource)
202
203    @_handle_job_set
204    def undo(self):
205        self._operations.move(self.new_resource, self.resource)
206
207    def __str__(self):
208        return 'Move <%s>' % self.resource.path
209
210    def get_description(self):
211        return 'rename from %s\nrename to %s' % (self.resource.path,
212                                                 self.new_resource.path)
213
214    def get_changed_resources(self):
215        return [self.resource, self.new_resource]
216
217
218class CreateResource(Change):
219    """A class to create a resource
220
221    Fields:
222
223    * `resource`: The resource to create
224    """
225
226    def __init__(self, resource):
227        self.resource = resource
228
229    @_handle_job_set
230    def do(self):
231        self._operations.create(self.resource)
232
233    @_handle_job_set
234    def undo(self):
235        self._operations.remove(self.resource)
236
237    def __str__(self):
238        return 'Create Resource <%s>' % (self.resource.path)
239
240    def get_description(self):
241        return 'new file %s' % (self.resource.path)
242
243    def get_changed_resources(self):
244        return [self.resource]
245
246    def _get_child_path(self, parent, name):
247        if parent.path == '':
248            return name
249        else:
250            return parent.path + '/' + name
251
252
253class CreateFolder(CreateResource):
254    """A class to create a folder
255
256    See docs for `CreateResource`.
257    """
258
259    def __init__(self, parent, name):
260        resource = parent.project.get_folder(
261            self._get_child_path(parent, name))
262        super(CreateFolder, self).__init__(resource)
263
264
265class CreateFile(CreateResource):
266    """A class to create a file
267
268    See docs for `CreateResource`.
269    """
270
271    def __init__(self, parent, name):
272        resource = parent.project.get_file(self._get_child_path(parent, name))
273        super(CreateFile, self).__init__(resource)
274
275
276class RemoveResource(Change):
277    """A class to remove a resource
278
279    Fields:
280
281    * `resource`: The resource to be removed
282    """
283
284    def __init__(self, resource):
285        self.resource = resource
286
287    @_handle_job_set
288    def do(self):
289        self._operations.remove(self.resource)
290
291    # TODO: Undoing remove operations
292    @_handle_job_set
293    def undo(self):
294        raise NotImplementedError(
295            'Undoing `RemoveResource` is not implemented yet.')
296
297    def __str__(self):
298        return 'Remove <%s>' % (self.resource.path)
299
300    def get_changed_resources(self):
301        return [self.resource]
302
303
304def count_changes(change):
305    """Counts the number of basic changes a `Change` will make"""
306    if isinstance(change, ChangeSet):
307        result = 0
308        for child in change.changes:
309            result += count_changes(child)
310        return result
311    return 1
312
313
314def create_job_set(task_handle, change):
315    return task_handle.create_jobset(str(change), count_changes(change))
316
317
318class _ResourceOperations(object):
319
320    def __init__(self, project):
321        self.project = project
322        self.fscommands = project.fscommands
323        self.direct_commands = rope.base.fscommands.FileSystemCommands()
324
325    def _get_fscommands(self, resource):
326        if self.project.is_ignored(resource):
327            return self.direct_commands
328        return self.fscommands
329
330    def write_file(self, resource, contents):
331        data = rope.base.fscommands.unicode_to_file_data(contents)
332        fscommands = self._get_fscommands(resource)
333        fscommands.write(resource.real_path, data)
334        for observer in list(self.project.observers):
335            observer.resource_changed(resource)
336
337    def move(self, resource, new_resource):
338        fscommands = self._get_fscommands(resource)
339        fscommands.move(resource.real_path, new_resource.real_path)
340        for observer in list(self.project.observers):
341            observer.resource_moved(resource, new_resource)
342
343    def create(self, resource):
344        if resource.is_folder():
345            self._create_resource(resource.path, kind='folder')
346        else:
347            self._create_resource(resource.path)
348        for observer in list(self.project.observers):
349            observer.resource_created(resource)
350
351    def remove(self, resource):
352        fscommands = self._get_fscommands(resource)
353        fscommands.remove(resource.real_path)
354        for observer in list(self.project.observers):
355            observer.resource_removed(resource)
356
357    def _create_resource(self, file_name, kind='file'):
358        resource_path = self.project._get_resource_path(file_name)
359        if os.path.exists(resource_path):
360            raise exceptions.RopeError('Resource <%s> already exists'
361                                       % resource_path)
362        resource = self.project.get_file(file_name)
363        if not resource.parent.exists():
364            raise exceptions.ResourceNotFoundError(
365                'Parent folder of <%s> does not exist' % resource.path)
366        fscommands = self._get_fscommands(resource)
367        try:
368            if kind == 'file':
369                fscommands.create_file(resource_path)
370            else:
371                fscommands.create_folder(resource_path)
372        except IOError as e:
373            raise exceptions.RopeError(e)
374
375
376def _get_destination_for_move(resource, destination):
377    dest_path = resource.project._get_resource_path(destination)
378    if os.path.isdir(dest_path):
379        if destination != '':
380            return destination + '/' + resource.name
381        else:
382            return resource.name
383    return destination
384
385
386class ChangeToData(object):
387
388    def convertChangeSet(self, change):
389        description = change.description
390        changes = []
391        for child in change.changes:
392            changes.append(self(child))
393        return (description, changes, change.time)
394
395    def convertChangeContents(self, change):
396        return (change.resource.path, change.new_contents, change.old_contents)
397
398    def convertMoveResource(self, change):
399        return (change.resource.path, change.new_resource.path)
400
401    def convertCreateResource(self, change):
402        return (change.resource.path, change.resource.is_folder())
403
404    def convertRemoveResource(self, change):
405        return (change.resource.path, change.resource.is_folder())
406
407    def __call__(self, change):
408        change_type = type(change)
409        if change_type in (CreateFolder, CreateFile):
410            change_type = CreateResource
411        method = getattr(self, 'convert' + change_type.__name__)
412        return (change_type.__name__, method(change))
413
414
415class DataToChange(object):
416
417    def __init__(self, project):
418        self.project = project
419
420    def makeChangeSet(self, description, changes, time=None):
421        result = ChangeSet(description, time)
422        for child in changes:
423            result.add_change(self(child))
424        return result
425
426    def makeChangeContents(self, path, new_contents, old_contents):
427        resource = self.project.get_file(path)
428        return ChangeContents(resource, new_contents, old_contents)
429
430    def makeMoveResource(self, old_path, new_path):
431        resource = self.project.get_file(old_path)
432        return MoveResource(resource, new_path, exact=True)
433
434    def makeCreateResource(self, path, is_folder):
435        if is_folder:
436            resource = self.project.get_folder(path)
437        else:
438            resource = self.project.get_file(path)
439        return CreateResource(resource)
440
441    def makeRemoveResource(self, path, is_folder):
442        if is_folder:
443            resource = self.project.get_folder(path)
444        else:
445            resource = self.project.get_file(path)
446        return RemoveResource(resource)
447
448    def __call__(self, data):
449        method = getattr(self, 'make' + data[0])
450        return method(*data[1])
451