Package: recollection
Recollection OverviewRecollection is a state recalling system. It allows for state of objects
to be snap-shotted exposing functionality to then 'rollback' to any
previously stored state.Recollection gives two distinctly different exposures of this mechanism. If
performance is not the most critical concern and your focus is on code
simplicity then the inheritence approach is possibly your best option
providing you have ownership of the classes which need historical state.Note: This is currently pre-releaseInstallationYou can install this using pip:pip install recollectionAlternatively you can get the source from:https://github.com/mikemalinowski/recollectionRecollection Inheritence (Inference)This example shows how to setup a class using inheritence to
automatically handle state storing. As you can see from the example, there
is no need to explicitly ask recollection to store at any time as it is handled
entirely for you. This example specifically shows attribute storing :importrecollection# -- Inference is the recollection class designed specifically# -- for inheritance situations.classFoo(recollection.Inference):def__init__(self):super(Foo,self).__init__()self.number=10# -- Register 'number' as a property to monitorself.memento.register('number')# -- Instance our objectfoo=Foo()# -- Here we demonstrate directly changing properties# -- which are registeredfoo.number=5foo.number=99print(foo.number==99)# -- Now restore back one stepfoo.recollection.restore(1)print(foo.number==5)Whilst this example shows how decorators can be utilised to store method
getters and setters:importrecollection# -- Inference is the recollection class designed specifically# -- for inheritence situations.classFoo(recollection.Inference):def__init__(self):super(Foo,self).__init__()self._number=10# -- Declare that this is a recollection getter@recollection.infer.get('number')defnumber(self):returnself._number# -- Declare this as a recollection setter@recollection.infer.store('number')defset_number(self,value):self._number=value# -- Instance our objectfoo=Foo()# -- Update our variable using our accessorfornumberinrange(10):foo.set_number(number)# -- Demonstrate that the current state is 'e'print(foo.number())# -- Prints 9# -- Roll back one step in the memento historyfoo.recollection.restore(1)print(foo.number())# -- Prints 8Memento StackHowever, we do not always have the luxury of changing class inheritance
or you may specifically want to keep the recollection state management out
of your actual inheritance hierarchy. The following examples all demonstrate
how this can be achieved.In this example we have a class with two properties. We the instance
a Memento class targeting our foo instance. Each time we call the
store method within Memento we are taking a snapshot of the values
returned by the registered properties/functionsimportrecollectionclassFoo(object):def__init__(self):self.number=0# -- Instance our objectfoo=Foo()# -- Instance a memento object pointing at foomemento=recollection.Memento(foo)memento.register('number')# -- Start changing some values on foo, and# -- ask our stack to store those changesfornumberinrange(11):foo.number=number# -- Ask the memento object to store the statememento.store()# -- Printing i, shows us 10print(foo.number)# -- But lets say we roll back to the state 5 versions# -- agomemento.restore(5)# -- Now we can see i is at the version it was when# -- it was stored 5 versions backprint(foo.number)Lock-Stepped StorageIt also allows multiple Memento objects to be put into a lock-step,
such that whenever one memento object is storing or restoring then
all other memento objects in that sync group will also store or
restore.importrecollectionclassFoo(object):def__init__(self):self.number=0# -- This time we instance two completely seperate# -- foo objectsfoo_a=Foo()foo_b=Foo()# -- Instance a memento stack for eachmemento_a=recollection.Memento(foo_a)memento_b=recollection.Memento(foo_b)memento_a.register('number')memento_b.register('number')# -- Now we will put our stacks into a state of lock-step# -- which means whenever one of them is stored or restored# -- all others in the lock-step group will have the same# -- action performedmemento_a.group(memento_b)# -- Increment some values on both objectsforiinrange(11):foo_a.i=ifoo_b.i=i# -- Trigger a store on only one stackmemento_a.store()# -- We can see that both A and B have a value of 10print(foo_a.i==10andfoo_b.i==10)# -- Now we rollback - knowing that this action will occur# -- across all grouped memento objectsmemento_a.restore(5)# -- Now we can see i is at the version it was when# -- it was stored 5 versions backprint(foo_a.i==5andfoo_b.i==5)SerialisationSerialisers can also be registered against memento instances allowing
the stored state of a memento object to be serialised into a persistent
state.This example shows how we might define a user preferences class, and
within that class we define a memento object to store the preference
state. By registering a serialiser the preferences state will be written
to disk whenever the 'store' is called.Notice that in this example we're also choosing not to store private
member variables, but instead we're harnessing the public api of the
class as getters and setters.classUserPreferences(object):def__init__(self):# -- Visual preferencesself._theme='default'# -- Define our memento, which we utilise specifically to# -- store our preferences to a persistent locationself._memento=recollection.Memento(self)# -- We will utilise the JSON Appdata serialiser, which# -- writes our memento information to the app data system# -- locationself._memento.register_serialiser(serialiser=recollection.JsonAppSerialiser,identifier='memento/demos/userprefs/UserPreferenceA',)# -- Register which properties we want the store to focus onself._memento.register(label='theme',getter=self.get_theme,setter=self.set_theme,)# -- Finally, we deserialise - which will update this class# -- with any previously stored stateself._memento.deserialise()# --------------------------------------------------------------defget_theme(self):returnself._theme# --------------------------------------------------------------defset_theme(self,theme):self._theme=themeself._memento.store(serialise=True)DecorationEqually, if we want to make it a little more obvious at the class level
which functions are storing we could opt to utilise the Memento decorator,
which stores and serialises:classUserPreferences(object):@recollection.serialise_after('theme')defset_theme(self,theme):self._theme=themeExamplesThese mechanics are all demonstrated in the example modules, specifically:User Preferences ObjectThis demo shows a user preferences object being interacted
which which works in the same way as the example above, where the
settings are stored as changed come in - allowing the preferences to
be  'undone'.# -- This demo shows a user preferences object being interacted# -- which which works in the same way as the example above.fromrecollection.examples.userprefs.demoimportdemodemo()Alternate User Preferences ObjectThis demo is identical to the demo above in terms of output but is
handled through decorators.fromrecollection.examples.userprefs.demoimportdemo2demo2()Board Game with roll-backThis demo utilises a 'boardgame' style scenario where
we're given two players and the desire to 'undo' the results
of turns if they are not desirable!fromrecollection.examples.boardgame.gameimportdemodemo()Pin Movement (Multi-attribute altering)This demo shows the utilisation of a setter which is actually setting
multiple recorded attributes but wants to have a single restore step.fromrecollection.examples.pins.demoimportdemodemo()Renamer (Ui and Code)This demo shows how recollection can be used to store state of a functional
object which is represented as a visual tool (utilising PySide2).fromrecollection.examples.renamer.demoimportdemodemo()Testing and StabilityThere are currently unittests which cover most of Memento's core, but it is not yet exhaustive.CompatabilityThis has been tested under Python 2.7.13 and Python 3.6.6 on both Ubuntu and Windows.ContributeIf you would like to contribute thoughts, ideas, fixes or features please get in touch!mike@twisted.space