Automat-20.2.0/ 0000755 0000765 0000024 00000000000 13622314660 013456 5 ustar glyph staff 0000000 0000000 Automat-20.2.0/benchmark/ 0000755 0000765 0000024 00000000000 13622314660 015410 5 ustar glyph staff 0000000 0000000 Automat-20.2.0/benchmark/test_transitions.py 0000644 0000765 0000024 00000001251 13225625561 021401 0 ustar glyph staff 0000000 0000000 # https://github.com/glyph/automat/issues/60
import automat
class Simple(object):
"""
"""
_m = automat.MethodicalMachine()
@_m.input()
def one(self, data):
"some input data"
@_m.state(initial=True)
def waiting(self):
"patiently"
@_m.output()
def boom(self, data):
pass
waiting.upon(
one,
enter=waiting,
outputs=[boom],
)
def simple_one(machine, data):
machine.one(data)
def test_simple_machine_transitions(benchmark):
benchmark(simple_one, Simple(), 0)
Automat-20.2.0/PKG-INFO 0000644 0000765 0000024 00000051670 13622314660 014564 0 ustar glyph staff 0000000 0000000 Metadata-Version: 2.1
Name: Automat
Version: 20.2.0
Summary: Self-service finite-state machines for the programmer on the go.
Home-page: https://github.com/glyph/Automat
Author: Glyph
Author-email: glyph@twistedmatrix.com
License: MIT
Description:
Automat
=======
.. image:: https://readthedocs.org/projects/automat/badge/?version=latest
: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 `_\ , 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 `_.
What makes Automat different?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There are
`dozens of libraries on PyPI implementing state machines `_.
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!
Keywords: fsm finite state machine automata
Platform: UNKNOWN
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Provides-Extra: visualize
Automat-20.2.0/LICENSE 0000644 0000765 0000024 00000002035 13225625561 014467 0 ustar glyph staff 0000000 0000000 Copyright (c) 2014
Rackspace
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Automat-20.2.0/docs/ 0000755 0000765 0000024 00000000000 13622314660 014406 5 ustar glyph staff 0000000 0000000 Automat-20.2.0/docs/index.rst 0000644 0000765 0000024 00000004324 13305150226 016244 0 ustar glyph staff 0000000 0000000 =========================================================================
Automat: Self-service finite-state machines for the programmer on the go.
=========================================================================
.. image:: https://upload.wikimedia.org/wikipedia/commons/d/db/Automat.jpg
:width: 250
:align: right
Automat is a library for concise, idiomatic Python expression
of finite-state automata
(particularly deterministic finite-state transducers).
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 more about state machines and their advantages for Python programmers
`in an excellent article by J.P. Calderone. `_
.. toctree::
:maxdepth: 2
:caption: Contents:
about
visualize
api
debugging
Automat-20.2.0/docs/Makefile 0000644 0000765 0000024 00000001137 13225625561 016054 0 ustar glyph staff 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = automat
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) Automat-20.2.0/docs/conf.py 0000644 0000765 0000024 00000012720 13622314366 015712 0 ustar glyph staff 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# automat documentation build configuration file, created by
# sphinx-quickstart on Thu Sep 14 19:11:24 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
docs_dir = os.path.dirname(os.path.abspath(__file__))
automat_dir = os.path.dirname(docs_dir)
sys.path.insert(0, automat_dir)
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'automat'
copyright = '2017, Glyph'
author = 'Glyph'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
def _get_release():
import pkg_resources
try:
distribution = pkg_resources.get_distribution(project)
except pkg_resources.DistributionNotFound:
raise Exception(
"You must install Automat to build the documentation."
)
else:
return distribution.version
# The full version, including alpha/beta/rc tags.
release = _get_release()
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
'donate.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'automatdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'automat.tex', 'automat Documentation',
'Glyph', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'automat', 'automat Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'automat', 'automat Documentation',
author, 'automat', 'One line description of project.',
'Miscellaneous'),
]
Automat-20.2.0/docs/_static/ 0000755 0000765 0000024 00000000000 13622314660 016034 5 ustar glyph staff 0000000 0000000 Automat-20.2.0/docs/_static/mystate.machine.MyMachine._machine.dot.png 0000644 0000765 0000024 00000034624 13225625561 026050 0 ustar glyph staff 0000000 0000000 PNG
IHDR , 9 *f] bKGD IDATxwXTW?PAHU jHDEq%617M1[d75e5&((1b" E"Ed\<