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