Skip to main content

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.

PyPI - Version PyPI - Python Version

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:

ExampleMachine

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dynamicstatemachine-0.0.2.tar.gz (50.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dynamicstatemachine-0.0.2-py3-none-any.whl (13.6 kB view details)

Uploaded Python 3

File details

Details for the file dynamicstatemachine-0.0.2.tar.gz.

File metadata

  • Download URL: dynamicstatemachine-0.0.2.tar.gz
  • Upload date:
  • Size: 50.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-httpx/0.28.1

File hashes

Hashes for dynamicstatemachine-0.0.2.tar.gz
Algorithm Hash digest
SHA256 a3abe5bfda55490a0f3f7751b9c7e739aa2b209ddfd112a87030f5fe7d97822f
MD5 28f07975c3fec7b388e4aae757c8d96f
BLAKE2b-256 b3f17cf812a09c812c9d08252104165a8a520e973bf3dfd79b96f03b62cb0c29

See more details on using hashes here.

File details

Details for the file dynamicstatemachine-0.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for dynamicstatemachine-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 06d47d6cb7f5cfd2fd6013a26a45d39201f4ec9f648677dcec335920752050c8
MD5 644a5a071dacdc99a663285961031953
BLAKE2b-256 eb361ce2317a564abe89710579768f355832310a7fa12791652d3fde8c857910

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page