1 #=======================================================================
3 __version__
= '''0.3.41'''
4 __sub_version__
= '''20060731164833'''
5 __copyright__
= '''(c) Alex A. Naanou 2003'''
8 #-----------------------------------------------------------------------
11 This module defines a Finite State Machine framework.
14 # TODO add threading support for autostart FSMs...
15 # e.g. run start in a seporate context (thread, process, .. etc.)
16 # TODO add default and conditional error states...
19 #-----------------------------------------------------------------------
25 import pli
.pattern
.store
.stored
as stored
26 import pli
.event
as event
27 ##import pli.event.instanceevent as instanceevent
32 #-----------------------------------------------------------------------
33 #---------------------------------------------FiniteStateMachineError---
34 # TODO write more docs...
35 class FiniteStateMachineError(Exception):
40 #----------------------------------------------FiniteStateMachineStop---
41 class FiniteStateMachineStop(Exception):
46 #-----------------------------------------------------TransitionError---
47 # TODO write more docs...
48 class TransitionError(FiniteStateMachineError
):
54 #-----------------------------------------------------------------------
55 # TODO consistency checks...
56 # TODO more thorough documentation...
57 #-----------------------------------------------------------------------
58 # predicates based on transitions:
59 #-----------------------------------------------------------isinitial---
62 return True is s is the initial state of an FSM.
64 return hasattr(s
, '__is_initial_state__') and s
.__is
_initial
_state
__
67 #----------------------------------------------------------isterminal---
70 return True is s is a terminal state in an FSM.
72 return hasattr(s
, '__is_terminal_state__') and s
.__is
_terminal
_state
__
75 #--------------------------------------------------------------isnext---
78 return True if there is a direct transition from s1 to s2.
80 return s2
in s1
.iternextstates()
83 #--------------------------------------------------------------isprev---
86 return True if there is a direct transition from s2 to s1.
91 #------------------------------------------------------------isbefore---
92 # TODO make an iterative version...
93 # TODO might be good to make a breadth-first version...
94 def isbefore(s1
, s2
, exclude
=None):
96 return True if s2 is reachable from s1.
100 # this is depth first...
101 for n
in [s1
] + list(s1
.iternextstates()):
102 if n
not in exclude
and (isnext(n
, s2
) or isbefore(n
, s2
, exclude
=exclude
+[n
])):
107 #-------------------------------------------------------------isafter---
110 return True if s1 is reachable from s2.
112 return isbefore(s2
, s1
)
115 #-----------------------------------------------------------isbetween---
116 def isbetween(s1
, s2
, s3
):
118 return True if s1 is reachable from s2 and s3 is reachable from s1 (e.g. s1 is between s2 and s3).
120 return isbefore(s2
, s1
) and isbefore(s1
, s3
)
123 #------------------------------------------------------------isonpath---
124 def isonpath(s1
, s2
, s3
, *p
):
126 return True if s2 is reachable from s1 and s3 from s2 ... sN from sN-1.
129 for i
, n
in enumerate(p
[1:-1]):
130 if not isbetween(n
, p
[i
], p
[i
+2]):
135 #------------------------------------------------------------isinloop---
138 return true if s is inside a loop (e.g. s is reachable from s)
140 return isbefore(s
, s
)
144 #----------------------------------------------------------transition---
145 # TODO add support for string state names... (+ check consistency... (?))
146 # TODO add doc paramiter to transitions...
147 # TODO add support for logic var states (e.g. define a transition
148 # before the states are created...)
154 def transition(s1
, s2
, condition
=None, mode
=AUTO
):
156 create a transition from s1 to s2.
158 for more information see the BasicState.transition method.
160 if not isterminal(s1
):
161 s1
.transition(s2
, condition
, mode
)
163 raise FiniteStateMachineError
, 'can\'t add transition to a terminal state %s.' % s1
166 #-----------------------------------------------------------------------
167 #--------------------------------------------------------onEnterState---
168 # TODO write more docs...
169 class onEnterState(event
.InstanceEvent
):
172 __suppress_exceptions__
= False
174 def __init__(self
, state_name
, source
):
177 self
.event_source
= source
178 self
.state_name
= state_name
179 super(onEnterState
, self
).__init
__()
182 #---------------------------------------------------------onExitState---
183 # TODO write more docs...
184 class onExitState(event
.InstanceEvent
):
187 __suppress_exceptions__
= False
189 def __init__(self
, state_name
, source
):
192 self
.event_source
= source
193 self
.state_name
= state_name
194 super(onExitState
, self
).__init
__()
197 #--------------------------------------------onFiniteStateMachineStop---
198 # TODO write more docs...
199 class onFiniteStateMachineStop(event
.InstanceEvent
):
202 __suppress_exceptions__
= False
204 def __init__(self
, state_name
=None, fsm
=None):
207 self
.state_name
= state_name
208 # XXX this is a cyclic reference....
211 super(onFiniteStateMachineStop
, self
).__init
__()
214 #--------------------------------------------------FiniteStateMachine---
215 # WARNING: this has not been tested for being thread-safe...
216 # NOTE: whole FSMs can not (yet) be reproduced by deep copying... (not
218 # TODO test for safety of parallel execution of two fsm instances...
219 # (is this data-isolated?)
220 # TODO error state handler...
223 class FiniteStateMachine(state
.State
):
225 this is the base FSM class.
227 this acts as a collection of states.
229 if an initial state is defined for an FSM the instance of
230 FiniteStateMachine will change state to the initial
233 # class configuration:
234 # these will define the state enter/exit event constructors..
235 __state_enter_event__
= onEnterState
236 __state_exit_event__
= onExitState
237 # if this is set, all state changes without transitions will be
238 # blocked (e.g. raise an exception)...
239 __strict_transitions__
= True
240 # this will enable automatic state changing in a state loop...
241 __auto_change_state__
= True
242 # if this is set the FSM will block until a transition is
244 __pole_for_next_state__
= False
245 # this if set will determain the time to sleep until the next
247 __pole_delay__
= None
248 # this if true will start the fsm on init...
249 __auto_start__
= False
250 # this will define the state to which we will auto-change...
251 __next_state__
= None
255 __initial_state__
= None
256 _stop_exception
= None
259 # this is the super safe version of init.... (incase we mix the
260 # incompatible classes....)
261 def __init__(self
, *p
, **n
):
265 super(FiniteStateMachine
, self
).__init
__(*p
, **n
)
268 # store a ref to the original startup class....
269 # NOTE: this is an instance attribute! (might pose a problem on
271 self
.__startup
_class
__ = self
.__class
__
272 # change state to the initial state if one defined...
273 if hasattr(self
, '__initial_state__') and self
.__initial
_state
__ != None:
274 self
.changestate(self
.__initial
_state
__)
275 # create a general fsm stop event...
276 self
.onFiniteStateMachineStop
= onFiniteStateMachineStop(fsm
=self
)
278 if hasattr(self
, '__auto_start__') and self
.__auto
_start
__:
282 this is the FSM main loop.
284 ## # sanity checks...
285 ## if not hasattr(self, '__fsm__'):
286 ## raise FiniteStateMachineError, 'can\'t start a raw FSM object (change to a valid state).'
288 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
289 if hasattr(self
, '_fsm_running') and self
._fsm
_running
:
290 raise FiniteStateMachineError
, 'the %s FSM is already running.' % self
292 self
._fsm
_running
= True
295 # break on terminal state...
297 # fire the state stop event...
298 evt_name
= 'onStop' + self
.__class
__.__name
__
299 if hasattr(self
, evt_name
):
300 getattr(self
, evt_name
).fire()
304 ## if self._fsm_running == False:
305 ## if self._stop_exception != None:
306 ## raise self._stop_exception, self._stop_reason
307 ## return self._stop_reason
308 if self
.__next
_state
__ != None:
310 tostate
= self
.__next
_state
__
311 self
.__next
_state
__ = None
312 self
._changestate
(tostate
)
313 except FiniteStateMachineStop
:
316 self
._fsm
_running
= False
317 # fire the fsm stop event...
318 self
.onFiniteStateMachineStop
.fire()
320 raise FiniteStateMachineError
, 'can\'t start a manual (non-auto-change-state) FSM.'
323 ## this will step through the fsm.
326 ## def stop(self, reason=None, exception=None):
329 ## self._stop_exception = exception
330 ## self._stop_reason = reason
331 ## self._fsm_running = False
332 # TODO automaticly init newly added states per FSM object on their
333 # (event) addition to the FSM class...
334 # ...or do a lazy init (as in RPG.action)
335 # actually the best thing would be to do both...
336 def initstates(self
):
338 this will init state event.
340 NOTE: it is safe to run this more than once (though this might not be very fast).
343 for state
in self
.__states
__.values():
344 state_name
= state
.__name
__
345 for evt_name
, evt_cls
in (('onEnter' + state_name
, self
.__state
_enter
_event
__),
346 ('onExit' + state_name
, self
.__state
_exit
_event
__)):
347 if not hasattr(self
, evt_name
):
348 setattr(self
, evt_name
, evt_cls(state_name
, self
))
350 if isterminal(state
):
351 setattr(self
, 'onStop' + state_name
, onFiniteStateMachineStop(state_name
))
352 def changestate(self
, tostate
):
355 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
356 self
.__next
_state
__ = tostate
358 self
._changestate
(tostate
)
359 def _changestate(self
, tostate
):
362 # call the __onexitstate__...
363 if hasattr(self
, '__onexitstate__'):
364 self
.__onexitstate
__()
365 # fire the exit event...
366 evt_name
= 'onExit' + self
.__class
__.__name
__
367 if hasattr(self
, evt_name
):
368 getattr(self
, evt_name
).fire()
369 # change the state...
370 super(FiniteStateMachine
, self
).changestate(tostate
)
371 # fire the enter event...
372 evt_name
= 'onEnter' + self
.__class
__.__name
__
373 if hasattr(self
, evt_name
):
374 getattr(self
, evt_name
).fire()
375 # run the post init method...
378 if hasattr(self
, '__onafterstatechange__'):
379 self
.__onafterstatechange
__()
380 except FiniteStateMachineStop
, e
:
382 if hasattr(self
, '__onenterstate__'):
383 self
.__onenterstate
__()
384 # if a stop condition occurred, pass it on...
389 #--------------------------------------------------------------_State---
390 class _StoredState(stored
._StoredClass
):
392 this meta-class will register the state classes with the FSM.
394 # _StoredClass configuration:
395 __class_store_attr_name__
= '__fsm__'
397 def storeclass(cls
, name
, state
):
399 register the state with the FSM.
401 fsm
= getattr(cls
, cls
.__class
_store
_attr
_name
__)
402 # check state name...
403 if hasattr(fsm
, name
):
404 raise NameError, 'state named "%s" already exists.' % name
405 # register the state...
406 if fsm
.__states
__ == None:
407 fsm
.__states
__ = {name
: state
}
409 fsm
.__states
__[name
] = state
410 # set the fsm initial state...
411 if hasattr(state
, '__is_initial_state__') and state
.__is
_initial
_state
__:
412 if hasattr(fsm
, '__initial_state__') and fsm
.__initial
_state
__ != None:
413 raise FiniteStateMachineError
, 'an initial state is already defined for the %s FSM.' % fsm
414 fsm
.__initial
_state
__ = state
417 #---------------------------------------------------------------State---
418 # TODO add doc paramiter to transitions...
419 # TODO error state handler...
421 # TODO revise magic method names and function...
422 # XXX see if this need a redesign...
423 class BasicState(FiniteStateMachine
):
425 this is the base state class for the FSM framwork.
428 transition : this will create a transition (see the method
429 doc for more detail).
430 NOTE: this is a class method.
432 there are three utility methods that can be defined:
433 __runonce__ : this will be run only once per state, this is
434 done mainly to register callbacks.
435 __onstatechange__ : this is run once per state change, right after
436 the change is made and just before the onEnter
438 __onafterstatechange__
439 : this is called after the state is finalized.
440 that is, just after the state onEnter event
442 NOTE: by default, this will select the first
443 usable transition and use it to change
445 __onenterstate__ : this is called once per state change, just after
446 the onEnter event is fired.
447 NOTE: this is fired after the __onafterstatechange__
448 method, and does NOTHING by default.
449 __onexitstate__ : this is called once per state change, just before
450 the change and before the onExit event is fired.
451 __resolvestatechange__
452 : this is called by the above method if no usable
453 transition was found and current state is not
455 NOTE: all of the above methods receive no arguments but the object
456 reference (e.g. self).
458 on state instance creation, two events will get defined and added to the FSM:
459 onEnter<state-name> : this is fired on state change, just after the
460 __onstatechange__ method is finished.
461 onExit<state-name> : this is fired just before the state is changed,
462 just after the __onexitstate__ is done.
464 for more information see: pli.pattern.state.State
467 __metaclass__
= _StoredState
469 # class configuration:
470 # this is the class of fsm this state will belong to
472 # if this is set the state will be registered as initial/start state
473 __is_initial_state__
= False
474 # if this is set the state will be registered as terminal/end state
475 __is_terminal_state__
= False
477 ## ##!!! not yet implemmented section....
478 ## # Error Handling setop options:
479 ## # this will enable/disable the error case exit mechanism...
480 ## __error_exit_enabled__ = False
481 ## # if this is set, the value will be used a an error case exit from
483 ## __error_state__ = None
484 ## # if this is true, this state will be used as the default error
485 ## # exit for the fsm...
486 ## # NOTE: there may be only one default error exit.
487 ## __is_default_error_state__ = False
489 # StoredClass options:
490 # do not register this class... (not inherited)
491 __ignore_registration__
= True
492 # auto register all subclasses (inherited)
493 __auto_register_type__
= True
498 # TODO add support for string state names... (+ check consistency... (?))
499 # TODO write a "removetransition" method....
500 def transition(cls
, tostate
, condition
=None, mode
=AUTO
):
502 this will create a transition from the current state to the tostate.
504 the condition, if given, is passed the FSM object and if True is returned,
505 the transition is finalized, else the transition is abborted.
508 AUTO - for automatic transitioning (default).
509 MANUAL - for manual transitioning.
511 NOTE: the transitions are tested in order of definition.
512 NOTE: if automatic transitioning is enabled the MANUAL mode transitions
513 are skipped when searching for an appropriate exit.
515 transitions
= cls
._transitions
516 if transitions
== None:
517 transitions
= cls
._transitions
= {tostate
: (condition
, mode
)}
518 ## elif tostate in transitions:
519 ## raise TransitionError, 'a transition from %s to %s already exists.' % (cls, tostate)
521 cls
._transitions
[tostate
] = (condition
, mode
)
522 transition
= classmethod(transition
)
523 def changestate(self
, tostate
):
525 change the object stete
527 # prevent moving out of a terminal state...
528 if hasattr(self
, '__is_terminal_state__') and self
.__is
_terminal
_state
__:
529 raise FiniteStateMachineError
, 'can\'t change state of a terminal FSM node %s.' % self
531 # check for transitions...
532 if hasattr(fsm
, '__strict_transitions__') and fsm
.__strict
_transitions
__ and \
533 (not hasattr(self
, '_transitions') or self
._transitions
== None or \
534 tostate
not in self
._transitions
):
535 raise TransitionError
, 'can\'t change state of %s to state %s without a transition.' % (self
, tostate
)
537 transitions
= self
._transitions
538 if transitions
!= None and tostate
in transitions \
539 and transitions
[tostate
][0] != None and not transitions
[tostate
][0](self
):
540 raise TransitionError
, 'conditional transition from %s to state %s failed.' % (self
, tostate
)
541 super(BasicState
, self
).changestate(tostate
)
542 # restart the fsm if __auto_change_state__ is set and we are
544 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__ \
545 and not self
._fsm
_running
:
547 def iternextstates(self
):
549 this will iterate through the states directly reachable from
550 self (e.g. to which there are direct transitions).
552 if self
._transitions
== None:
554 for n
in self
._transitions
:
556 iternextstates
= classmethod(iternextstates
)
557 # this method is called after the state is finalized (e.g. after the
558 # __onstatechange__ is called and the onEnter event is processed).
559 ##!!!!! might be wrong: check the situations where FiniteStateMachineStop is raised... (especially on FSM init)
560 def __onafterstatechange__(self
):
562 this will try to next change state.
564 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
566 transitions
= self
._transitions
567 if transitions
!= None:
568 for tostate
, (cond
, mode
) in transitions
.items():
571 self
.changestate(tostate
)
576 ## if not hasattr(self, '__is_terminal_state__') or not self.__is_terminal_state__:
577 ## if hasattr(self, '__resolvestatechange__'):
578 ## # try to save the day and call the resolve method...
579 ## return self.__resolvestatechange__()
580 ## # we endup here if there are no exiting transitions
581 ## # from a non-terminal state...
582 ## # Q: whay do we need a terminal state?
583 ## # A: to prevent exiting from it...
584 ## raise FiniteStateMachineError, 'can\'t exit a non-terminal state %s.' % self
585 if (not hasattr(self
, '__is_terminal_state__') or not self
.__is
_terminal
_state
__) \
586 and hasattr(self
, '__resolvestatechange__'):
587 # try to save the day and call the resolve method...
588 return self
.__resolvestatechange
__()
589 elif hasattr(self
, '__pole_for_next_state__') and self
.__pole
_for
_next
_state
__:
590 if hasattr(self
, '__pole_delay__') and self
.__pole
_delay
__ != None:
591 time
.sleep(self
.__pole
_delay
__)
594 raise FiniteStateMachineStop
, 'stop.'
595 # this is here for documentation...
596 ## def __resolvestatechange__(self):
598 ## this is called if no transition from current state is found, to
599 ## resolve the situation.
604 #-------------------------------------------------StateWithAttrPriority---
605 class StateWithAttrPriority(BasicState
):
607 this will add the ability to resolve names to the startup class (e.g. the
608 class that was defined as FSM base, or in other words, the desendant of
609 the FSM used to create the FSM instance).
611 the resolution order is as folows:
612 1. local object state.
613 2. names defined in __startupfirstattrs__ in startup class.
617 NOTE: after a name is found no further searching is done.
618 NOTE: in steps 2-4 class data is searched (as local data is stored in the
619 current object and is not affected by state change directly).
621 for further information see the BasicState class.
623 __ignore_registration__
= True
624 __startupfirstattrs__
= (
625 # interface specific...
629 # XXX this will make attr resolution quite slow... (find a better
630 # way.... if possible)
631 def __getattribute__(self
, name
):
633 the resolution order is as folows:
634 1. local object state.
635 2. names defined in __startupfirstattrs__ in the startup class.
639 getattribute
= super(StateWithAttrPriority
, self
).__getattribute
__
640 # first check the local state...
641 __dict__
= getattribute('__dict__')
643 return __dict__
[name
]
644 # check the class data...
646 if name
in getattribute('__startupfirstattrs__'):
649 res
= getattr(getattribute('__startup_class__'), name
)
650 # XXX implementation dependant.... (is there a
652 if type(res
) == new
.instancemethod
:
653 res
= new
.instancemethod(res
.im_func
, self
, self
.__class
__)
655 except AttributeError:
657 return getattribute(name
)
661 return getattribute(name
)
662 except AttributeError:
664 res
= getattr(getattribute('__startup_class__'), name
)
665 # XXX implementation dependant.... (is there a
667 if type(res
) == new
.instancemethod
:
668 res
= new
.instancemethod(res
.im_func
, self
, self
.__class
__)
671 ## return getattribute(name)
672 except AttributeError:
674 raise AttributeError, '%s object has no attribute "%s"' % (self
, name
)
675 ## # Q: does this need to be __gatattr__ or __getattribute__ ????
676 ## # NOTE: this might make the attr access quite slow...
677 ## # Q: is there a faster way??? (via MRO manipulation or something...)
678 ## def __getattr__(self, name):
680 ## this will proxy the attr access to the original startup class....
682 ## ##!!! check for looping search !!!##
683 ## # get the name in the startup class...
685 ## return getattr(self.__startup_class__, name)
686 ## except AttributeError:
687 ## raise AttributeError, '%s object has no attribute "%s"' % (self, name)
690 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
691 # this is here to simplify things...
692 State
= StateWithAttrPriority
695 #--------------------------------------------------------InitialState---
696 class InitialState(State
):
698 initial state base class.
700 __is_initial_state__
= True
701 __ignore_registration__
= True
704 #-------------------------------------------------------TerminalState---
705 class TerminalState(State
):
707 terminal state base class.
709 __is_terminal_state__
= True
710 __ignore_registration__
= True
714 #=======================================================================
715 # vim:set ts=4 sw=4 nowrap :