removed generator imports (style incompatibility with Py25)
[pli.git] / pli / pattern / state / fsm.py
blobac380fdffa768d47f67706a1b2f01300af83a532
1 #=======================================================================
3 __version__ = '''0.3.41'''
4 __sub_version__ = '''20060731164833'''
5 __copyright__ = '''(c) Alex A. Naanou 2003'''
8 #-----------------------------------------------------------------------
10 __doc__ = '''\
11 This module defines a Finite State Machine framework.
12 '''
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 #-----------------------------------------------------------------------
21 import time
22 import new
23 import types
25 import pli.pattern.store.stored as stored
26 import pli.event as event
27 ##import pli.event.instanceevent as instanceevent
29 import state
32 #-----------------------------------------------------------------------
33 #---------------------------------------------FiniteStateMachineError---
34 # TODO write more docs...
35 class FiniteStateMachineError(Exception):
36 '''
37 '''
40 #----------------------------------------------FiniteStateMachineStop---
41 class FiniteStateMachineStop(Exception):
42 '''
43 '''
46 #-----------------------------------------------------TransitionError---
47 # TODO write more docs...
48 class TransitionError(FiniteStateMachineError):
49 '''
50 '''
54 #-----------------------------------------------------------------------
55 # TODO consistency checks...
56 # TODO more thorough documentation...
57 #-----------------------------------------------------------------------
58 # predicates based on transitions:
59 #-----------------------------------------------------------isinitial---
60 def isinitial(s):
61 '''
62 return True is s is the initial state of an FSM.
63 '''
64 return hasattr(s, '__is_initial_state__') and s.__is_initial_state__
67 #----------------------------------------------------------isterminal---
68 def isterminal(s):
69 '''
70 return True is s is a terminal state in an FSM.
71 '''
72 return hasattr(s, '__is_terminal_state__') and s.__is_terminal_state__
75 #--------------------------------------------------------------isnext---
76 def isnext(s1, s2):
77 '''
78 return True if there is a direct transition from s1 to s2.
79 '''
80 return s2 in s1.iternextstates()
83 #--------------------------------------------------------------isprev---
84 def isprev(s1, s2):
85 '''
86 return True if there is a direct transition from s2 to s1.
87 '''
88 return isnext(s2, 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):
95 '''
96 return True if s2 is reachable from s1.
97 '''
98 if exclude == None:
99 exclude = []
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])):
103 return True
104 return False
107 #-------------------------------------------------------------isafter---
108 def isafter(s1, s2):
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.
128 r = (s1, s2, s3) + p
129 for i, n in enumerate(p[1:-1]):
130 if not isbetween(n, p[i], p[i+2]):
131 return False
132 return True
135 #------------------------------------------------------------isinloop---
136 def isinloop(s):
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...)
150 # modes:
151 AUTO = 0
152 MANUAL = 1
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)
162 else:
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....
209 if fsm != None:
210 self.fsm = fsm
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
217 # tested)
218 # TODO test for safety of parallel execution of two fsm instances...
219 # (is this data-isolated?)
220 # TODO error state handler...
221 # TODO "Sub-FSMs"
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
231 state on init.
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
243 # available...
244 __pole_for_next_state__ = False
245 # this if set will determain the time to sleep until the next
246 # pole...
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
253 # class data:
254 __states__ = None
255 __initial_state__ = None
256 _stop_exception = None
257 _stop_reason = None
259 # this is the super safe version of init.... (incase we mix the
260 # incompatible classes....)
261 def __init__(self, *p, **n):
264 # super-safe...
265 super(FiniteStateMachine, self).__init__(*p, **n)
266 # init all states...
267 self.initstates()
268 # store a ref to the original startup class....
269 # NOTE: this is an instance attribute! (might pose a problem on
270 # serializatio....)
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)
277 # autostart?
278 if hasattr(self, '__auto_start__') and self.__auto_start__:
279 self.start()
280 def start(self):
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).'
287 # start the loop...
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
291 try:
292 self._fsm_running = True
293 try:
294 while True:
295 # break on terminal state...
296 if isterminal(self):
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()
301 # exit...
302 break
303 ## # handle stops...
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:
309 # change state...
310 tostate = self.__next_state__
311 self.__next_state__ = None
312 self._changestate(tostate)
313 except FiniteStateMachineStop:
314 pass
315 finally:
316 self._fsm_running = False
317 # fire the fsm stop event...
318 self.onFiniteStateMachineStop.fire()
319 else:
320 raise FiniteStateMachineError, 'can\'t start a manual (non-auto-change-state) FSM.'
321 ## def step(self):
322 ## '''
323 ## this will step through the fsm.
324 ## '''
325 ## pass
326 ## def stop(self, reason=None, exception=None):
327 ## '''
328 ## '''
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).
342 # init all states...
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))
349 # the stop event...
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
357 else:
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...
376 e = None
377 try:
378 if hasattr(self, '__onafterstatechange__'):
379 self.__onafterstatechange__()
380 except FiniteStateMachineStop, e:
381 pass
382 if hasattr(self, '__onenterstate__'):
383 self.__onenterstate__()
384 # if a stop condition occurred, pass it on...
385 if e != None:
386 raise e
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}
408 else:
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...
420 # TODO "Sub-FSMs"
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.
427 interface methods:
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
437 event is fired.
438 __onafterstatechange__
439 : this is called after the state is finalized.
440 that is, just after the state onEnter event
441 processing is done.
442 NOTE: by default, this will select the first
443 usable transition and use it to change
444 state.
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
454 terminal.
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
471 __fsm__ = None
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
482 ## # this state...
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
495 # class data:
496 _transitions = None
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.
507 the mode can be:
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)
520 else:
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
530 fsm = self.__fsm__
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)
536 # check condition...
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
543 # not running...
544 if hasattr(self, '__auto_change_state__') and self.__auto_change_state__ \
545 and not self._fsm_running:
546 self.start()
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:
553 return
554 for n in self._transitions:
555 yield n
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__:
565 while True:
566 transitions = self._transitions
567 if transitions != None:
568 for tostate, (cond, mode) in transitions.items():
569 try:
570 if mode == AUTO:
571 self.changestate(tostate)
572 return
573 except:
574 ##!!!
575 pass
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__)
592 continue
593 else:
594 raise FiniteStateMachineStop, 'stop.'
595 # this is here for documentation...
596 ## def __resolvestatechange__(self):
597 ## '''
598 ## this is called if no transition from current state is found, to
599 ## resolve the situation.
600 ## '''
601 ## pass
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.
614 3. current class
615 4. 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...
626 '__implemments__',
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.
636 3. current class.
637 4. startup class.
639 getattribute = super(StateWithAttrPriority, self).__getattribute__
640 # first check the local state...
641 __dict__ = getattribute('__dict__')
642 if name in __dict__:
643 return __dict__[name]
644 # check the class data...
645 try:
646 if name in getattribute('__startupfirstattrs__'):
647 try:
648 # get startup...
649 res = getattr(getattribute('__startup_class__'), name)
650 # XXX implementation dependant.... (is there a
651 # better way?)
652 if type(res) == new.instancemethod:
653 res = new.instancemethod(res.im_func, self, self.__class__)
654 return res
655 except AttributeError:
656 # get current...
657 return getattribute(name)
658 else:
659 try:
660 # get current...
661 return getattribute(name)
662 except AttributeError:
663 # get startup...
664 res = getattr(getattribute('__startup_class__'), name)
665 # XXX implementation dependant.... (is there a
666 # better way?)
667 if type(res) == new.instancemethod:
668 res = new.instancemethod(res.im_func, self, self.__class__)
669 return res
670 ## # get current...
671 ## return getattribute(name)
672 except AttributeError:
673 # fail...
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):
679 ## '''
680 ## this will proxy the attr access to the original startup class....
681 ## '''
682 ## ##!!! check for looping search !!!##
683 ## # get the name in the startup class...
684 ## try:
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 :