diff --git a/stateful/README.txt b/stateful/README.txt index 2927d78..1de7213 100644 --- a/stateful/README.txt +++ b/stateful/README.txt @@ -1,16 +1,99 @@ ================ -Stateful objects +Stateful Objects ================ ($Id$) >>> from cybertools.stateful.definition import StatesDefinition >>> from cybertools.stateful.definition import State, Transition + >>> from cybertools.stateful.definition import registerStatesDefinition >>> from cybertools.stateful.base import Stateful +We start with a simple demonstration class that provides stateful +behaviour directly. + >>> class Demo(Stateful): ... pass >>> demo = Demo() + +The default states definition has the `started` state has its initial +state. + >>> demo.getState() 'started' + >>> demo.getStateObject().title + 'Started' + +We can now execute the `finish` Transition. + + >>> demo.doTransition('finish') + >>> demo.getState() + 'finished' + +More complex states definitions +------------------------------- + + >>> registerStatesDefinition( + ... StatesDefinition('publishing', + ... State('private', 'private', ('show',)), + ... State('visible', 'visible', ('publish', 'hide',)), + ... State('published', 'published', ('retract',)), + ... Transition('show', 'show', 'visible'), + ... Transition('hide', 'hide', 'private'), + ... Transition('publish', 'publish', 'published'), + ... Transition('retract', 'retract', 'visible'), + ... initialState='visible')) + + >>> demo = Demo() + >>> demo.statesDefinition = 'publishing' + >>> demo.getState() + 'visible' + +If we try to execute a transition that is not an outgoing transition +of the current state we get an error. + + >>> demo.doTransition('retract') + Traceback (most recent call last): + ... + ValueError: Transition 'retract' is not reachable from state 'visible'. + >>> demo.getState() + 'visible' + + +Stateful Adapters +================= + +Objects that show stateful behaviour need not be derived from the Stateful +class, for persistent objects one can also provide a stateful adapter. + + >>> from persistent import Persistent + >>> class Demo(Persistent): + ... pass + + >>> demo = Demo() + + >>> from zope import component + >>> from cybertools.stateful.base import StatefulAdapter + >>> component.provideAdapter(StatefulAdapter) + +We can now retrieve a stateful adapter using the IStateful interface. + + >>> from cybertools.stateful.interfaces import IStateful + + >>> statefulDemo = IStateful(demo) + >>> statefulDemo.getState() + 'started' + >>> statefulDemo.getStateObject().title + 'Started' + + >>> statefulDemo.doTransition('finish') + >>> statefulDemo.getState() + 'finished' + +If we make a new adapter for the same persistent object we get +back the state that is stored with the object. + + >>> statefulDemo = IStateful(demo) + >>> statefulDemo.getState() + 'finished' diff --git a/stateful/base.py b/stateful/base.py index f4b76e0..0bbcd8b 100644 --- a/stateful/base.py +++ b/stateful/base.py @@ -17,28 +17,35 @@ # """ -State definition implementation. +Basic implementations for stateful objects and adapters. $Id$ """ +from persistent.interfaces import IPersistent +from persistent.mapping import PersistentMapping +from zope.component import adapts from zope.interface import implements from cybertools.stateful.interfaces import IStateful from cybertools.stateful.definition import statesDefinitions -class Stateful: +class Stateful(object): implements(IStateful) - _statesDefinition = 'default' - _state = None + statesDefinition = 'default' + state = None def getState(self): - if self._state is None: - self._state = self.getStatesDefinition()._initialState - return self._state + if self.state is None: + self.state = self.getStatesDefinition().initialState + return self.state + + def getStateObject(self): + state = self.getState() + return self.getStatesDefinition().states[state] def doTransition(self, transition): """ execute transition. @@ -51,6 +58,29 @@ class Stateful: return sd.getAvailableTransitionsFor(self) def getStatesDefinition(self): - return statesDefinitions.get(self._statesDefinition, None) + return statesDefinitions.get(self.statesDefinition, None) +class StatefulAdapter(Stateful): + """ An adapter for persistent objects to make the stateful. + """ + + adapts(IPersistent) + + statesAttributeName = '__states__' + + def __init__(self, context): + self.context = context + + def getState(self): + statesAttr = getattr(self.context, self.statesAttributeName, {}) + return statesAttr.get(self.statesDefinition, + self.getStatesDefinition().initialState) + def setState(self, value): + statesAttr = getattr(self.context, self.statesAttributeName, None) + if statesAttr is None: + statesAttr = PersistentMapping() + setattr(self.context, self.statesAttributeName, statesAttr) + statesAttr[self.statesDefinition] = value + state = property(getState, setState) + diff --git a/stateful/definition.py b/stateful/definition.py index 123411c..705aca0 100644 --- a/stateful/definition.py +++ b/stateful/definition.py @@ -23,22 +23,28 @@ $Id$ """ from zope.interface import implements +from cybertools.util.jeep import Jeep +from cybertools.stateful.interfaces import IState, ITransition from cybertools.stateful.interfaces import IStatesDefinition class State(object): - def __init__(self, id, title, transitions): - self.id = id + implements(IState) + + def __init__(self, name, title, transitions): + self.name = self.__name__ = name self.title = title self.transitions = transitions class Transition(object): - def __init__(self, id, title, targetState): - self.id = id + implements(ITransition) + + def __init__(self, name, title, targetState): + self.name = self.__name__ = name self.title = title self.targetState = targetState @@ -47,27 +53,49 @@ class StatesDefinition(object): implements(IStatesDefinition) - # Basic/example states definition: - _states = { - 'started': State('started', 'Started', ('finish',)), - 'finished': State('finished', 'Finished', ()), - } - _transitions = { - 'finish': Transition('finish', 'Finish', 'finished') - } - _initialState = 'started' + initialState = 'started' - def doTransitionFor(self, object, transition): - object._state = self._transitions[transition].targetState + def __init__(self, name, *details, **kw): + self.name = self.__name__ = name + self.states = Jeep() + self.transitions = Jeep() + for d in details: + if ITransition.providedBy(d): + self.transitions.append(d) + elif IState.providedBy(d): + self.states.append(d) + else: + raise TypeError('Only states or transitions are allowed here, ' + 'got %s instead.' % repr(d)) + for k, v in kw.items(): + setattr(self, k, v) - def getAvailableTransitionsFor(self, object): - state = object.getState() - return [ self._transitions[t] for t in self._states[state].transitions ] + def doTransitionFor(self, obj, transition): + if transition not in self.transitions: + raise ValueError('Transition %s is not available.' % transition) + if transition not in [t.name for t in self.getAvailableTransitionsFor(obj)]: + raise ValueError("Transition '%s' is not reachable from state '%s'." + % (transition, obj.getState())) + obj.state = self.transitions[transition].targetState + + def getAvailableTransitionsFor(self, obj): + state = obj.getState() + return [self.transitions[t] for t in self.states[state].transitions] + +# dummy default states definition + +defaultSD = StatesDefinition('default', + State('started', 'Started', ('finish',)), + State('finished', 'Finished', ()), + Transition('finish', 'Finish', 'finished'), +) -def registerStatesDefinition(id, definition): - statesDefinitions[id] = definition +# states definitions registry -statesDefinitions = { - 'default': StatesDefinition(), -} +statesDefinitions = dict() + +def registerStatesDefinition(definition): + statesDefinitions[definition.name] = definition + +registerStatesDefinition(defaultSD) diff --git a/stateful/interfaces.py b/stateful/interfaces.py index e562de8..3ca7b2e 100644 --- a/stateful/interfaces.py +++ b/stateful/interfaces.py @@ -22,7 +22,23 @@ Interfaces for the `stateful` package. $Id$ """ -from zope.interface import Interface +from zope.interface import Interface, Attribute + + +class IState(Interface): + + name = Attribute('The name or identifier of the state') + title = Attribute('A user-readable name or title of the state') + transitions = Attribute('A sequence of strings naming the transitions ' + 'that can be executed from this state') + + +class ITransition(Interface): + + name = Attribute('The name or identifier of the transition') + title = Attribute('A user-readable name or title of the transition') + targetState = Attribute('A string naming the state that will be the ' + 'result of executing this transition') class IStateful(Interface): @@ -30,11 +46,15 @@ class IStateful(Interface): """ def getState(): - """ Return the workflow state of the object. + """ Return the name of the state of the object. + """ + + def getStateObject(): + """ Return the state (an IState implementation) of the object. """ def doTransition(transition): - """ Execute a transition; the transition is specified by its id. + """ Execute a transition; the transition is specified by its name. """ def getAvailableTransitions(): @@ -50,9 +70,11 @@ class IStatesDefinition(Interface): Similar to an entity-based workflow definition. """ + name = Attribute('The name or identifier of the states definition') + def doTransitionFor(object, transition): """ Execute a transition for the object given; - the transition is specified by its id. + the transition is specified by its name. """ def getAvailableTransitionsFor(object):