A Python library for easily implementing state machines with complex conditionals
Project description
DynamicStateMachine
NOTE: This is in beta version. It only has basic tests implemented.
Table of Contents
Description
This project lets you easily implement, run, and graph a state machine that has complex conditionals. There's other projects similar to this, and honestly, for most things, they're probably better. The ones I looked at (and took inspiration from) are:
These are both very good, much more mature projects. I recommend looking at them before turning to this project. What they don't do, however (as far as I could tell), is let you easily decide what state to go to next based on parameters given to the advancement function. It's possible in at least one of them (I don't remember which), but it was clunky, frustating, and less elegant that what I was already doing, which was piles of match and if statements.
This library lets you define a bunch of states (with any type of value) and decide which state to go to next, either by always going to a given state, or by calling a transition method which returns the state to go to (or another transition method) based on given input. But that all sounds complicated, here's an example:
Example
You start by putting all your states in a single class which inherits from States. Eventually, I'll allow defining states other ways, like via dict, Enum or even list, but this is all it takes for now.
The States class will automatically convert all of the values into State's, so when you reference ExampleStates.a later, it will be of type State, not of type str.
class ExampleStates(States):
a = 'this is a'
b = 'this is b'
c = 'this is c'
# A virtual State
pre_c = None
Virtual states are states that immediately go to the next state, without waiting for a call to next to advance it. This can be helpful for sectioning off parts of your program, reusing logic, or just make the generated graph look nicer. If you want to use None as a state, you can change the value which defines a virtual state by setting virtual_value in the class definition, like this:
class BoolStates(States, virtual_value=...):
t = True
f = False
n = None
x = ...
Next, you define the machine. This inherits from DynamicStateMachine, and defines all the side effects of the steps:
class ExampleMachine(DynamicStateMachine):
def before_a(self):
print('Now in state a...', end=' ')
def after_a(self):
print('done')
def before_c(self):
print('Now in state c!')
def before_pre_c(self):
print('Now in state pre_c, but not for long...', end=' ')
def after_pre_c(self):
print('done')
As you can see, before/after methods are optional, they'll only get called if available.
The machine class also defines the side effects of starting and stopping the machine by overriding these methods:
def on_start(self):
print('Starting...')
def on_end(self):
print('Finished!')
The machine class also defines the transition methods to decide what state to go to next. Transition methods can have side effects as well, but they must return either a State, a reference to another transition method, which will then get called to decide where to go, or None, to indicate the end of the State Machine. This lets you chain complex logic together in a modular way.
Transition methods can also return a 2nd value, see Graphing below.
def do_the_thing(self, decider=True):
# Transition methods can have side effects
print('Deciding...')
if decider:
print('Decided on a')
return ExampleStates.a, 'if decider is True'
else:
print('Decided on c')
return ExampleStates.pre_c
def decide_if_done(self, done=False):
print('Deciding if done...')
if done:
print('Decided we\'re done')
return None, 'Im done talking to you now.'
else:
print('Decided we\'re not done')
return ExampleStates.a, 'no keep going!'
Lastly, it defines the initial state, the States class we're using, and the transitions that actually build the state machine:
states = ExampleStates
initial = ExampleStates.a
transitions = (
# Can be either simple transitions, like so
ExampleStates.a >> ExampleStates.b,
# ...or transition methods that determine the next state
ExampleStates.b >> do_the_thing,
ExampleStates.pre_c >> ExampleStates.c,
# Transition methods can return a state, another transition method, or None
ExampleStates.c >> decide_if_done,
)
The transitions are defined by using the >> syntax between a State, and either another State, a transition method which defines where to go to next, or None, just like the returns of the transition methods. Note that you can also use lambdas in place of full methods, with or without the self parameter.
Advacing the machine is done by the next method. Any parameters passed to it are fed into the transition methods, as well as the before/after methods, if they can handle them. No errors are raised if they can't, it just won't pass the invalid parameters. Similar to the other projects.
if __name__ == "__main__":
# Initial state is a
m = ExampleMachine()
# Starting...
m.next() # a -> b
m.next(False) # b -> c
m.next(False) # c -> a
# You can also use the built-in next function
next(m) # a -> b
m.next(True) # b -> a
next(m) # a -> b
m.next(False) # b -> c
m.next(True) # c -> None
# Finished!
Graphing
Like the other libraries mentioned above, DynamicStateMachine can auto-generate it's own graphical representation. It uses GraphViz for this. The above example machine looks like this:
The styles of all the different types of nodes can be customized in the DynamicStateMachine.construct_graphviz() function, see the doc string for more details. The names come from either the names or the values (depending on the parameters passed) of the States, the transition names come from the name of the methods, and the edge names come from the returns of the transitions. The construct_graphviz() method parses all the transition methods for return statements, and connects them that way to the nodes they go to. If a string is additionally returned by a transition method (i.e. return ExampleStates.a, "some explanation"), the latter is ignored entirely when running, but is parsed by construct_graphviz() and added as the edge text.
History
I made this project after writing a helper program to help me at my job. I had a series of steps, all very conditional on the input I gave it, and all very conditional on other parameters. I was using match and if statements, which worked surprisingly well, but once it got up to 700+ lines, it became hard to maintain. Auto-generating the graph helped debug, implement, and show my boss how it worked.
Installation
pip install DynamicStateMachine
The import name is the same as the package name:
from DynamicStateMachine import DynamicStateMachine
License
DynamicStateMachine is distributed under the terms of the MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dynamicstatemachine-0.0.3.tar.gz.
File metadata
- Download URL: dynamicstatemachine-0.0.3.tar.gz
- Upload date:
- Size: 50.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fa3be3447cae96e34ad3015a516a47ccb52264919a776d38c889758ae89308b
|
|
| MD5 |
c7dd59f6ce9920853eb68a5024f5bdfb
|
|
| BLAKE2b-256 |
48aa1faf84edb320c4ed38598760f068060422c449471710c6833cf55d8c18b2
|
File details
Details for the file dynamicstatemachine-0.0.3-py3-none-any.whl.
File metadata
- Download URL: dynamicstatemachine-0.0.3-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8525c7a17d1c0ae748f456e4863c8e70487c16d90660e055654120c94c266387
|
|
| MD5 |
4e14f4953da3bb4bf407fbd4bcde61bd
|
|
| BLAKE2b-256 |
6bcb9c4db07205aeadd0eef9d1a63ff835401a05fbc83a11641502e9985800a9
|