[Python-modules-commits] [automat] 01/02: Import Upstream version 0.5.0
Free Ekanayaka
freee at moszumanska.debian.org
Fri Feb 17 13:40:24 UTC 2017
This is an automated email from the git hooks/post-receive script.
freee pushed a commit to branch master
in repository automat.
commit c30152b9c0a070cb29ac68d8e9a3a8e8bd882b27
Author: Free Ekanayaka <freee at debian.org>
Date: Fri Feb 17 11:41:57 2017 +0000
Import Upstream version 0.5.0
---
.gitignore | 6 +
.travis.yml | 22 ++
Automat.egg-info/PKG-INFO | 448 +++++++++++++++++++++++++
Automat.egg-info/SOURCES.txt | 28 ++
Automat.egg-info/dependency_links.txt | 1 +
Automat.egg-info/entry_points.txt | 3 +
Automat.egg-info/requires.txt | 6 +
Automat.egg-info/top_level.txt | 1 +
LICENSE | 21 ++
PKG-INFO | 448 +++++++++++++++++++++++++
README.md | 421 +++++++++++++++++++++++
automat/__init__.py | 8 +
automat/_core.py | 149 +++++++++
automat/_discover.py | 144 ++++++++
automat/_introspection.py | 41 +++
automat/_methodical.py | 309 +++++++++++++++++
automat/_test/__init__.py | 0
automat/_test/test_core.py | 86 +++++
automat/_test/test_discover.py | 609 ++++++++++++++++++++++++++++++++++
automat/_test/test_methodical.py | 400 ++++++++++++++++++++++
automat/_test/test_visualize.py | 430 ++++++++++++++++++++++++
automat/_visualize.py | 182 ++++++++++
docs/examples/automat_example.py | 135 ++++++++
docs/examples/io_coffee_example.py | 39 +++
docs/examples/lightswitch.py | 39 +++
docs/examples/turnstile_example.py | 55 +++
setup.cfg | 7 +
setup.py | 47 +++
tox.ini | 24 ++
29 files changed, 4109 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f031edd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.tox/
+.coverage*
+*.egg-info/
+*.py[co]
+build/
+dist/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f5ea3c1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+language: python
+python: 2.7
+env:
+ - TOX_ENV=py27-extras
+ - TOX_ENV=py27-noextras
+ - TOX_ENV=pypy-extras
+ - TOX_ENV=pypy-noextras
+ - TOX_ENV=py33-extras
+ - TOX_ENV=py33-noextras
+ - TOX_ENV=py34-extras
+ - TOX_ENV=py34-noextras
+
+install:
+ - sudo apt-get install graphviz
+ - pip install tox coveralls
+
+script:
+ - tox -e $TOX_ENV
+
+after_success:
+ - tox -e coverage-report
+ - coveralls
diff --git a/Automat.egg-info/PKG-INFO b/Automat.egg-info/PKG-INFO
new file mode 100644
index 0000000..c173d43
--- /dev/null
+++ b/Automat.egg-info/PKG-INFO
@@ -0,0 +1,448 @@
+Metadata-Version: 1.0
+Name: Automat
+Version: 0.5.0
+Summary: Self-service finite-state machines for the programmer on the go.
+Home-page: https://github.com/glyph/Automat
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: MIT
+Description:
+ Automat
+ =======
+
+
+ .. 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).
+
+ 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."
+
+ ``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
diff --git a/Automat.egg-info/SOURCES.txt b/Automat.egg-info/SOURCES.txt
new file mode 100644
index 0000000..6511a7b
--- /dev/null
+++ b/Automat.egg-info/SOURCES.txt
@@ -0,0 +1,28 @@
+.gitignore
+.travis.yml
+LICENSE
+README.md
+setup.cfg
+setup.py
+tox.ini
+Automat.egg-info/PKG-INFO
+Automat.egg-info/SOURCES.txt
+Automat.egg-info/dependency_links.txt
+Automat.egg-info/entry_points.txt
+Automat.egg-info/requires.txt
+Automat.egg-info/top_level.txt
+automat/__init__.py
+automat/_core.py
+automat/_discover.py
+automat/_introspection.py
+automat/_methodical.py
+automat/_visualize.py
+automat/_test/__init__.py
+automat/_test/test_core.py
+automat/_test/test_discover.py
+automat/_test/test_methodical.py
+automat/_test/test_visualize.py
+docs/examples/automat_example.py
+docs/examples/io_coffee_example.py
+docs/examples/lightswitch.py
+docs/examples/turnstile_example.py
\ No newline at end of file
diff --git a/Automat.egg-info/dependency_links.txt b/Automat.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/Automat.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/Automat.egg-info/entry_points.txt b/Automat.egg-info/entry_points.txt
new file mode 100644
index 0000000..d793199
--- /dev/null
+++ b/Automat.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+automat-visualize = automat._visualize:tool
+
diff --git a/Automat.egg-info/requires.txt b/Automat.egg-info/requires.txt
new file mode 100644
index 0000000..d1c9e61
--- /dev/null
+++ b/Automat.egg-info/requires.txt
@@ -0,0 +1,6 @@
+attrs
+six
+
+[visualize]
+graphviz>0.5.1
+Twisted>=16.1.1
diff --git a/Automat.egg-info/top_level.txt b/Automat.egg-info/top_level.txt
new file mode 100644
index 0000000..b69387b
--- /dev/null
+++ b/Automat.egg-info/top_level.txt
@@ -0,0 +1 @@
+automat
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9773501
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+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.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..c173d43
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,448 @@
+Metadata-Version: 1.0
+Name: Automat
+Version: 0.5.0
+Summary: Self-service finite-state machines for the programmer on the go.
+Home-page: https://github.com/glyph/Automat
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: MIT
+Description:
+ Automat
+ =======
+
+
+ .. 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).
+
+ 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."
+
+ ``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"
... 3318 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/automat.git
More information about the Python-modules-commits
mailing list