Package: python-stateengine
StateEngineFinite state machines in PythonExample Usage"""Using StateEngine to simulate the change of physical states of water"""fromStateEngineimportStateEnginewater_states=StateEngine()@water_states.state_handler("solid")defice(sm_input):ifsm_input=="melting":print("turning to liquid")return"liquid"elifsm_input=="sublimation":print("turning to gas")return"gas"else:print("remaining as a solid")return"solid"@water_states.state_handler("liquid",default=True)defwater(sm_input):ifsm_input=="freezing":print("turning to solid")return"solid"elifsm_input=="boiling":print("turning to gas")return"gas"else:print("remaining as a liquid")return"liquid"@water_states.state_handler("gas")defvapour(sm_input):ifsm_input=="condensing":print("turning to liquid")return"liquid"elifsm_input=="depositing":print("turning to solid")return"solid"else:print("remaining as a gas")return"gas"if__name__=="__main__":state=NonewhileTrue:user_input=input("> ")state=water_states.execute(state,user_input)InstallationThe 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.gitUsageCreating a state machineA state machine can be created by creating a state machine object. The constructor does not take any arguments:fromstateengineimportStateEnginestate_machine=StateEngine()...Assigning states to state handlersStates can be assigned to state handlers using thestate_handler()decorator function as:...@state_machine.state_handler(state="example_state")defexample_state_handler(input):...A default state can be assigned by passingdefault=Trueas an argument tostate_handler. Note that there can only be one default state....@state_machine.state_handler(state="example_state",default=True)defexample_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 handlerMultiple states can also be assigned to a handler. This can be done by stacking thestate_handlerdecorator:...@state_machine.state_handler("state_1")@state_machine.state_handler("state_2")@state_machine.state_handler("state_3")defa_handler_function(input):...return......The order in which the decorators for each state are stacked does not matter.Running the state machineThe 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 usingexecute()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 requestAccessing the current state using thestate_handlerpropertycurrent_stateis 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")defa_handler_function(input):print(f"The current state is{state_machine.current_state}")return......A note onNonestatesThe default handler (if defined) for a state machine can be executed by passing the corresponding state value ORNoneas thestateargument toexecute(). If a default state handler is not defined and aNoneis passed, an exception will be raised.Important pointsA 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 aNoHandlerAssociationexception.All handlers for a state machine should have the same type of input (number of inputs)The states can only be of typesstrandint. AnInvalidStateTypeexception will be raised otherwise.States must be unique. Two state handlers can not have the same state argument. AStateHandlerClashexception 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 aDefaultStateHandlerClashexception.A default state is not necessary. However, ifstate=Noneis passed toexecute, and a default state handler is not defined. ANoDefaultStateexception will be raised.Thecurrent_stateproperty 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 aOutsideHandlerContextexception.Good practicesThe 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 DoMake unpacking inputs to state handlers more Pythonic.Allow a handler to handle multiple states.Improve API documentation.Add use cases in the docs.Examples of StateEngine in usespndrIf you have any questions or suggestions about StateEngine, you can open up an issue or create a PR if you've made some improvements . You can also email me ataymanimtyaz@gmail.com:)