class VersionedFile(object): """ Usage example: versioned_file = VersionedFile(managed_file, backup_dir, max_versions) versioned_file.checkpoint() # Called before an update is made versioned_file.undo() # Recover a previous version versioned_file.checkpoint() # Called before an update is made versioned_file.redo() # Return to the version before the undo """ def __init__(self, filepath, backup_dir, max_versions): """ :param str filepath: File path to be managed :param str backup_dir: :param int max_versions: """ self._filepath = filepath self._backup_dir = backup_dir self._max_versions = max_versions self._checkpoint_history = [] if not os.path.exists(backup_dir): os.mkdir(backup_dir) undo_pfx = self._filenamePrefix(UNDO_PREFIX) self._undo_stack = FileStack(backup_dir, undo_pfx, self._max_versions) redo_pfx = self._filenamePrefix(REDO_PREFIX) self._redo_stack = FileStack(backup_dir, redo_pfx, self._max_versions) def _filenamePrefix(self, pfx): """ :param str pfx: prefix in the file extension Returns the filename prefix """ full_name = os.path.split(self._filepath)[1] filename, ext = os.path.splitext(full_name) filename_pfx = "%s.%s" % (filename, pfx) return filename_pfx def checkpoint(self, id=None): """ Create a checkpoint in the undo stack for the current version of the managed file. :param str id: record in the checkpoint """ self._undo_stack.push(self._filepath) if not "_checkpoint_history" in dir(self): self._checkpoint_history = [] self._checkpoint_history.append(id) def clear(self): """ Empty the undo and redo stacks. """ self._undo_stack.clear() self._redo_stack.clear() def getFilepath(self): """ :returns str: filepath """ return self._filepath def getDirectory(self): """ :returns str: directory path """ return self._backup_dir def getCheckpointHistory(self): return self._checkpoint_history def getMaxVersions(self): """ :returns int: maximum depth """ return self._max_versions def undo(self): """ Reinstates the last verion of the file :raises RuntimeError: undo stack is empty """ if self._undo_stack.isEmpty(): raise RuntimeError("Undo stack is empty.") self._redo_stack.push(self._filepath) self._undo_stack.pop(self._filepath) pass def redo(self): """ Undoes the last undo :raises RuntimeError: undo stack is empty """ if self._redo_stack.isEmpty(): raise RuntimeError("Redo stack is empty.") self.checkpoint() self._redo_stack.pop(self._filepath)
class TestFileStack(unittest.TestCase): def setUp(self): if os.path.exists(TEST_DIR): shutil.rmtree(TEST_DIR) os.mkdir(TEST_DIR) self._writeManagedFile(0) self.stack = FileStack(TEST_DIR, FILENAME_PFX, max_depth=MAX_DEPTH) def _writeManagedFile(self, value): writeFile(TEST_FILEPATH, value) def _populateStack(self, size): """ Creates the specified number of files in the stack with values 1 through size. :param int size: number of stack files to create """ for idx in range(1, size+1): filepath = self.stack._makeFilepath(idx) writeFile(filepath, idx) def testMakeFilepath(self): filepath = self.stack._makeFilepath(1) self.assertEqual(filepath, "/tmp/file_stack/file_stack.t01") filepath = self.stack._makeFilepath(10) self.assertEqual(filepath, "/tmp/file_stack/file_stack.t10") def testGetFilepaths(self): filepaths = self.stack._getFilepaths() self.assertEqual(len(filepaths), 0) self._populateStack(MAX_DEPTH) filepaths = self.stack._getFilepaths() self.assertEqual(len(filepaths), MAX_DEPTH) filepath = self.stack._makeFilepath(3) # Removing the 3rd file should cause files, 4, 5 to be deleted os.remove(filepath) filepaths = self.stack._getFilepaths() self.assertEqual(len(filepaths), 2) for sfx in range(3, MAX_DEPTH+1): filepath = self.stack._makeFilepath(sfx) self.assertFalse(os.path.exists(filepath)) def testGetTop(self): filepath = self.stack._getTop() self.assertIsNone(filepath) self._populateStack(MAX_DEPTH) filepath = self.stack._getTop() expected_filepath = self.stack._makeFilepath(1) self.assertEqual(filepath, expected_filepath) def testClear(self): self._populateStack(MAX_DEPTH) filepaths = self.stack._getFilepaths() self.assertEqual(len(filepaths), MAX_DEPTH) self.stack.clear() filepaths = self.stack._getFilepaths() self.assertEqual(len(filepaths), 0) def _testAdjustStackDown(self, size): self.stack.clear() self._populateStack(size) self.stack._adjustStack(is_move_down=True) filepaths = self.stack._getFilepaths() if size > 0: limit = min(size+1, MAX_DEPTH) else: limit = 0 self.assertEqual(len(filepaths), limit) for idx in range(2, size+1): filepath = self.stack._makeFilepath(idx) self.assertTrue(checkFilepathValue(filepath, idx-1)) def testAdjustStackDown(self): self._testAdjustStackDown(MAX_DEPTH-1) self._testAdjustStackDown(MAX_DEPTH) self._testAdjustStackDown(0) self._testAdjustStackDown(2) def _testAdjustStackUp(self, size): self.stack.clear() self._populateStack(size) self.stack._adjustStack(is_move_down=False) filepaths = self.stack._getFilepaths() limit = max(0, size - 1) self.assertEqual(len(filepaths), limit) for idx in range(1, size-1): filepath = self.stack._makeFilepath(idx) self.assertTrue(checkFilepathValue(filepath, idx+1)) def testAdjustStackUp(self): self._testAdjustStackUp(0) self._testAdjustStackUp(MAX_DEPTH) self._testAdjustStackUp(1) def testGetDepth(self): self._populateStack(0) self.assertEqual(self.stack.getDepth(), 0) self.stack.clear() self._populateStack(MAX_DEPTH) self.assertEqual(self.stack.getDepth(), MAX_DEPTH) self.stack.clear() self._populateStack(1) self.assertEqual(self.stack.getDepth(), 1) def testIsEmpty(self): self.assertTrue(self.stack.isEmpty()) self._populateStack(1) self.assertFalse(self.stack.isEmpty()) self._populateStack(MAX_DEPTH) self.assertFalse(self.stack.isEmpty()) self.stack.clear() self.assertTrue(self.stack.isEmpty()) def _testPop(self, size): self.stack.clear() self._populateStack(size) self.stack.pop(TEST_FILEPATH) self.assertTrue(checkFilepathValue(TEST_FILEPATH, 1)) filepaths = self.stack._getFilepaths() limit = max(0, size-1) self.assertEqual(len(filepaths), limit) for idx in range(1, size): filepath = self.stack._makeFilepath(idx) self.assertTrue(checkFilepathValue(filepath, idx+1)) def testPop(self): self._testPop(2) with self.assertRaises(ValueError): self._testPop(0) self._testPop(MAX_DEPTH) def _testPush(self, size): self.stack.clear() self._populateStack(size) self.stack.push(TEST_FILEPATH) self.assertTrue(checkFilepathValue(TEST_FILEPATH, 0)) filepaths = self.stack._getFilepaths() limit = min(MAX_DEPTH, size+1) self.assertEqual(len(filepaths), limit) for idx in range(1, size): filepath = self.stack._makeFilepath(idx) self.assertTrue(checkFilepathValue(filepath, idx-1)) def testPush(self): self._testPush(2) self._testPush(0) self._testPush(MAX_DEPTH)