Description
I'm writing a program that makes changes to a few Python source files--creating some, and appending content to others. Since some changes depend on others, I check frequently to make sure everything is going smoothly (try importing the modified file, etc). I would like to revert any and all changes my program has made if it encounters a problem.
I decided the best way of going about doing this would be to use an Action class, with methods to perform and reverse the changes (like migrations in Django's South, or Rails migrations). An ActionManager class performs each action in turn, and if any fail, reverts them in the reverse order.
Dilemma
The code I wrote appears to be working fine. However, I have three problems with it:
The section marked
???
in the code below seems like it is faulty. Are there any conditions where this would break? If so, is there a better way?I am not 100% sure all changes will be reversed if, say, an exception occurs. If possible, I want to make sure all changes are reverted even if the program is interrupted midway. I believe my exception handling is flimsy at best, faulty at worst. Any advice appreciated.
The API is atrocious. Specifying a manager every time an action is initialized seems tedious, and so does calling
super
in every action method. Is there a more elegant solution?
Any and all advice on the above or anything else in the source below is appreciated.
Code
Base Classes
#-*- coding: utf-8 -*-
import errno
import os
import shutil
import tempfile
class FileAction(object):
"""A reversible action performed on the filesystem."""
def __init__(self, mgr, origfile):
self.manager = mgr
self.manager.pending_actions.append(self)
self.origfile = origfile
def execute(self):
"""Perform action.
Return a Boolean indicating whether action
was successful or not.
"""
filename = os.path.basename(self.origfile)
# ??? - Append object id to filename in order to make sure
# two actions can modify the same file without colliding.
action_id = str(id(self))
dst = os.path.join(self.manager.tmpdir, filename + action_id)
if os.path.isfile(dst):
raise OSError(
errno.EPERM, 'File already exists at {0}.'.format(dst))
return False
else:
shutil.copy2(self.origfile, dst)
self.tmpfile = dst
return True
def revert(self):
"""Undo action."""
shutil.copy2(self.tmpfile, self.origfile)
def cleanup(self):
"""Remove any temporary files."""
try:
os.remove(self.tmpfile)
except:
raise
return False
else:
return True
class FileActionManager(object):
def __init__(self):
"""Create temporary directory used by actions."""
self.executed_actions = []
self.pending_actions = []
def execute(self):
"""Perform all actions, return Boolean indicating their success."""
self.tmpdir = tempfile.mkdtemp()
for action in self.pending_actions:
is_success = action.execute()
self.pending_actions.remove(action)
self.executed_actions.append(action)
if not is_success:
self.revert()
return False
return self.cleanup()
def revert(self):
return all([a.revert() for a in reversed(self.executed_actions)])
def cleanup(self):
"""Remove all temporary files."""
is_success = all([a.cleanup() for a in self.executed_actions])
try:
os.rmdir(self.tmpdir)
except:
raise
return False
else:
if is_success:
self.executed_actions = []
self.pending_actions = []
return is_success
Sample Usage
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import time
from actionmanager import FileAction, FileActionManager
class AppendTimestampAction(FileAction):
def execute(self):
if super(AppendTimestampAction, self).execute():
with open(self.origfile, 'a') as f:
f.write(str(time.time()) + '\n')
return True
return False
class FailingAction(FileAction):
def execute(self):
super(FailingAction, self).execute()
return False
def main():
target_file = 'target.txt'
mgr = FileActionManager()
AppendTimestampAction(mgr, target_file)
AppendTimestampAction(mgr, target_file)
mgr.execute() # => file includes two timestamps
AppendTimestampAction(mgr, target_file)
FailingAction(mgr, target_file)
mgr.execute() # => file has timestamp appended, but is then reverted due to failure
if __name__ == '__main__':
main()