1 #=======================================================================
3 __version__
= '''0.3.19'''
4 __sub_version__
= '''20041118120308'''
5 __copyright__
= '''(c) Alex A. Naanou 2003'''
8 #-----------------------------------------------------------------------
11 This module defines a Finite State Machine framework.
15 #-----------------------------------------------------------------------
17 import pli
.pattern
.store
.stored
as stored
18 import pli
.event
as event
19 ##import pli.event.instanceevent as instanceevent
24 #-----------------------------------------------------------------------
25 #---------------------------------------------FiniteStateMachineError---
26 # TODO write more docs...
27 class FiniteStateMachineError(Exception):
32 #----------------------------------------------FiniteStateMachineStop---
33 class FiniteStateMachineStop(Exception):
38 #-----------------------------------------------------TransitionError---
39 # TODO write more docs...
40 class TransitionError(FiniteStateMachineError
):
46 #-----------------------------------------------------------------------
47 # TODO consistency checks...
48 # TODO more thorough documentation...
49 #-----------------------------------------------------------------------
50 # predicates based on transitions:
51 #-----------------------------------------------------------isinitial---
54 return True is s is the initial state of an FSM.
56 return hasattr(s
, '__is_initial_state__') and s
.__is
_initial
_state
__
59 #----------------------------------------------------------isterminal---
62 return True is s is a terminal state in an FSM.
64 return hasattr(s
, '__is_terminal_state__') and s
.__is
_terminal
_state
__
67 #--------------------------------------------------------------isnext---
70 return True if there is a direct transition from s1 to s2.
72 return s2
in s1
.iternextstates()
75 #--------------------------------------------------------------isprev---
78 return True if there is a direct transition from s2 to s1.
83 #------------------------------------------------------------isbefore---
84 # TODO make an iterative version...
85 # TODO might be good to make a breadth-first version...
86 def isbefore(s1
, s2
, exclude
=None):
88 return True if s2 is reachable from s1.
92 # this is depth first...
93 for n
in [s1
] + list(s1
.iternextstates()):
94 if n
not in exclude
and (isnext(n
, s2
) or isbefore(n
, s2
, exclude
=exclude
+[n
])):
99 #-------------------------------------------------------------isafter---
102 return True if s1 is reachable from s2.
104 return isbefore(s2
, s1
)
107 #-----------------------------------------------------------isbetween---
108 def isbetween(s1
, s2
, s3
):
110 return True if s1 is reachable from s2 and s3 is reachable from s1 (e.g. s1 is between s2 and s3).
112 return isbefore(s2
, s1
) and isbefore(s1
, s3
)
115 #------------------------------------------------------------isonpath---
116 def isonpath(s1
, s2
, s3
, *p
):
118 return True if s2 is reachable from s1 and s3 from s2 ... sN from sN-1.
121 for i
, n
in enumerate(p
[1:-1]):
122 if not isbetween(n
, p
[i
], p
[i
+2]):
127 #------------------------------------------------------------isinloop---
130 return true if s is inside a loop (e.g. s is reachable from s)
132 return isbefore(s
, s
)
136 #----------------------------------------------------------transition---
137 # TODO add support for string state names... (+ check consistency... (?))
138 # TODO add doc paramiter to transitions...
139 def transition(s1
, s2
, condition
=None):
141 create a transition from s1 to s2.
143 if not isterminal(s1
):
144 s1
.transition(s2
, condition
)
146 raise FiniteStateMachineError
, 'can\'t add transition to a terminal state %s.' % s1
149 #-----------------------------------------------------------------------
150 #--------------------------------------------------------onEnterState---
151 # TODO write more docs...
152 class onEnterState(event
.InstanceEvent
):
155 __suppress_exceptions__
= False
157 def __init__(self
, state_name
):
160 self
.state_name
= state_name
161 super(onEnterState
, self
).__init
__()
164 #---------------------------------------------------------onExitState---
165 # TODO write more docs...
166 class onExitState(event
.InstanceEvent
):
169 __suppress_exceptions__
= False
171 def __init__(self
, state_name
):
174 self
.state_name
= state_name
175 super(onExitState
, self
).__init
__()
178 #--------------------------------------------onFiniteStateMachineStop---
179 class onFiniteStateMachineStop(event
.InstanceEvent
):
182 __suppress_exceptions__
= False
184 def __init__(self
, state_name
=None, fsm
=None):
187 self
.state_name
= state_name
188 # XXX this is a cyclic reference....
191 super(onFiniteStateMachineStop
, self
).__init
__()
194 #--------------------------------------------------FiniteStateMachine---
195 # WARNING: this has not been tested for being thread-safe...
196 # NOTE: whole FSMs can not (yet) be reproduced by deep copying... (not
198 # TODO test for safety of parallel execution of two fsm instances...
199 # TODO write more docs...
200 # TODO error state handler...
203 # TODO name resolution through the fsm.... (as a default to the startup
205 # this should look like the FSM subclass is mixed-in (or a
206 # superclass of every state) to the originil FSM, yet not touching
209 class FiniteStateMachine(state
.State
):
211 this is the base FSM class.
213 this acts as a collection of states.
215 if an initial state is defined for an FSM the instance of
216 FiniteStateMachine will change state to the initial
219 # class configuration:
220 # these will define the state enter/exit event constructors..
221 __state_enter_event__
= onEnterState
222 __state_exit_event__
= onExitState
223 # if this is set, all state changes without transitions will be
224 # blocked (e.g. raise an exception)...
225 __strict_transitions__
= True
226 # this will enable automatic state changing...
227 __auto_change_state__
= True
228 # this will define the state to which we will auto-change...
229 __next_state__
= None
233 __initial_state__
= None
234 _stop_exception
= None
237 # this is the super safe version of init.... (incase w mix the
238 # incompatible classes....)
239 def __init__(self
, *p
, **n
):
243 super(FiniteStateMachine
, self
).__init
__(*p
, **n
)
246 # store a ref to the original startup class....
247 # NOTE: this is an instance attribute! (might pose a problem on
249 self
.__startup
_class
__ = self
.__class
__
250 # change state to the initial state if one defined...
251 if hasattr(self
, '__initial_state__') and self
.__initial
_state
__ != None:
252 self
.changestate(self
.__initial
_state
__)
253 # create a general fsm stop event...
254 self
.onFiniteStateMachineStop
= onFiniteStateMachineStop(fsm
=self
)
257 this is the FSM main loop.
259 ## # sanity checks...
260 ## if not hasattr(self, '__fsm__'):
261 ## raise FiniteStateMachineError, 'can\'t start a raw FSM object (change to a valid state).'
263 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
264 if hasattr(self
, '_running') and not self
._running
:
265 raise FiniteStateMachineError
, 'the %s FSM is already running.' % self
269 # break on terminal state...
271 # fire the state stop event...
272 evt_name
= 'onStop' + self
.__class
__.__name
__
273 if hasattr(self
, evt_name
):
274 getattr(self
, evt_name
).fire()
278 ## if self._running == False:
279 ## if self._stop_exception != None:
280 ## raise self._stop_exception, self._stop_reason
281 ## return self._stop_reason
282 if self
.__next
_state
__ != None:
284 tostate
= self
.__next
_state
__
285 self
.__next
_state
__ = None
286 self
._changestate
(tostate
)
287 except FiniteStateMachineStop
:
289 self
._running
= False
290 # fire the fsm stop event...
291 self
.onFiniteStateMachineStop
.fire()
293 raise FiniteStateMachineError
, 'can\'t start a manual (non-auto-change-state) FSM.'
294 ## def stop(self, reason=None, exception=None):
297 ## self._stop_exception = exception
298 ## self._stop_reason = reason
299 ## self._running = False
300 # TODO automaticly init newly added states per FSM object on their
301 # (event) addition to the FSM class...
302 # ...or do a lazy init (as in RPG.action)
303 # actually the best thing would be to do both...
304 def initstates(self
):
306 this will init state event.
308 NOTE: it is safe to run this more than once (though this might not be very fast).
311 for state
in self
.__states
__.values():
312 state_name
= state
.__name
__
313 for evt_name
, evt_cls
in (('onEnter' + state_name
, self
.__state
_enter
_event
__),
314 ('onExit' + state_name
, self
.__state
_exit
_event
__)):
315 if not hasattr(self
, evt_name
):
316 setattr(self
, evt_name
, evt_cls(state_name
))
318 if isterminal(state
):
319 setattr(self
, 'onStop' + state_name
, onFiniteStateMachineStop(state_name
))
320 def changestate(self
, tostate
):
323 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
324 self
.__next
_state
__ = tostate
326 self
._changestate
(tostate
)
327 def _changestate(self
, tostate
):
330 # call the __onexitstate__...
331 if hasattr(self
, '__onexitstate__'):
332 self
.__onexitstate
__()
333 # fire the exit event...
334 evt_name
= 'onExit' + self
.__class
__.__name
__
335 if hasattr(self
, evt_name
):
336 getattr(self
, evt_name
).fire()
337 # change the state...
338 super(FiniteStateMachine
, self
).changestate(tostate
)
339 # fire the enter event...
340 evt_name
= 'onEnter' + self
.__class
__.__name
__
341 if hasattr(self
, evt_name
):
342 getattr(self
, evt_name
).fire()
343 # run the post init method...
344 if hasattr(self
, '__onafterstatechange__'):
345 self
.__onafterstatechange
__()
346 if hasattr(self
, '__onenterstate__'):
347 self
.__onenterstate
__()
350 #--------------------------------------------------------------_State---
351 class _StoredState(stored
._StoredClass
):
353 this meta-class will register the state classes with the FSM.
355 # _StoredClass configuration:
356 __class_store_attr_name__
= '__fsm__'
358 def storeclass(cls
, name
, state
):
360 register the state with the FSM.
362 fsm
= getattr(cls
, cls
.__class
_store
_attr
_name
__)
363 # check state name...
364 if hasattr(fsm
, name
):
365 raise NameError, 'state named "%s" already exists.' % name
366 # register the state...
367 if fsm
.__states
__ == None:
368 fsm
.__states
__ = {name
: state
}
370 fsm
.__states
__[name
] = state
371 # set the fsm initial state...
372 if hasattr(state
, '__is_initial_state__') and state
.__is
_initial
_state
__:
373 if hasattr(fsm
, '__initial_state__') and fsm
.__initial
_state
__ != None:
374 raise FiniteStateMachineError
, 'an initial state is already defined for the %s FSM.' % fsm
375 fsm
.__initial
_state
__ = state
378 #---------------------------------------------------------------State---
379 # TODO write more docs...
380 # TODO add doc paramiter to transitions...
381 # TODO error state handler...
383 # TODO revise magic method names and function...
384 class State(FiniteStateMachine
):
386 this is the base state class for the FSM framwork.
388 there are three utility methods that can be defined:
389 __runonce__ : this will be run only once per state, this is
390 done mainly to register callbacks.
391 __onstatechange__ : this is run once per state change, right after
392 the change is made and just before the onEnter
394 __onafterstatechange__
395 : this is called after the state is finalized.
396 that is, just after the state onEnter event
398 NOTE: by default, this will select the first
399 usable transition and use it to change
401 __onenterstate__ : this is called once per state change, just after
402 the onEnter event is fired.
403 NOTE: this is fired after the __onafterstatechange__
404 method, and does NOTHING by default.
405 __onexitstate__ : this is called once per state change, just before
406 the change and before the onExit event is fired.
407 __resolvestatechange__
408 : this is called by the above method if no usable
409 transition was found and current state is not
411 NOTE: all of the above methods receive no arguments but the object
412 reference (e.g. self).
414 on state instance creation, two events will get defined and added to the FSM:
415 onEnter<state-name> : this is fired on state change, just after the
416 __onstatechange__ method is finished.
417 onExit<state-name> : this is fired just before the state is changed,
418 just after the __onexitstate__ is done.
420 for more information see: pli.pattern.state.State
423 __metaclass__
= _StoredState
425 # class configuration:
426 # this is the class of fsm this state will belong to
428 # if this is set the state will be registered as initial/start state
429 __is_initial_state__
= False
430 # if this is set the state will be registered as terminal/end state
431 __is_terminal_state__
= False
433 ## ##!!! not yet implemmented section....
434 ## # Error Handling setop options:
435 ## # this will enable/disable the error case exit mechanism...
436 ## __error_exit_enabled__ = False
437 ## # if this is set, the value will be used a an error case exit from
439 ## __error_state__ = None
440 ## # if this is true, this state will be used as the default error
441 ## # exit for the fsm...
442 ## # NOTE: there may be only one default error exit.
443 ## __is_default_error_state__ = False
445 # StoredClass options:
446 # do not register this class... (not inherited)
447 __ignore_registration__
= True
448 # auto register all subclasses (inherited)
449 __auto_register_type__
= True
454 # TODO add support for string state names... (+ check consistency... (?))
455 # TODO write a "removetransition" method....
456 def transition(cls
, tostate
, condition
=None):
458 this will create a transition from the current state to the tostate.
460 transitions
= cls
._transitions
461 if transitions
== None:
462 transitions
= cls
._transitions
= {tostate
: condition
}
463 ## elif tostate in transitions:
464 ## raise TransitionError, 'a transition from %s to %s already exists.' % (cls, tostate)
466 cls
._transitions
[tostate
] = condition
467 transition
= classmethod(transition
)
468 def changestate(self
, tostate
):
470 change the object stete
472 # prevent moving out of a terminal state...
473 if hasattr(self
, '__is_terminal_state__') and self
.__is
_terminal
_state
__:
474 raise FiniteStateMachineError
, 'can\'t change state of a terminal FSM node %s.' % self
476 # check for transitions...
477 if hasattr(fsm
, '__strict_transitions__') and fsm
.__strict
_transitions
__ and \
478 (not hasattr(self
, '_transitions') or self
._transitions
== None or \
479 tostate
not in self
._transitions
):
480 raise TransitionError
, 'can\'t change state of %s to state %s without a transition.' % (self
, tostate
)
482 transitions
= self
._transitions
483 if transitions
[tostate
] != None and not transitions
[tostate
](self
):
484 raise TransitionError
, 'conditional transition from %s to state %s failed.' % (self
, tostate
)
485 super(State
, self
).changestate(tostate
)
486 def iternextstates(self
):
488 this will iterate through the states directly reachable from
489 self (e.g. to which there are direct transitions).
491 if self
._transitions
== None:
493 for n
in self
._transitions
:
495 iternextstates
= classmethod(iternextstates
)
496 # this method is called after the state is finalized (e.g. after the
497 # __onstatechange__ is called and the onEnter event is processed).
498 def __onafterstatechange__(self
):
500 this will try to next change state.
502 if hasattr(self
, '__auto_change_state__') and self
.__auto
_change
_state
__:
503 if self
._transitions
!= None:
504 for tostate
in self
._transitions
:
506 self
.changestate(tostate
)
511 if not hasattr(self
, '__is_terminal_state__') or not self
.__is
_terminal
_state
__:
512 if hasattr(self
, '__resolvestatechange__'):
513 # try to save the day and call the resolve method...
514 return self
.__resolvestatechange
__()
515 raise FiniteStateMachineError
, 'can\'t exit a non-terminal state %s.' % self
516 # this is here for documentation...
517 ## def __resolvestatechange__(self):
519 ## this is called if no transition from current state is found, to
520 ## resolve the situation.
523 # Q: does this need to be __gatattr__ or __getattribute__ ????
524 # NOTE: this might make the attr access quite slow...
525 # Q: is there a faster way??? (via MRO manipulation or something...)
526 def __getattr__(self
, name
):
528 this will proxy the attr access to the original startup class....
530 ##!!! check for looping searching !!!##
531 # get the name in the startup class...
533 return getattr(self
.__startup
_class
__, name
)
534 except AttributeError:
535 raise AttributeError, '%s object has no attribute "%s"' % (self
, name
)
538 #--------------------------------------------------------InitialState---
539 class InitialState(State
):
542 __is_initial_state__
= True
543 __ignore_registration__
= True
546 #-------------------------------------------------------TerminalState---
547 class TerminalState(State
):
550 __is_terminal_state__
= True
551 __ignore_registration__
= True
555 #=======================================================================
556 # vim:set ts=4 sw=4 nowrap :