change.py 13 KB

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