|
|
|
Automat
|
|
=======
|
|
|
|
|
|
.. image:: https://readthedocs.org/projects/automat/badge/?version=stable
|
|
:target: http://automat.readthedocs.io/en/latest/
|
|
:alt: Documentation Status
|
|
|
|
|
|
.. image:: https://travis-ci.org/glyph/automat.svg?branch=master
|
|
:target: https://travis-ci.org/glyph/automat
|
|
:alt: Build Status
|
|
|
|
|
|
.. image:: https://coveralls.io/repos/glyph/automat/badge.png
|
|
:target: https://coveralls.io/r/glyph/automat
|
|
:alt: Coverage Status
|
|
|
|
|
|
Self-service finite-state machines for the programmer on the go.
|
|
----------------------------------------------------------------
|
|
|
|
Automat is a library for concise, idiomatic Python expression of finite-state
|
|
automata (particularly deterministic finite-state transducers).
|
|
|
|
Read more here, or on `Read the Docs <https://automat.readthedocs.io/>`_\ , or watch the following videos for an overview and presentation
|
|
|
|
Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017:
|
|
|
|
.. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg
|
|
:target: https://www.youtube.com/watch?v=0wOZBpD1VVk
|
|
:alt: Glyph Lefkowitz - Automat - Pyninsula #0
|
|
|
|
|
|
Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017:
|
|
|
|
.. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg
|
|
:target: https://www.youtube.com/watch?v=TedUKXhu9kE
|
|
:alt: Clinton Roy - State Machines - Pycon Australia 2017
|
|
|
|
|
|
Why use state machines?
|
|
^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Sometimes you have to create an object whose behavior varies with its state,
|
|
but still wishes to present a consistent interface to its callers.
|
|
|
|
For example, let's say you're writing the software for a coffee machine. It
|
|
has a lid that can be opened or closed, a chamber for water, a chamber for
|
|
coffee beans, and a button for "brew".
|
|
|
|
There are a number of possible states for the coffee machine. It might or
|
|
might not have water. It might or might not have beans. The lid might be open
|
|
or closed. The "brew" button should only actually attempt to brew coffee in
|
|
one of these configurations, and the "open lid" button should only work if the
|
|
coffee is not, in fact, brewing.
|
|
|
|
With diligence and attention to detail, you can implement this correctly using
|
|
a collection of attributes on an object; ``has_water``\ , ``has_beans``\ ,
|
|
``is_lid_open`` and so on. However, you have to keep all these attributes
|
|
consistent. As the coffee maker becomes more complex - perhaps you add an
|
|
additional chamber for flavorings so you can make hazelnut coffee, for
|
|
example - you have to keep adding more and more checks and more and more
|
|
reasoning about which combinations of states are allowed.
|
|
|
|
Rather than adding tedious 'if' checks to every single method to make sure that
|
|
each of these flags are exactly what you expect, you can use a state machine to
|
|
ensure that if your code runs at all, it will be run with all the required
|
|
values initialized, because they have to be called in the order you declare
|
|
them.
|
|
|
|
You can read about state machines and their advantages for Python programmers
|
|
in considerably more detail
|
|
`in this excellent series of articles from ClusterHQ <https://clusterhq.com/blog/what-is-a-state-machine/>`_.
|
|
|
|
What makes Automat different?
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
There are
|
|
`dozens of libraries on PyPI implementing state machines <https://pypi.org/search/?q=finite+state+machine>`_.
|
|
So it behooves me to say why yet another one would be a good idea.
|
|
|
|
Automat is designed around this principle: while organizing your code around
|
|
state machines is a good idea, your callers don't, and shouldn't have to, care
|
|
that you've done so. In Python, the "input" to a stateful system is a method
|
|
call; the "output" may be a method call, if you need to invoke a side effect,
|
|
or a return value, if you are just performing a computation in memory. Most
|
|
other state-machine libraries require you to explicitly create an input object,
|
|
provide that object to a generic "input" method, and then receive results,
|
|
sometimes in terms of that library's interfaces and sometimes in terms of
|
|
classes you define yourself.
|
|
|
|
For example, a snippet of the coffee-machine example above might be implemented
|
|
as follows in naive Python:
|
|
|
|
.. code-block:: python
|
|
|
|
class CoffeeMachine(object):
|
|
def brew_button(self):
|
|
if self.has_water and self.has_beans and not self.is_lid_open:
|
|
self.heat_the_heating_element()
|
|
# ...
|
|
|
|
With Automat, you'd create a class with a ``MethodicalMachine`` attribute:
|
|
|
|
.. code-block:: python
|
|
|
|
from automat import MethodicalMachine
|
|
|
|
class CoffeeBrewer(object):
|
|
_machine = MethodicalMachine()
|
|
|
|
and then you would break the above logic into two pieces - the ``brew_button``
|
|
*input*\ , declared like so:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.input()
|
|
def brew_button(self):
|
|
"The user pressed the 'brew' button."
|
|
|
|
It wouldn't do any good to declare a method *body* on this, however, because
|
|
input methods don't actually execute their bodies when called; doing actual
|
|
work is the *output*\ 's job:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.output()
|
|
def _heat_the_heating_element(self):
|
|
"Heat up the heating element, which should cause coffee to happen."
|
|
self._heating_element.turn_on()
|
|
|
|
As well as a couple of *states* - and for simplicity's sake let's say that the
|
|
only two states are ``have_beans`` and ``dont_have_beans``\ :
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.state()
|
|
def have_beans(self):
|
|
"In this state, you have some beans."
|
|
@_machine.state(initial=True)
|
|
def dont_have_beans(self):
|
|
"In this state, you don't have any beans."
|
|
|
|
``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans
|
|
in it.
|
|
|
|
(And another input to put some beans in:)
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.input()
|
|
def put_in_beans(self):
|
|
"The user put in some beans."
|
|
|
|
Finally, you hook everything together with the ``upon`` method of the functions
|
|
decorated with ``_machine.state``\ :
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
# When we don't have beans, upon putting in beans, we will then have beans
|
|
# (and produce no output)
|
|
dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[])
|
|
|
|
# When we have beans, upon pressing the brew button, we will then not have
|
|
# beans any more (as they have been entered into the brewing chamber) and
|
|
# our output will be heating the heating element.
|
|
have_beans.upon(brew_button, enter=dont_have_beans,
|
|
outputs=[_heat_the_heating_element])
|
|
|
|
To *users* of this coffee machine class though, it still looks like a POPO
|
|
(Plain Old Python Object):
|
|
|
|
.. code-block:: python
|
|
|
|
>>> coffee_machine = CoffeeMachine()
|
|
>>> coffee_machine.put_in_beans()
|
|
>>> coffee_machine.brew_button()
|
|
|
|
All of the *inputs* are provided by calling them like methods, all of the
|
|
*outputs* are automatically invoked when they are produced according to the
|
|
outputs specified to ``upon`` and all of the states are simply opaque tokens -
|
|
although the fact that they're defined as methods like inputs and outputs
|
|
allows you to put docstrings on them easily to document them.
|
|
|
|
How do I get the current state of a state machine?
|
|
--------------------------------------------------
|
|
|
|
Don't do that.
|
|
|
|
One major reason for having a state machine is that you want the callers of the
|
|
state machine to just provide the appropriate input to the machine at the
|
|
appropriate time, and *not have to check themselves* what state the machine is
|
|
in. So if you are tempted to write some code like this:
|
|
|
|
.. code-block:: python
|
|
|
|
if connection_state_machine.state == "CONNECTED":
|
|
connection_state_machine.send_message()
|
|
else:
|
|
print("not connected")
|
|
|
|
Instead, just make your calling code do this:
|
|
|
|
.. code-block:: python
|
|
|
|
connection_state_machine.send_message()
|
|
|
|
and then change your state machine to look like this:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.state()
|
|
def connected(self):
|
|
"connected"
|
|
@_machine.state()
|
|
def not_connected(self):
|
|
"not connected"
|
|
@_machine.input()
|
|
def send_message(self):
|
|
"send a message"
|
|
@_machine.output()
|
|
def _actually_send_message(self):
|
|
self._transport.send(b"message")
|
|
@_machine.output()
|
|
def _report_sending_failure(self):
|
|
print("not connected")
|
|
connected.upon(send_message, enter=connected, [_actually_send_message])
|
|
not_connected.upon(send_message, enter=not_connected, [_report_sending_failure])
|
|
|
|
so that the responsibility for knowing which state the state machine is in
|
|
remains within the state machine itself.
|
|
|
|
Input for Inputs and Output for Outputs
|
|
---------------------------------------
|
|
|
|
Quite often you want to be able to pass parameters to your methods, as well as
|
|
inspecting their results. For example, when you brew the coffee, you might
|
|
expect a cup of coffee to result, and you would like to see what kind of coffee
|
|
it is. And if you were to put delicious hand-roasted small-batch artisanal
|
|
beans into the machine, you would expect a *better* cup of coffee than if you
|
|
were to use mass-produced beans. You would do this in plain old Python by
|
|
adding a parameter, so that's how you do it in Automat as well.
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.input()
|
|
def put_in_beans(self, beans):
|
|
"The user put in some beans."
|
|
|
|
However, one important difference here is that *we can't add any
|
|
implementation code to the input method*. Inputs are purely a declaration of
|
|
the interface; the behavior must all come from outputs. Therefore, the change
|
|
in the state of the coffee machine must be represented as an output. We can
|
|
add an output method like this:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.output()
|
|
def _save_beans(self, beans):
|
|
"The beans are now in the machine; save them."
|
|
self._beans = beans
|
|
|
|
and then connect it to the ``put_in_beans`` by changing the transition from
|
|
``dont_have_beans`` to ``have_beans`` like so:
|
|
|
|
.. code-block:: python
|
|
|
|
dont_have_beans.upon(put_in_beans, enter=have_beans,
|
|
outputs=[_save_beans])
|
|
|
|
Now, when you call:
|
|
|
|
.. code-block:: python
|
|
|
|
coffee_machine.put_in_beans("real good beans")
|
|
|
|
the machine will remember the beans for later.
|
|
|
|
So how do we get the beans back out again? One of our outputs needs to have a
|
|
return value. It would make sense if our ``brew_button`` method returned the cup
|
|
of coffee that it made, so we should add an output. So, in addition to heating
|
|
the heating element, let's add a return value that describes the coffee. First
|
|
a new output:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.output()
|
|
def _describe_coffee(self):
|
|
return "A cup of coffee made with {}.".format(self._beans)
|
|
|
|
Note that we don't need to check first whether ``self._beans`` exists or not,
|
|
because we can only reach this output method if the state machine says we've
|
|
gone through a set of states that sets this attribute.
|
|
|
|
Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change
|
|
the brewing transition to:
|
|
|
|
.. code-block:: python
|
|
|
|
have_beans.upon(brew_button, enter=dont_have_beans,
|
|
outputs=[_heat_the_heating_element,
|
|
_describe_coffee])
|
|
|
|
Now, we can call it:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> coffee_machine.brew_button()
|
|
[None, 'A cup of coffee made with real good beans.']
|
|
|
|
Except... wait a second, what's that ``None`` doing there?
|
|
|
|
Since every input can produce multiple outputs, in automat, the default return
|
|
value from every input invocation is a ``list``. In this case, we have both
|
|
``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing
|
|
both of their return values. However, this can be customized, with the
|
|
``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an
|
|
iterable of all the outputs' return values and "collects" a single return value
|
|
to return to the caller of the state machine.
|
|
|
|
In this case, we only care about the last output, so we can adjust the call to
|
|
``upon`` like this:
|
|
|
|
.. code-block:: python
|
|
|
|
have_beans.upon(brew_button, enter=dont_have_beans,
|
|
outputs=[_heat_the_heating_element,
|
|
_describe_coffee],
|
|
collector=lambda iterable: list(iterable)[-1]
|
|
)
|
|
|
|
And now, we'll get just the return value we want:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> coffee_machine.brew_button()
|
|
'A cup of coffee made with real good beans.'
|
|
|
|
If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...)
|
|
--------------------------------------------------------------------------------------------------------------------
|
|
|
|
There are APIs for serializing the state machine.
|
|
|
|
First, you have to decide on a persistent representation of each state, via the
|
|
``serialized=`` argument to the ``MethodicalMachine.state()`` decorator.
|
|
|
|
Let's take this very simple "light switch" state machine, which can be on or
|
|
off, and flipped to reverse its state:
|
|
|
|
.. code-block:: python
|
|
|
|
class LightSwitch(object):
|
|
_machine = MethodicalMachine()
|
|
@_machine.state(serialized="on")
|
|
def on_state(self):
|
|
"the switch is on"
|
|
@_machine.state(serialized="off", initial=True)
|
|
def off_state(self):
|
|
"the switch is off"
|
|
@_machine.input()
|
|
def flip(self):
|
|
"flip the switch"
|
|
on_state.upon(flip, enter=off_state, outputs=[])
|
|
off_state.upon(flip, enter=on_state, outputs=[])
|
|
|
|
In this case, we've chosen a serialized representation for each state via the
|
|
``serialized`` argument. The on state is represented by the string ``"on"``\ , and
|
|
the off state is represented by the string ``"off"``.
|
|
|
|
Now, let's just add an input that lets us tell if the switch is on or not.
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.input()
|
|
def query_power(self):
|
|
"return True if powered, False otherwise"
|
|
@_machine.output()
|
|
def _is_powered(self):
|
|
return True
|
|
@_machine.output()
|
|
def _not_powered(self):
|
|
return False
|
|
on_state.upon(query_power, enter=on_state, outputs=[_is_powered],
|
|
collector=next)
|
|
off_state.upon(query_power, enter=off_state, outputs=[_not_powered],
|
|
collector=next)
|
|
|
|
To save the state, we have the ``MethodicalMachine.serializer()`` method. A
|
|
method decorated with ``@serializer()`` gets an extra argument injected at the
|
|
beginning of its argument list: the serialized identifier for the state. In
|
|
this case, either ``"on"`` or ``"off"``. Since state machine output methods can
|
|
also affect other state on the object, a serializer method is expected to
|
|
return *all* relevant state for serialization.
|
|
|
|
For our simple light switch, such a method might look like this:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.serializer()
|
|
def save(self, state):
|
|
return {"is-it-on": state}
|
|
|
|
Serializers can be public methods, and they can return whatever you like. If
|
|
necessary, you can have different serializers - just multiple methods decorated
|
|
with ``@_machine.serializer()`` - for different formats; return one data-structure
|
|
for JSON, one for XML, one for a database row, and so on.
|
|
|
|
When it comes time to unserialize, though, you generally want a private method,
|
|
because an unserializer has to take a not-fully-initialized instance and
|
|
populate it with state. It is expected to *return* the serialized machine
|
|
state token that was passed to the serializer, but it can take whatever
|
|
arguments you like. Of course, in order to return that, it probably has to
|
|
take it somewhere in its arguments, so it will generally take whatever a paired
|
|
serializer has returned as an argument.
|
|
|
|
So our unserializer would look like this:
|
|
|
|
.. code-block:: python
|
|
|
|
@_machine.unserializer()
|
|
def _restore(self, blob):
|
|
return blob["is-it-on"]
|
|
|
|
Generally you will want a classmethod deserialization constructor which you
|
|
write yourself to call this, so that you know how to create an instance of your
|
|
own object, like so:
|
|
|
|
.. code-block:: python
|
|
|
|
@classmethod
|
|
def from_blob(cls, blob):
|
|
self = cls()
|
|
self._restore(blob)
|
|
return self
|
|
|
|
Saving and loading our ``LightSwitch`` along with its state-machine state can now
|
|
be accomplished as follows:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> switch1 = LightSwitch()
|
|
>>> switch1.query_power()
|
|
False
|
|
>>> switch1.flip()
|
|
[]
|
|
>>> switch1.query_power()
|
|
True
|
|
>>> blob = switch1.save()
|
|
>>> switch2 = LightSwitch.from_blob(blob)
|
|
>>> switch2.query_power()
|
|
True
|
|
|
|
More comprehensive (tested, working) examples are present in ``docs/examples``.
|
|
|
|
Go forth and machine all the state!
|
|
|
|
|