Created
December 13, 2013 19:51
-
-
Save ahawker/7950216 to your computer and use it in GitHub Desktop.
A basic "state machine" built using python descriptors.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
NO_OP = lambda *args, **kwargs: True | |
def iterable(item): | |
""" | |
Return an iterable which contains the given item. | |
""" | |
if isinstance(item, collections.Iterable) and not isinstance(item, basestring): | |
return item | |
return (item,) if item is not None else () | |
class InvalidStateMachineError(Exception): | |
""" | |
""" | |
class Transition(object): | |
""" | |
Transition is a descriptor which allows you to decorate multiple functions to compose a transition. Transitions | |
are composed of up to four separate functions: | |
condition -> Callable to return a bool which denotes if the transition should be executed. | |
enter -> Callable to be executed before the action. | |
action -> The meat of the transition. | |
exit -> Callable to be executed after the action. | |
Example: | |
class LightSwitch(object): | |
state = 'off' | |
@transition('off', 'on') | |
def on(self): | |
print 'Let there be light!' | |
@on.on_enter | |
def beg(self): | |
print 'Please be kind; I am afraid of the dark.' | |
@transition('on', 'off') | |
def off(self): | |
print 'The darkness engulfs you.' | |
@off.on_exit: | |
def at_ease(self): | |
print 'Thank you! I hate the dark!' | |
""" | |
def __init__(self, current_state, next_state, action=None, condition=None, enter=None, exit=None): | |
self.current_state = current_state | |
self.next_state = next_state | |
self.action = action | |
self.condition = condition or NO_OP | |
self.enter = enter or NO_OP | |
self.exit = exit or NO_OP | |
def __call__(self, func, *args, **kwargs): | |
self.action = func | |
return self | |
def __get__(self, instance, owner=None): | |
if instance is None: | |
return self | |
return self.transition(instance) | |
def guard(self, func): | |
self.condition = func | |
return self | |
def on_enter(self, func): | |
self.enter = func | |
return self | |
def on_exit(self, func): | |
self.exit = func | |
return self | |
def transition(self, state_machine): | |
""" | |
Returns a function which executes the entire flow of a transition. | |
:param state_machine: Instance of the object whose functions are decorated with @transition. | |
""" | |
def decorator(*args, **kwargs): | |
""" | |
Decorator which encapsulates execution of a single transition. | |
""" | |
# Classes which use the @transition decorator must have an attribute | |
# named 'state' or 'initial_state' in order to be considered a StateMachine. | |
state = getattr(state_machine, 'state', getattr(state_machine, 'initial_state', None)) | |
if state is None: | |
raise InvalidStateMachineError("Instance {0} must implement 'state' or 'initial_state' attribute") | |
# Ignore transitions which cannot execute within the current state. | |
if not state in iterable(self.current_state): | |
return state | |
# Ignore transitions whose guard conditions fail to return True. | |
if not bool(self.condition(state_machine, *args, **kwargs)): | |
return state | |
# Perform a state transition. | |
self.enter(state_machine, *args, **kwargs) | |
self.action(state_machine, *args, **kwargs) | |
self.exit(state_machine, *args, **kwargs) | |
# Return the next_state as defined by the transition we just executed. | |
state = state_machine.state = self.next_state | |
return state | |
return decorator | |
transition = Transition |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment