StateEngine
Finite state machines in Python
Example Usage
"""
Using StateEngine to simulate the change of physical states of water
"""
from StateEngine import StateEngine
water_states = StateEngine()
@water_states.state_handler("solid")
def ice(sm_input):
if sm_input == "melting":
print("turning to liquid")
return "liquid"
elif sm_input == "sublimation":
print("turning to gas")
return "gas"
else:
print("remaining as a solid")
return "solid"
@water_states.state_handler("liquid", default=True)
def water(sm_input):
if sm_input == "freezing":
print("turning to solid")
return "solid"
elif sm_input == "boiling":
print("turning to gas")
return "gas"
else:
print("remaining as a liquid")
return "liquid"
@water_states.state_handler("gas")
def vapour(sm_input):
if sm_input == "condensing":
print("turning to liquid")
return "liquid"
elif sm_input == "depositing":
print("turning to solid")
return "solid"
else:
print("remaining as a gas")
return "gas"
if __name__ == "__main__":
state = None
while True:
user_input = input("> ")
state = water_states.execute(state, user_input)
Installation
The stateengine package is not on PyPi yet. To use stateengine, clone the repository and put it in the working directory of your project:
git clone https://github.com/aymanimtyaz/stateengine.git
Usage
Creating a state machine
A state machine can be created by creating a state machine object. The constructor does not take any arguments:
from stateengine import StateEngine
state_machine = StateEngine()
...
Assigning states to state handlers
States can be assigned to state handlers using the state_handler()
decorator function as:
...
@state_machine.state_handler(state="example_state")
def example_state_handler(input):
...
A default state can be assigned by passing default=True
as an argument to state_handler
. Note that there can only be one default state.
...
@state_machine.state_handler(state="example_state", default=True)
def example_state_handler(input):
...
The state handler function should only return other states that have been registered with a state handler, signifying a state transition.
Assigning multiple states to a single state handler
Multiple states can also be assigned to a handler. This can be done by stacking the state_handler
decorator:
...
@state_machine.state_handler("state_1")
@state_machine.state_handler("state_2")
@state_machine.state_handler("state_3")
def a_handler_function(input):
...
return ...
...
The order in which the decorators for each state are stacked does not matter.
Running the state machine
The state machine can be executed by passing it a state and an input. It should be run after all the state handlers have been defined. A state machine can be run using execute()
as:
...
some_state = ...
some_input_1 = ...; some_input_2 = ...; ...
new_state = state_machine.execute(
state=some_state, some_input_1=some_input_1, some_input_2=some_input_2, ...)
...
A new state will be returned depending on the input and the logic defined in the corresponding handler. A practical way to make use of the state machine would be to run it in a loop, or as a response to an event such as an HTTP request
state_handler
property
Accessing the current state using the current_state
is a property that can be used to access the current state from within a handler function. This may seem pointless at first glance, but becomes really useful when a handler is assigned to more than one state:
...
@state_machine.state_handler("state_1")
@state_machine.state_handler("state_2")
def a_handler_function(input):
print(f"The current state is {state_machine.current_state}")
return ...
...
None
states
A note on The default handler (if defined) for a state machine can be executed by passing the corresponding state value OR None
as the state
argument to execute()
. If a default state handler is not defined and a None
is passed, an exception will be raised.
Important points
- A handler function should only return states. Furthermore, it should only return states that are registered to a state handler. Returning an unregistered state will raise a
NoHandlerAssociation
exception. - All handlers for a state machine should have the same type of input (number of inputs)
- The states can only be of types
str
andint
. AnInvalidStateType
exception will be raised otherwise. - States must be unique. Two state handlers can not have the same state argument. A
StateHandlerClash
exception will be raised otherwise. - Only one default state can exist for a state machine. Trying to assign more than one default state handler will raise a
DefaultStateHandlerClash
exception. - A default state is not necessary. However, if
state=None
is passed toexecute
, and a default state handler is not defined. ANoDefaultState
exception will be raised. - The
current_state
property can only be accessed from within a handler context, that is, inside a handler function. Trying to access it from outside a handler function will raise aOutsideHandlerContext
exception.
Good practices
- The states' names should reflect what their handlers are supposed to do. This will make it easy to maintain and debug the state machine code in the future.
To Do
- Make unpacking inputs to state handlers more Pythonic.
- Allow a handler to handle multiple states.
- Improve API documentation.
- Add use cases in the docs.