""" Supports implementation of state pattern. State Machine is defined a class containing one or more StateTable objeects, and using decorators to define Events and Transitions that are handled by the state machine. Individual states are Subclasses of the state machine, with a __metaclass__ specifier. Author: Rodney Drenth Date: January, 2009 Version: Whatever Copyright 2009, Rodney Drenth Permission to use granted under the terms and conditions of the Python Software Foundation License (http://www.python.org/download/releases/2.4.2/license/) """ #! /usr/bin/env Python # import types class _StateVariable( object ): """Used as attribute of a class to maintain state. State Variable objects are not directly instantiated in the user's state machine class but are instantiated by the StateTable class attribute defined within the state machine. """ def __init__(self, stateTable): """ Constructs state variable and sets it to the initial state """ self._current_state = stateTable.initialstate self._next_state = stateTable.initialstate self.sTable = stateTable def _toNextState(self, context): """Sets state of to the next state, if new state is different. calls onLeave and onEnter methods """ if self._next_state is not self._current_state: if hasattr(self._current_state, 'onLeave'): self._current_state.onLeave(context) self._current_state = self._next_state if hasattr(self._current_state, 'onEnter'): self._current_state.onEnter(context) def name(self): """Returns ame of current state.""" return self._current_state.__name__ def setXition(self, func): """ Sets the state to transitionto upon seeing Transition event""" # next_state = self.sTable.nextStates[self._current_state][func.__name__] next_state = self._current_state.nextStates[func.__name__] if next_state is not None: self._next_state = next_state def getFunc(self, func): """ Gets event handler function based on current State""" funky = getattr( self._current_state, func.__name__) # print 'current State:', self._current_state.__name__ if funky is None: raise NotImplementedError('%s.%s'%(self.name(),func.__name__)) # when funky.name is objCall, it means that we've recursed all the # way back to the context class and need to call func as a default return funky if funky.__name__ != 'objCall' else func class StateTable( object ): """Defines a state table for a state machine class A state table for a class is associated with the state variable in the instances of the class. The name of the state variable is given in the constructor to the StateTable object. StateTable objects are attributes of state machine classes, not intances of the state machine class. A state machine class can have more than one StateTable. """ def __init__(self, stateVarblName): """State Table initializer stateVarblName is the name of the associated state variable, which will be instantiated in each instance of the state machine class """ self.inst_state_name = stateVarblName self.eventList = [] self.initialstate = None nextStates = {} def initialize(self, obj ): """Initialization of StateTable and state varible obj is the instance of the state machine being initialized. This method must be called in the __init__ method of the user's state machine. """ obj.__dict__[self.inst_state_name] = _StateVariable( self ) def _addEventHandler(self, funcName): """Notifies State table of a method that's handle's an transition. This is called by @Transition and @TransitionEvent decorators, whose definitions are below. """ self.eventList.append(funcName) def nextStates(self, subState, nslList): """Sets up transitions from the state specified by subState subState is a state class, subclassed from user's state machine class nslList is a list of states that will be transitioned to upon Transitions. This functions maps each @Transition decorated method in the state machine class to a to the corresponding state in this list. None can be specified as a state to indicate the state should not change. """ if len(nslList) != len(self.eventList): raise RuntimeError( "Wrong number of states in transition list.") subState.nextStates = dict(zip(self.eventList, nslList)) # self.nextStates[subState] = dict(zip( self.eventList, nslList)) def Event( state_table ): """Decorator for indicating state dependant method Decorator is applied to methods of state machine class to indicate that invoking the method will call state dependant method. States are implemented as subclasses of the state machine class with a metaclass qualification. """ stateVarName = state_table.inst_state_name def wrapper(func): # no adding of event handler to statetable... def objCall( self, *args, **kwargs): state_var = getattr(self, stateVarName ) retn = state_var.getFunc(func)(self, *args, **kwargs) return retn return objCall return wrapper def Transition( state_table ): """Decorator for indicating the method causes a state transition. Decorator is applied to methods of the state machine class. Invokeing the method can cause a transition to another state. Transitions are defined using the nextStates method of the StateTable class """ stateVarName = state_table.inst_state_name def wrapper(func): state_table._addEventHandler( func.__name__) def objCall( self, *args, **kwargs): state_var = getattr(self, stateVarName ) state_var.setXition(func) rtn = func(self, *args, **kwargs) state_var._toNextState(self) return rtn return objCall return wrapper def TransitionEvent( state_table ): """Decorator for defining a method that both triggers a transition and invokes a state dependant method. This is equivalent to, but more efficient than using @Transition(stateTable) @Event(stateTable) """ stateVarName = state_table.inst_state_name def wrapper(func): state_table._addEventHandler( func.__name__) def objCall( self, *args, **kwargs): state_var = getattr(self, stateVarName ) # if not issubclass( state_var._current_state, self.__class__): # raise TypeError('expecting instance to be derived from %s'% baseClasss.__name__) state_var.setXition(func) retn = state_var.getFunc(func)(self, *args, **kwargs) state_var._toNextState(self) return retn return objCall return wrapper class _stateclass(type): """ A stateclass metaclass Modifies class so its subclass's methods so can be called as methods of a base class. Is used for implementing states """ def __init__(self, name, bases, cl_dict): self.__baseClass__ = _bcType # global - set by stateclass(), which follows if not issubclass(self, _bcType): raise TypeError( 'Class must derive from %s'%_bcType.__name__ ) type.__init__(self, name, bases, cl_dict) def __getattribute__(self, name): if name.startswith('__'): return type.__getattribute__(self, name) try: atr = self.__dict__[name] if type(atr) == types.FunctionType: atr = types.MethodType(atr, None, self.__baseClass__) except KeyError, ke: for bclas in self.__bases__: try: atr = getattr(bclas, name) break except AttributeError, ae: pass else: raise AttributeError( "'%s' has no attribute '%s'" % (self.__name__, name) ) return atr def stateclass( statemachineclass ): """A method that returns a metaclass for constructing states of the users state machine class """ global _bcType _bcType = statemachineclass return _stateclass # vim : set ai sw=3 et ts=6 : ----------------------------------------------------------- """ Example of setting up a state machine using the EasyStatePattern module. """ import EasyStatePattern as esp import math class Parent(object): """ the context for the state machine class """ moodTable = esp.StateTable('mood') def __init__(self, pocketbookCash, piggybankCash): """Instance initializer must invoke the initialize method of the StateTable """ Parent.moodTable.initialize( self) self.pocketBook = pocketbookCash self.piggyBank = piggybankCash """The Transiton decorator defines a method which causes transitions to other states""" @esp.Transition(moodTable) def getPromotion(self): pass @esp.Transition(moodTable) def severalDaysPass(self): pass """The Event decorator defines a method whose exact method will depend on the current state. These normally do not cause a state transition. For this example, this method will return an amount of money that the asker is to receive, which will depend on the parent's mood, the amount asked for, and the availability of money.""" @esp.Event(moodTable) def askForMoney(self, amount): pass """The TransitionEvent decorator acts like a combination of both the Transition and Event decorators. Calling this causes a transition(if defined in the state table) and the called function is state dependant.""" @esp.TransitionEvent(moodTable) def cashPaycheck(self, amount): pass @esp.TransitionEvent(moodTable) def getUnexpectedBill(self, billAmount ): pass """onEnter is called when entering a new state. This can be overriden in the individual state classes. onLeave (not defined here) is called upon leaving a state""" def onEnter(self): print 'Mood is now', self.mood.name() # # Below are defined the states. Each state is a class, States may be derived from # other states. Topmost states must have a __metaclass__ = stateclass( state_machine_class ) # declaration. # class Normal( Parent ): __metaclass__ = esp.stateclass( Parent ) """Normal becomes the name of the state.""" def getPromotion(self): """This shouldn't get called, since get was defined as a transition in the Parent context""" pass def severalDaysPass(self): """neither should this be called.""" pass def askForMoney(self, amount): amountToReturn = min(amount, self.pocketBook ) self.pocketBook -= amountToReturn if 40.0 < self.pocketBook: print 'Here you go, sport!' elif 0.0 < self.pocketBook < 40.00: print "Money doesn't grow on trees, you know." elif 0.0 < amountToReturn < amount: print "Sorry, honey, this is all I've got." elif amountToReturn == 0.0: print "I'm broke today ask your aunt." self.mood.nextState = Broke return amountToReturn def cashPaycheck(self, amount): self.pocketBook += .7 * amount self.piggyBank += .3*amount def getUnexpectedBill(self, billAmount ): amtFromPktBook = min(billAmount, self.pocketBook) rmngAmt = billAmount - amtFromPktBook self.piggyBank -= rmngAmt self.pocketBook -= amtFromPktBook class Happy( Parent ): __metaclass__ = esp.stateclass( Parent ) """Grouchy becomes the name of the state.""" def askForMoney(self, amount): availableMoney = self.pocketBook + self.piggyBank amountToReturn = max(min(amount, availableMoney), 0.0) amountFromPktbook = min(amountToReturn, self.pocketBook) self.pocketBook -= amountFromPktbook self.piggyBank -= (amountToReturn - amountFromPktbook) if 0.0 < amountToReturn < amount: print "Sorry, honey, this is all I've got." elif amountToReturn == 0.0: print "I'm broke today ask your aunt." self.mood.nextState = Broke else: print 'Here you go, sport!' return amountToReturn def cashPaycheck(self, amount): self.pocketBook += .75 * amount self.piggyBank += .25*amount def getUnexpectedBill(self, billAmount ): print "why do these things always happen?" amtFromPktBook = min(billAmount, self.pocketBook) rmngAmt = billAmount - amtFromPktBook self.piggyBank -= rmngAmt self.pocketBook -= amtFromPktBook def onEnter(self): print 'Yippee! Woo Hoo!', self.mood.name()*3 class Grouchy( Parent ): __metaclass__ = esp.stateclass( Parent ) """Grouchy becomes the name of the state.""" def askForMoney(self, amount): print """You're always spending too much. """ return 0.0 def cashPaycheck(self, amount): self.pocketBook += .70 * amount self.piggyBank += .30*amount def getUnexpectedBill(self, billAmount ): print 'These things always happen at the worst possible time!' amtFromPktBook = min(billAmount, self.pocketBook) rmngAmt = billAmount - amtFromPktBook self.piggyBank -= rmngAmt self.pocketBook -= amtFromPktBook class Broke( Normal ): # __metaclass__ = esp.stateclass( Parent ) """ No metaclass declaration as its as subclass of Grouchy. """ def cashPaycheck(self, amount): piggyBankAmt = min ( amount, max(-self.piggyBank, 0.0)) rmngAmount = amount - piggyBankAmount self.pocketBook += .40 * rmngAmount self.piggyBank += (.60 * rmngAmount + piggyBankAmt) def askForMoney(self, amount): amountToReturn = min(amount, self.pocketBook ) self.pocketBook -= amountToReturn if 40.0 < self.pocketBook: print 'Here you go, sport!' elif 0.0 < self.pocketBook < 40.00: print "Spend it wisely." elif 0.0 < amountToReturn < amount: print "This is all I've got." elif amountToReturn == 0.0: print "Sorry, honey, we're broke." self.mood.nextState = Broke return amountToReturn # After defining the states, The following lines set up the transitions. # We've set up four transitioning methods, # getPromotion, severalDaysPass, cashPaycheck, getUnexpectedBill # Therefore there are four states in each list, the ordering of the states in the # list corresponds toorder that the transitioning methods were defined. Parent.moodTable.nextStates( Normal, ( Happy, Normal, Normal, Grouchy )) Parent.moodTable.nextStates( Happy, ( Happy, Happy, Happy, Grouchy )) Parent.moodTable.nextStates( Grouchy, ( Happy, Normal, Grouchy, Grouchy )) Parent.moodTable.nextStates( Broke, ( Normal, Broke, Grouchy, Broke )) # This specifies the initial state. Instances of the Parent class are placed # in this state when they are initialized. Parent.moodTable.initialstate = Normal def Test(): dad = Parent(50.0, 60.0) amount = 20.0 print amount, dad.askForMoney(amount) print amount, dad.askForMoney(amount) dad.cashPaycheck( 40.0) print amount, dad.askForMoney(amount) dad.cashPaycheck( 40.0) dad.severalDaysPass() print amount, dad.askForMoney(amount) dad.getUnexpectedBill(100.0 ) print amount, dad.askForMoney(amount) dad.severalDaysPass() print amount, dad.askForMoney(amount) dad.severalDaysPass() dad.cashPaycheck( 100.0) print amount, dad.askForMoney(amount) dad.cashPaycheck( 50.0) print amount, dad.askForMoney(amount) dad.getPromotion() dad.cashPaycheck( 200.0) amount = 250 print amount, dad.askForMoney(amount) Test()