Skip to content

Commit

Permalink
Merge pull request #16 from nazavode/dev
Browse files Browse the repository at this point in the history
Improved README.rst
  • Loading branch information
nazavode committed Feb 5, 2017
2 parents 0f74dfc + 29f0cfc commit 46e9b91
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 43 deletions.
223 changes: 189 additions & 34 deletions README.rst
Expand Up @@ -2,56 +2,211 @@
Automaton
=========

A minimal Python finite-state machine.
Pythonic finite-state machines.

*Automaton requires Python version 3.4 or greater.*
|build-status| |coverage-status| |documentation-status| |codeqa| |pypi| |license-status|

Automaton is an easy to use, *pythonic* `finite-state machine`_ module for Python 3.4 or greater.
The goal here is to have something to define finite-state machines in a minimal, straightforward
and elegant way that enforces **clarity** and **correctness**.

Installation
============
Automaton is available on `pypi <https://pypi.python.org/pypi/python-automaton>`_,
so to install it just use:

.. code::
$ pip3 install python-automaton
Dependencies
============

* `Python >= 3.4`
* `networkx <https://github.com/networkx/networkx>`_
* `tabulate <https://pypi.python.org/pypi/tabulate>`_

|build-status| |coverage-status| |documentation-status| |codeqa| |pypi| |license-status|

Automaton is an easy to use, easy to maintain `finite-state machine`_ package for Python 3.4 or greater.
The goal here is to have something minimal to enforce correctness and to avoid clutter from useless features.
Getting started
===============

In order to define an automaton, just subclass a provided base:

>>> from automaton import *
>>>
>>> class TrafficLight(Automaton):
>>>
>>> go = Event("red", "green")
>>> slowdown = Event("green", "yellow")
>>> stop = Event("yellow", "red")
.. code-block:: python
You're done: you now have a new *automaton* definition that can be instantiated and used as a state machine:
from automaton import *
>>> crossroads = TrafficLight(initial_state="red")
>>> crossroads.state
"red"
class TrafficLight(Automaton):
The automaton can be operated via *events*: signalling the occurrence of an event to the state machine triggers the
evolution of the automaton from an initial state to a final state. You can trigger an event calling the class
attributes themeselves:
go = Event("red", "green")
slowdown = Event("green", "yellow")
stop = Event("yellow", "red")
>>> crossroads.go()
>>> crossroads.state
"green"
>>> crossroads.slowdown()
>>> crossroads.state
"yellow"
You're done: you now have a new *automaton* definition that can be instantiated to an **initial state**:

.. code-block:: python
crossroads = TrafficLight(initial_state="red")
assert crossroads.state == "red"
Each **event** (also known as **transition**) is a class attribute defined by its **source state** and
**destination state**. When an *event fires*, the automaton changes its state from the *source* to the *destination*:
you can *fire* an event by calling it:

.. code-block:: python
crossroads.go()
assert crossroads.state == "green"
crossroads.slowdown()
assert crossroads.state == "yellow"
An alternative way, more convenient if triggering events progammatically, is to call the ``event()`` method:

>>> crossroads.event("stop")
>>> crossroads.state
"red"
.. code-block:: python
crossroads.event("stop")
assert crossroads.state == "red"
Correctness
-----------

Automaton enforces correctness in two ways:

1. checking that the requested event is *valid*, that is a transition from the current state to the destination
state exists in the state machine definition;
#. checking whether the *state graph* representing the automaton is *connected* or not (that is it must have only
1. checking whether the *state graph* representing the automaton is *connected* or not (that is it must have only
one `connected component`_).

So, if you try to trigger an invalid event...

.. code-block:: python
crossroads = TrafficLight(initial_state="red")
crossroads.stop()
...you will end up with an error:

.. code::
automaton.InvalidTransitionError: The specified event 'Event(source_states=('yellow',), dest_state='red')'
is invalid in current state 'red'.
Again, trying to define a class like this...

.. code-block:: python
class BrokenTrafficLight(Automaton):
go = Event("red", "green")
slowdown = Event("green", "yellow")
# broken!
stop = Event("black", "purple")
...will trigger an error:

.. code::
automaton.DefinitionError: The state graph contains 2 connected components:
['green', 'yellow', 'red'], ['purple', 'black']
How to visualize an automaton?
------------------------------

When things are getting complex and it seems that our automata are becoming autonomous life forms grasping to escape
our control, it could be useful to have a *human friendly* representation of their behaviour.

You can ask for the *transition table*...

.. code-block:: python
transitiontable(TrafficLight, fmt='rst')
...and you will be presented with a nice ``reStructuredText`` snippet:

.. code::
======== ====== ========
Source Dest Event
======== ====== ========
green yellow slowdown
yellow red stop
red green go
======== ====== ========
You can ask for the *state graph* as well...

.. code-block:: python
stategraph(TrafficLight, fmt='plantuml')
...and you'll end up with a proper `PlantUML <http://plantuml.com/>`_ representation...

.. code::
@startuml
[*] --> red
green --> yellow : slowdown
red --> green : go
yellow --> red : stop
@enduml
...that can of course be rendered through ``plantuml``:

.. image:: docs/source/_static/trafficlight.png
:alt: Traffic Light Graph


Keep your docstrings tidy!
--------------------------

Since *automata are classes* here, it would be great to have a textual representation of the automaton's behaviour
in our docstrings. What about having one that updates itself in order to stay up-to-date with the
actual code?

Here you have it:

.. code-block:: python
class TrafficLight(Automaton):
""" This is a pretty standard traffic light.
This automaton follows the behaviour defined by
the following transition table:
{automaton:rst}
"""
go = Event("red", "green")
slowdown = Event("green", "yellow")
stop = Event("yellow", "red")
Using a standard format specifier with the ``automaton`` keyword and the proper output format (e.g.: ``rst``), *the
automaton representation will be inserted in the docstring during the class definition*, **just where it should be**:

.. code-block:: python
>>> print(TrafficLight.__doc__)
""" This is a pretty standard traffic light.
This automaton follows the behaviour defined by
the following transition table:
======== ====== ========
Source Dest Event
======== ====== ========
green yellow slowdown
yellow red stop
red green go
======== ====== ========
"""
*Easy!*


Documentation
=============
Expand All @@ -63,7 +218,7 @@ Changes
=======


1.3.0 - **unreleased**
1.3.0 (**unreleased**)
----------------------

Added
Expand All @@ -79,7 +234,7 @@ Changed
- Improved reference and documentation.


1.2.1 - 2017-01-30
1.2.1 (2017-01-30)
------------------

Fixed
Expand All @@ -89,7 +244,7 @@ Fixed
**not against the package installed in ``tox`` virtualenv**.


1.2.0 - 2017-01-29
1.2.0 (2017-01-29)
------------------

Added
Expand All @@ -104,15 +259,15 @@ Changed
- Removed package, now the whole library lives in one module file.


1.1.0 - 2017-01-28
1.1.0 (2017-01-28)
------------------

Added
`````
- Automaton representation as transition table or state-transition graph.


1.0.0 - 2017-01-25
1.0.0 (2017-01-25)
------------------

Added
Expand Down
Binary file added docs/source/_static/trafficlight.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/source/conf.py
Expand Up @@ -67,7 +67,7 @@
# The full version, including alpha/beta/rc tags.
release = '1.2.1'
# The short X.Y version.
version = release.rsplit('.', 1)
version = release.rsplit('.', 1)[0]


# The language for content autogenerated by Sphinx. Refer to documentation
Expand Down
20 changes: 12 additions & 8 deletions src/automaton.py
Expand Up @@ -302,7 +302,7 @@ class Automaton(metaclass=AutomatonMeta):
and `initial_event` arguments *are mutually exclusive*,
specifying both of them will raise a `TypeError`.
.. note:
.. note::
Since the *destination state* of an event is a *single state*
and due to the fact that events are class attributes
(so each event name is unique), *deducing the initial state
Expand Down Expand Up @@ -539,9 +539,9 @@ def __automaton_format__(automaton, fmt):
Parameters
----------
automaton : `~automaton.Automaton` (instance or subclass)
automaton : :class:`~automaton.Automaton`
The automaton to be formatted. It can be both an
instance and a class.
instance or a class.
fmt : str
The format specifier.
Expand All @@ -565,7 +565,7 @@ def get_table(automaton, traversal=None):
Parameters
----------
automaton : `~automaton.Automaton`
automaton : :class:`~automaton.Automaton`
The automaton to be rendered. It can be both
a class and an instance.
traversal : callable(graph), optional
Expand All @@ -580,7 +580,7 @@ def get_table(automaton, traversal=None):
Yields
------
(source, dest, event)
tuple(source, dest, event)
Yields one row at a time as a tuple containing
the source and destination node of the edge and
the name of the event associated with the edge.
Expand All @@ -607,6 +607,8 @@ def stategraph(automaton, fmt=None, traversal=None): # pylint: disable=unused-a
... stop = Event('yellow', 'red')
>>> print( plantuml(TrafficLight) ) # doctest: +SKIP
::
@startuml
green --> yellow : slowdown
yellow --> red : stop
Expand All @@ -615,10 +617,10 @@ def stategraph(automaton, fmt=None, traversal=None): # pylint: disable=unused-a
Parameters
----------
automaton : `~automaton.Automaton`
automaton : :class:`~automaton.Automaton`
The automaton to be rendered. It can be both
a class and an instance.
fmt str, optional
fmt : str, optional
Specifies the output format for the graph.
Currently, the only supported format is
`PlantUML <http://plantuml.com/state-diagram>`_.
Expand Down Expand Up @@ -669,6 +671,8 @@ def transitiontable(automaton, header=None, fmt=None, traversal=None):
... stop = Event('yellow', 'red')
>>> tabulate(TrafficLight) # doctest: +SKIP
::
======== ====== ========
Source Dest Event
======== ====== ========
Expand All @@ -679,7 +683,7 @@ def transitiontable(automaton, header=None, fmt=None, traversal=None):
Parameters
----------
automaton : `~automaton.Automaton`
automaton : :class:`~automaton.Automaton`
The automaton to be rendered. It can be both
a class and an instance.
header : list[str, str, str]
Expand Down

0 comments on commit 46e9b91

Please sign in to comment.