# This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. """ Transactions of attributes by inheriting the Transaction class Basic Usage: class Test(Transaction): pass a = Test() a.test = "old state" a.commit() a.test = "bad state, roll me back" a.rollback() assert(a.test == "old state") See also: https://harald.hoyer.xyz/2015/10/13/a-python-transaction-class/ Copyright (C) 2008 Harald Hoyer Copyright (C) 2008 Red Hat, Inc. """ import copy def _checksetseen(what, seen): "checks and sets the obj id in seen" if what in seen: return True seen.add(what) return False class Transaction(object): """ This class allows sub-classes to commit changes to an instance to a history, and rollback to previous states. Because the class only stores attributes in self.__dict__ sub-classes need to use the methods __getstate__ and __setstate__ to provide additional state information. See the Transactionlist below for an example usage. """ def commit(self, **kwargs): """ Commit the object state. If the optional argument "deep" is set to False, objects of class Transaction stored in this object will not be committed. """ seen = kwargs.get("_commit_seen", set()) if _checksetseen(id(self), seen): return deep = kwargs.get("deep", True) # Do not deepcopy the Transaction objects. We want to keep the # reference. Instead commit() them. state = dict() for key, val in self.__dict__.items(): if isinstance(val, Transaction): state[key] = val if deep: val.commit(_commit_seen=seen) elif key == "__l": # do not deepcopy our old state state[key] = val else: state[key] = copy.deepcopy(val) if hasattr(self, "__getstate__"): state = (state, getattr(self, "__getstate__")()) self.__dict__["__l"] = state def rollback(self, **kwargs): """ Rollback the last committed object state. If the optional argument "deep" is set to False, objects of class Transaction stored in this object will not be rolled back. """ seen = kwargs.get("_rollback_seen", set()) if _checksetseen(id(self), seen): return deep = kwargs.get("deep", True) state = None extrastate = None gotstate = False gotextrastate = False if "__l" in self.__dict__: state = self.__dict__["__l"] gotstate = True if type(state) is tuple: gotextrastate = True (state, extrastate) = state # rollback our childs, then ourselves for child in self.__dict__.values(): if isinstance(child, Transaction): if deep: child.rollback(_rollback_seen=seen) if gotstate: self.__dict__.clear() self.__dict__.update(state) if gotextrastate and hasattr(self, "__setstate__"): getattr(self, "__setstate__")(extrastate) class Transactionlist(list, Transaction): """ An example subclass of list, which inherits transactions. Due to the special list implementation, we need the __getstate__ and __setstate__ methods. See the code for the implementation. """ def commit(self, **kwargs): """ Commit the object state. If the optional argument "deep" is set to False, objects of class Transaction stored in this object will not be committed. """ # make a local copy of the recursive marker seen = set(kwargs.get("_commit_seen", set())) super(Transactionlist, self).commit(**kwargs) if _checksetseen(id(self), seen): return deep = kwargs.get("deep", True) if deep: for val in self: if isinstance(val, Transaction): val.commit() def rollback(self, **kwargs): """ Rollback the last committed object state. If the optional argument "deep" is set to False, objects of class Transaction stored in this object will not be rolled back. """ # make a local copy of the recursive marker seen = set(kwargs.get("_rollback_seen", set())) super(Transactionlist, self).rollback(**kwargs) if _checksetseen(id(self), seen): return deep = kwargs.get("deep", True) if deep: for val in self: if isinstance(val, Transaction): val.rollback() def __getstate__(self): """ return a deepcopy of all non Transaction class objects in our list, and a reference for the committed Transaction objects. """ state = [] for val in self: if isinstance(val, Transaction): state.append(val) else: state.append(copy.deepcopy(val)) return state def __setstate__(self, state): "clear the list and restore all objects from the state" del self[:] self.extend(state) __author__ = "Harald Hoyer "