Automat-0.6.0/ 0000755 0001750 0001750 00000000000 13106704143 012562 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/.gitignore 0000644 0001750 0001750 00000000063 13014133630 014544 0 ustar mrw mrw 0000000 0000000 .tox/
.coverage*
*.egg-info/
*.py[co]
build/
dist/
Automat-0.6.0/.travis.yml 0000644 0001750 0001750 00000001455 13106675015 014705 0 ustar mrw mrw 0000000 0000000 language: python
matrix:
include:
- python: 2.7
env: TOX_ENV=py27-extras
- python: 2.7
env: TOX_ENV=py27-noextras
- python: pypy
env: TOX_ENV=pypy-extras
- python: pypy
env: TOX_ENV=pypy-noextras
- python: 3.3
env: TOX_ENV=py33-extras
- python: 3.3
env: TOX_ENV=py33-noextras
- python: 3.4
env: TOX_ENV=py34-extras
- python: 3.4
env: TOX_ENV=py34-noextras
- python: 3.5
env: TOX_ENV=py35-extras
- python: 3.5
env: TOX_ENV=py35-noextras
- python: 3.6
env: TOX_ENV=py36-extras
- python: 3.6
env: TOX_ENV=py36-noextras
install:
- sudo apt-get install graphviz
- pip install tox coveralls
script:
- tox -e $TOX_ENV
after_success:
- tox -e coverage-report
- coveralls
Automat-0.6.0/Automat.egg-info/ 0000755 0001750 0001750 00000000000 13106704143 015666 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/Automat.egg-info/PKG-INFO 0000644 0001750 0001750 00000046613 13106704143 016775 0 ustar mrw mrw 0000000 0000000 Metadata-Version: 1.0
Name: Automat
Version: 0.6.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://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 `_.
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."
``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
Automat-0.6.0/Automat.egg-info/SOURCES.txt 0000644 0001750 0001750 00000001307 13106704143 017553 0 ustar mrw mrw 0000000 0000000 .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_trace.py
automat/_test/test_visualize.py
docs/debugging.md
docs/examples/automat_example.py
docs/examples/io_coffee_example.py
docs/examples/lightswitch.py
docs/examples/turnstile_example.py Automat-0.6.0/Automat.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13106704143 021734 0 ustar mrw mrw 0000000 0000000
Automat-0.6.0/Automat.egg-info/entry_points.txt 0000644 0001750 0001750 00000000077 13106704143 021170 0 ustar mrw mrw 0000000 0000000 [console_scripts]
automat-visualize = automat._visualize:tool
Automat-0.6.0/Automat.egg-info/requires.txt 0000644 0001750 0001750 00000000066 13106704143 020270 0 ustar mrw mrw 0000000 0000000 attrs
six
[visualize]
graphviz>0.5.1
Twisted>=16.1.1
Automat-0.6.0/Automat.egg-info/top_level.txt 0000644 0001750 0001750 00000000010 13106704143 020407 0 ustar mrw mrw 0000000 0000000 automat
Automat-0.6.0/LICENSE 0000644 0001750 0001750 00000002035 12641273151 013572 0 ustar mrw mrw 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-0.6.0/PKG-INFO 0000644 0001750 0001750 00000046613 13106704143 013671 0 ustar mrw mrw 0000000 0000000 Metadata-Version: 1.0
Name: Automat
Version: 0.6.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://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 `_.
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."
``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
Automat-0.6.0/README.md 0000644 0001750 0001750 00000035136 13037440020 014043 0 ustar mrw mrw 0000000 0000000 # Automat #
[](https://travis-ci.org/glyph/automat)
[](https://coveralls.io/r/glyph/automat)
## 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:
```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:
```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:
```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:
```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`:
```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:)
```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`:
```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):
```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:
```python
if connection_state_machine.state == "CONNECTED":
connection_state_machine.send_message()
else:
print("not connected")
```
Instead, just make your calling code do this:
```python
connection_state_machine.send_message()
```
and then change your state machine to look like this:
```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.
```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:
```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:
```python
dont_have_beans.upon(put_in_beans, enter=have_beans,
outputs=[_save_beans])
```
Now, when you call:
```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:
```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:
```python
have_beans.upon(brew_button, enter=dont_have_beans,
outputs=[_heat_the_heating_element,
_describe_coffee])
```
Now, we can call it:
```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:
```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:
```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:
```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.
```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:
```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:
```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:
```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:
```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!
Automat-0.6.0/automat/ 0000755 0001750 0001750 00000000000 13106704143 014234 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/automat/__init__.py 0000644 0001750 0001750 00000000251 12720705662 016353 0 ustar mrw mrw 0000000 0000000 # -*- test-case-name: automat -*-
from ._methodical import MethodicalMachine
from ._core import NoTransition
__all__ = [
'MethodicalMachine',
'NoTransition',
]
Automat-0.6.0/automat/_core.py 0000644 0001750 0001750 00000011331 13106675015 015701 0 ustar mrw mrw 0000000 0000000 # -*- test-case-name: automat._test.test_core -*-
"""
A core state-machine abstraction.
Perhaps something that could be replaced with or integrated into machinist.
"""
from itertools import chain
_NO_STATE = ""
class NoTransition(Exception):
"""
A finite state machine in C{state} has no transition for C{symbol}.
@param state: the finite state machine's state at the time of the
illegal transition.
@param symbol: the input symbol for which no transition exists.
"""
def __init__(self, state, symbol):
self.state = state
self.symbol = symbol
super(Exception, self).__init__(
"no transition for {} in {}".format(symbol, state)
)
class Automaton(object):
"""
A declaration of a finite state machine.
Note that this is not the machine itself; it is immutable.
"""
def __init__(self):
"""
Initialize the set of transitions and the initial state.
"""
self._initialState = _NO_STATE
self._transitions = set()
@property
def initialState(self):
"""
Return this automaton's initial state.
"""
return self._initialState
@initialState.setter
def initialState(self, state):
"""
Set this automaton's initial state. Raises a ValueError if
this automaton already has an initial state.
"""
if self._initialState is not _NO_STATE:
raise ValueError(
"initial state already set to {}".format(self._initialState))
self._initialState = state
def addTransition(self, inState, inputSymbol, outState, outputSymbols):
"""
Add the given transition to the outputSymbol. Raise ValueError if
there is already a transition with the same inState and inputSymbol.
"""
# keeping self._transitions in a flat list makes addTransition
# O(n^2), but state machines don't tend to have hundreds of
# transitions.
for (anInState, anInputSymbol, anOutState, _) in self._transitions:
if (anInState == inState and anInputSymbol == inputSymbol):
raise ValueError(
"already have transition from {} via {}".format(inState, inputSymbol))
self._transitions.add(
(inState, inputSymbol, outState, tuple(outputSymbols))
)
def allTransitions(self):
"""
All transitions.
"""
return frozenset(self._transitions)
def inputAlphabet(self):
"""
The full set of symbols acceptable to this automaton.
"""
return set(inputSymbol for (inState, inputSymbol, outState,
outputSymbol) in self._transitions)
def outputAlphabet(self):
"""
The full set of symbols which can be produced by this automaton.
"""
return set(
chain.from_iterable(
outputSymbols for
(inState, inputSymbol, outState, outputSymbols)
in self._transitions
)
)
def states(self):
"""
All valid states; "Q" in the mathematical description of a state
machine.
"""
return frozenset(
chain.from_iterable(
(inState, outState)
for
(inState, inputSymbol, outState, outputSymbol)
in self._transitions
)
)
def outputForInput(self, inState, inputSymbol):
"""
A 2-tuple of (outState, outputSymbols) for inputSymbol.
"""
for (anInState, anInputSymbol,
outState, outputSymbols) in self._transitions:
if (inState, inputSymbol) == (anInState, anInputSymbol):
return (outState, list(outputSymbols))
raise NoTransition(state=inState, symbol=inputSymbol)
class Transitioner(object):
"""
The combination of a current state and an L{Automaton}.
"""
def __init__(self, automaton, initialState):
self._automaton = automaton
self._state = initialState
self._tracer = None
def setTrace(self, tracer):
self._tracer = tracer
def transition(self, inputSymbol):
"""
Transition between states, returning any outputs.
"""
outState, outputSymbols = self._automaton.outputForInput(self._state,
inputSymbol)
outTracer = None
if self._tracer:
outTracer = self._tracer(self._state._name(),
inputSymbol._name(),
outState._name())
self._state = outState
return (outputSymbols, outTracer)
Automat-0.6.0/automat/_discover.py 0000644 0001750 0001750 00000010417 13014133630 016561 0 ustar mrw mrw 0000000 0000000 import collections
import inspect
from automat import MethodicalMachine
from twisted.python.modules import PythonModule, getModule
def isOriginalLocation(attr):
"""
Attempt to discover if this appearance of a PythonAttribute
representing a class refers to the module where that class was
defined.
"""
sourceModule = inspect.getmodule(attr.load())
if sourceModule is None:
return False
currentModule = attr
while not isinstance(currentModule, PythonModule):
currentModule = currentModule.onObject
return currentModule.name == sourceModule.__name__
def findMachinesViaWrapper(within):
"""
Recursively yield L{MethodicalMachine}s and their FQPNs within a
L{PythonModule} or a L{twisted.python.modules.PythonAttribute}
wrapper object.
Note that L{PythonModule}s may refer to packages, as well.
The discovery heuristic considers L{MethodicalMachine} instances
that are module-level attributes or class-level attributes
accessible from module scope. Machines inside nested classes will
be discovered, but those returned from functions or methods will not be.
@type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute}
@param within: Where to start the search.
@return: a generator which yields FQPN, L{MethodicalMachine} pairs.
"""
queue = collections.deque([within])
visited = set()
while queue:
attr = queue.pop()
value = attr.load()
if isinstance(value, MethodicalMachine) and value not in visited:
visited.add(value)
yield attr.name, value
elif (inspect.isclass(value) and isOriginalLocation(attr) and
value not in visited):
visited.add(value)
queue.extendleft(attr.iterAttributes())
elif isinstance(attr, PythonModule) and value not in visited:
visited.add(value)
queue.extendleft(attr.iterAttributes())
queue.extendleft(attr.iterModules())
class InvalidFQPN(Exception):
"""
The given FQPN was not a dot-separated list of Python objects.
"""
class NoModule(InvalidFQPN):
"""
A prefix of the FQPN was not an importable module or package.
"""
class NoObject(InvalidFQPN):
"""
A suffix of the FQPN was not an accessible object
"""
def wrapFQPN(fqpn):
"""
Given an FQPN, retrieve the object via the global Python module
namespace and wrap it with a L{PythonModule} or a
L{twisted.python.modules.PythonAttribute}.
"""
# largely cribbed from t.p.reflect.namedAny
if not fqpn:
raise InvalidFQPN("FQPN was empty")
components = collections.deque(fqpn.split('.'))
if '' in components:
raise InvalidFQPN(
"name must be a string giving a '.'-separated list of Python "
"identifiers, not %r" % (fqpn,))
component = components.popleft()
try:
module = getModule(component)
except KeyError:
raise NoModule(component)
# find the bottom-most module
while components:
component = components.popleft()
try:
module = module[component]
except KeyError:
components.appendleft(component)
break
else:
module.load()
else:
return module
# find the bottom-most attribute
attribute = module
for component in components:
try:
attribute = next(child for child in attribute.iterAttributes()
if child.name.rsplit('.', 1)[-1] == component)
except StopIteration:
raise NoObject('{}.{}'.format(attribute.name, component))
return attribute
def findMachines(fqpn):
"""
Recursively yield L{MethodicalMachine}s and their FQPNs in and
under the a Python object specified by an FQPN.
The discovery heuristic considers L{MethodicalMachine} instances
that are module-level attributes or class-level attributes
accessible from module scope. Machines inside nested classes will
be discovered, but those returned from functions or methods will not be.
@type within: an FQPN
@param within: Where to start the search.
@return: a generator which yields FQPN, L{MethodicalMachine} pairs.
"""
return findMachinesViaWrapper(wrapFQPN(fqpn))
Automat-0.6.0/automat/_introspection.py 0000644 0001750 0001750 00000002134 12717000352 017643 0 ustar mrw mrw 0000000 0000000 """
Python introspection helpers.
"""
from types import CodeType as code, FunctionType as function
def copycode(template, changes):
names = [
"argcount", "nlocals", "stacksize", "flags", "code", "consts",
"names", "varnames", "filename", "name", "firstlineno", "lnotab",
"freevars", "cellvars"
]
if str is not bytes:
names.insert(1, "kwonlyargcount")
values = [
changes.get(name, getattr(template, "co_" + name))
for name in names
]
return code(*values)
def copyfunction(template, funcchanges, codechanges):
names = [
"globals", "name", "defaults", "closure",
]
values = [
funcchanges.get(name, getattr(template, "__" + name + "__"))
for name in names
]
return function(copycode(template.__code__, codechanges), *values)
def preserveName(f):
"""
Preserve the name of the given function on the decorated function.
"""
def decorator(decorated):
return copyfunction(decorated,
dict(name=f.__name__), dict(name=f.__name__))
return decorator
Automat-0.6.0/automat/_methodical.py 0000644 0001750 0001750 00000027116 13106703427 017071 0 ustar mrw mrw 0000000 0000000 # -*- test-case-name: automat._test.test_methodical -*-
from functools import wraps
from itertools import count
try:
# Python 3
from inspect import getfullargspec as getArgsSpec
except ImportError:
# Python 2
from inspect import getargspec as getArgsSpec
import attr
from ._core import Transitioner, Automaton
from ._introspection import preserveName
def _keywords_only(f):
"""
Decorate a function so all its arguments must be passed by keyword.
A useful utility for decorators that take arguments so that they don't
accidentally get passed the thing they're decorating as their first
argument.
Only works for methods right now.
"""
@wraps(f)
def g(self, **kw):
return f(self, **kw)
return g
@attr.s(frozen=True)
class MethodicalState(object):
"""
A state for a L{MethodicalMachine}.
"""
machine = attr.ib(repr=False)
method = attr.ib()
serialized = attr.ib(repr=False)
def upon(self, input, enter, outputs, collector=list):
"""
Declare a state transition within the L{MethodicalMachine} associated
with this L{MethodicalState}: upon the receipt of the input C{input},
enter the state C{enter}, emitting each output in C{outputs}.
"""
inputSpec = getArgsSpec(input.method)
for output in outputs:
outputSpec = getArgsSpec(output.method)
if inputSpec != outputSpec:
raise TypeError(
"method {input} signature {inputSignature} "
"does not match output {output} "
"signature {outputSignature}".format(
input=input.method.__name__,
output=output.method.__name__,
inputSignature=inputSpec,
outputSignature=outputSpec,
))
self.machine._oneTransition(self, input, enter, outputs, collector)
def _name(self):
return self.method.__name__
def _transitionerFromInstance(oself, symbol, automaton):
"""
Get a L{Transitioner}
"""
transitioner = getattr(oself, symbol, None)
if transitioner is None:
transitioner = Transitioner(
automaton,
automaton.initialState,
)
setattr(oself, symbol, transitioner)
return transitioner
def _empty():
pass
def _docstring():
"""docstring"""
def assertNoCode(inst, attribute, f):
# The function body must be empty, i.e. "pass" or "return None", which
# both yield the same bytecode: LOAD_CONST (None), RETURN_VALUE. We also
# accept functions with only a docstring, which yields slightly different
# bytecode, because the "None" is put in a different constant slot.
# Unfortunately, this does not catch function bodies that return a
# constant value, e.g. "return 1", because their code is identical to a
# "return None". They differ in the contents of their constant table, but
# checking that would require us to parse the bytecode, find the index
# being returned, then making sure the table has a None at that index.
if f.__code__.co_code not in (_empty.__code__.co_code,
_docstring.__code__.co_code):
raise ValueError("function body must be empty")
@attr.s(cmp=False, hash=False)
class MethodicalInput(object):
"""
An input for a L{MethodicalMachine}.
"""
automaton = attr.ib(repr=False)
method = attr.ib(validator=assertNoCode)
symbol = attr.ib(repr=False)
collectors = attr.ib(default=attr.Factory(dict), repr=False)
def __get__(self, oself, type=None):
"""
Return a function that takes no arguments and returns values returned
by output functions produced by the given L{MethodicalInput} in
C{oself}'s current state.
"""
transitioner = _transitionerFromInstance(oself, self.symbol,
self.automaton)
@preserveName(self.method)
@wraps(self.method)
def doInput(*args, **kwargs):
self.method(oself, *args, **kwargs)
previousState = transitioner._state
(outputs, outTracer) = transitioner.transition(self)
collector = self.collectors[previousState]
values = []
for output in outputs:
if outTracer:
outTracer(output._name())
value = output(oself, *args, **kwargs)
values.append(value)
return collector(values)
return doInput
def _name(self):
return self.method.__name__
@attr.s(frozen=True)
class MethodicalOutput(object):
"""
An output for a L{MethodicalMachine}.
"""
machine = attr.ib(repr=False)
method = attr.ib()
def __get__(self, oself, type=None):
"""
Outputs are private, so raise an exception when we attempt to get one.
"""
raise AttributeError(
"{cls}.{method} is a state-machine output method; "
"to produce this output, call an input method instead.".format(
cls=type.__name__,
method=self.method.__name__
)
)
def __call__(self, oself, *args, **kwargs):
"""
Call the underlying method.
"""
return self.method(oself, *args, **kwargs)
def _name(self):
return self.method.__name__
@attr.s(cmp=False, hash=False)
class MethodicalTracer(object):
automaton = attr.ib(repr=False)
symbol = attr.ib(repr=False)
def __get__(self, oself, type=None):
transitioner = _transitionerFromInstance(oself, self.symbol,
self.automaton)
def setTrace(tracer):
transitioner.setTrace(tracer)
return setTrace
counter = count()
def gensym():
"""
Create a unique Python identifier.
"""
return "_symbol_" + str(next(counter))
class MethodicalMachine(object):
"""
A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods
on a class.
"""
def __init__(self):
self._automaton = Automaton()
self._reducers = {}
self._symbol = gensym()
def __get__(self, oself, type=None):
"""
L{MethodicalMachine} is an implementation detail for setting up
class-level state; applications should never need to access it on an
instance.
"""
if oself is not None:
raise AttributeError(
"MethodicalMachine is an implementation detail.")
return self
@_keywords_only
def state(self, initial=False, terminal=False,
serialized=None):
"""
Declare a state, possibly an initial state or a terminal state.
This is a decorator for methods, but it will modify the method so as
not to be callable any more.
@param initial: is this state the initial state? Only one state on
this L{MethodicalMachine} may be an initial state; more than one is
an error.
@type initial: L{bool}
@param terminal: Is this state a terminal state, i.e. a state that the
machine can end up in? (This is purely informational at this
point.)
@type terminal: L{bool}
@param serialized: a serializable value to be used to represent this
state to external systems. This value should be hashable;
L{unicode} is a good type to use.
@type serialized: a hashable (comparable) value
"""
def decorator(stateMethod):
state = MethodicalState(machine=self,
method=stateMethod,
serialized=serialized)
if initial:
self._automaton.initialState = state
return state
return decorator
@_keywords_only
def input(self):
"""
Declare an input.
This is a decorator for methods.
"""
def decorator(inputMethod):
return MethodicalInput(automaton=self._automaton,
method=inputMethod,
symbol=self._symbol)
return decorator
@_keywords_only
def output(self):
"""
Declare an output.
This is a decorator for methods.
This method will be called when the state machine transitions to this
state as specified in the L{MethodicalMachine.output} method.
"""
def decorator(outputMethod):
return MethodicalOutput(machine=self, method=outputMethod)
return decorator
def _oneTransition(self, startState, inputToken, endState, outputTokens,
collector):
"""
See L{MethodicalState.upon}.
"""
# FIXME: tests for all of this (some of it is wrong)
# if not isinstance(startState, MethodicalState):
# raise NotImplementedError("start state {} isn't a state"
# .format(startState))
# if not isinstance(inputToken, MethodicalInput):
# raise NotImplementedError("start state {} isn't an input"
# .format(inputToken))
# if not isinstance(endState, MethodicalState):
# raise NotImplementedError("end state {} isn't a state"
# .format(startState))
# for output in outputTokens:
# if not isinstance(endState, MethodicalState):
# raise NotImplementedError("output state {} isn't a state"
# .format(endState))
self._automaton.addTransition(startState, inputToken, endState,
tuple(outputTokens))
inputToken.collectors[startState] = collector
@_keywords_only
def serializer(self):
"""
"""
def decorator(decoratee):
@wraps(decoratee)
def serialize(oself):
transitioner = _transitionerFromInstance(oself, self._symbol,
self._automaton)
return decoratee(oself, transitioner._state.serialized)
return serialize
return decorator
@_keywords_only
def unserializer(self):
"""
"""
def decorator(decoratee):
@wraps(decoratee)
def unserialize(oself, *args, **kwargs):
state = decoratee(oself, *args, **kwargs)
mapping = {}
for eachState in self._automaton.states():
mapping[eachState.serialized] = eachState
transitioner = _transitionerFromInstance(
oself, self._symbol, self._automaton)
transitioner._state = mapping[state]
return None # it's on purpose
return unserialize
return decorator
@property
def _setTrace(self):
return MethodicalTracer(self._automaton, self._symbol)
def asDigraph(self):
"""
Generate a L{graphviz.Digraph} that represents this machine's
states and transitions.
@return: L{graphviz.Digraph} object; for more information, please
see the documentation for
U{graphviz}
"""
from ._visualize import makeDigraph
return makeDigraph(
self._automaton,
stateAsString=lambda state: state.method.__name__,
inputAsString=lambda input: input.method.__name__,
outputAsString=lambda output: output.method.__name__,
)
Automat-0.6.0/automat/_test/ 0000755 0001750 0001750 00000000000 13106704143 015352 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/automat/_test/__init__.py 0000644 0001750 0001750 00000000000 12717000352 017447 0 ustar mrw mrw 0000000 0000000 Automat-0.6.0/automat/_test/test_core.py 0000644 0001750 0001750 00000005440 12717000352 017714 0 ustar mrw mrw 0000000 0000000
from .._core import Automaton, NoTransition
from unittest import TestCase
class CoreTests(TestCase):
"""
Tests for Automat's (currently private, implementation detail) core.
"""
def test_NoTransition(self):
"""
A L{NoTransition} exception describes the state and input symbol
that caused it.
"""
# NoTransition requires two arguments
with self.assertRaises(TypeError):
NoTransition()
state = "current-state"
symbol = "transitionless-symbol"
noTransitionException = NoTransition(state=state, symbol=symbol)
self.assertIs(noTransitionException.symbol, symbol)
self.assertIn(state, str(noTransitionException))
self.assertIn(symbol, str(noTransitionException))
def test_noOutputForInput(self):
"""
L{Automaton.outputForInput} raises L{NoTransition} if no
transition for that input is defined.
"""
a = Automaton()
self.assertRaises(NoTransition, a.outputForInput,
"no-state", "no-symbol")
def test_oneTransition(self):
"""
L{Automaton.addTransition} adds its input symbol to
L{Automaton.inputAlphabet}, all its outputs to
L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to
start returning the new state and output symbols.
"""
a = Automaton()
a.addTransition("beginning", "begin", "ending", ["end"])
self.assertEqual(a.inputAlphabet(), set(["begin"]))
self.assertEqual(a.outputAlphabet(), set(["end"]))
self.assertEqual(a.outputForInput("beginning", "begin"),
("ending", ["end"]))
self.assertEqual(a.states(), set(["beginning", "ending"]))
def test_oneTransition_nonIterableOutputs(self):
"""
L{Automaton.addTransition} raises a TypeError when given outputs
that aren't iterable and doesn't add any transitions.
"""
a = Automaton()
nonIterableOutputs = 1
self.assertRaises(
TypeError,
a.addTransition,
"fromState", "viaSymbol", "toState", nonIterableOutputs)
self.assertFalse(a.inputAlphabet())
self.assertFalse(a.outputAlphabet())
self.assertFalse(a.states())
self.assertFalse(a.allTransitions())
def test_initialState(self):
"""
L{Automaton.initialState} is a descriptor that sets the initial
state if it's not yet set, and raises L{ValueError} if it is.
"""
a = Automaton()
a.initialState = "a state"
self.assertEqual(a.initialState, "a state")
with self.assertRaises(ValueError):
a.initialState = "another state"
# FIXME: addTransition for transition that's been added before
Automat-0.6.0/automat/_test/test_discover.py 0000644 0001750 0001750 00000052526 13014133630 020606 0 ustar mrw mrw 0000000 0000000 import operator
import os
import shutil
import sys
import textwrap
import tempfile
from unittest import skipIf, TestCase
import six
def isTwistedInstalled():
try:
__import__('twisted')
except ImportError:
return False
else:
return True
class _WritesPythonModules(TestCase):
"""
A helper that enables generating Python module test fixtures.
"""
def setUp(self):
super(_WritesPythonModules, self).setUp()
from twisted.python.modules import getModule, PythonPath
from twisted.python.filepath import FilePath
self.getModule = getModule
self.PythonPath = PythonPath
self.FilePath = FilePath
self.originalSysModules = set(sys.modules.keys())
self.savedSysPath = sys.path[:]
self.pathDir = tempfile.mkdtemp()
self.makeImportable(self.pathDir)
def tearDown(self):
super(_WritesPythonModules, self).tearDown()
sys.path[:] = self.savedSysPath
modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules
for module in modulesToDelete:
del sys.modules[module]
shutil.rmtree(self.pathDir)
def makeImportable(self, path):
sys.path.append(path)
def writeSourceInto(self, source, path, moduleName):
directory = self.FilePath(path)
module = directory.child(moduleName)
# FilePath always opens a file in binary mode - but that will
# break on Python 3
with open(module.path, 'w') as f:
f.write(textwrap.dedent(source))
return self.PythonPath([directory.path])
def makeModule(self, source, path, moduleName):
pythonModuleName, _ = os.path.splitext(moduleName)
return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
def attributesAsDict(self, hasIterAttributes):
return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
def loadModuleAsDict(self, module):
module.load()
return self.attributesAsDict(module)
def makeModuleAsDict(self, source, path, name):
return self.loadModuleAsDict(self.makeModule(source, path, name))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class OriginalLocationTests(_WritesPythonModules):
"""
Tests that L{isOriginalLocation} detects when a
L{PythonAttribute}'s FQPN refers to an object inside the module
where it was defined.
For example: A L{twisted.python.modules.PythonAttribute} with a
name of 'foo.bar' that refers to a 'bar' object defined in module
'baz' does *not* refer to bar's original location, while a
L{PythonAttribute} with a name of 'baz.bar' does.
"""
def setUp(self):
super(OriginalLocationTests, self).setUp()
from .._discover import isOriginalLocation
self.isOriginalLocation = isOriginalLocation
def test_failsWithNoModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to an
object whose source module cannot be determined.
"""
source = """\
class Fake(object):
pass
hasEmptyModule = Fake()
hasEmptyModule.__module__ = None
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'empty_module_attr.py')
self.assertFalse(self.isOriginalLocation(
moduleDict['empty_module_attr.hasEmptyModule']))
def test_failsWithDifferentModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to
an object outside of the module where that object was defined.
"""
originalSource = """\
class ImportThisClass(object):
pass
importThisObject = ImportThisClass()
importThisNestingObject = ImportThisClass()
importThisNestingObject.nestedObject = ImportThisClass()
"""
importingSource = """\
from original import (ImportThisClass,
importThisObject,
importThisNestingObject)
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingDict = self.makeModuleAsDict(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.ImportThisClass']))
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.importThisObject']))
nestingObject = importingDict['importing.importThisNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict[
'importing.importThisNestingObject.nestedObject']
self.assertFalse(self.isOriginalLocation(nestedObject))
def test_succeedsWithSameModule(self):
"""
L{isOriginalLocation} returns True when the attribute refers to an
object inside the module where that object was defined.
"""
mSource = textwrap.dedent("""
class ThisClassWasDefinedHere(object):
pass
anObject = ThisClassWasDefinedHere()
aNestingObject = ThisClassWasDefinedHere()
aNestingObject.nestedObject = ThisClassWasDefinedHere()
""")
mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py')
self.assertTrue(self.isOriginalLocation(
mDict['m.ThisClassWasDefinedHere']))
self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject']))
nestingObject = mDict['m.aNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict['m.aNestingObject.nestedObject']
self.assertTrue(self.isOriginalLocation(nestedObject))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesViaWrapperTests(_WritesPythonModules):
"""
L{findMachinesViaWrapper} recursively yields FQPN,
L{MethodicalMachine} pairs in and under a given
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute}.
"""
TEST_MODULE_SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
class NestedClass(object):
_nestedClassMachine = MethodicalMachine()
ignoredAttribute = "I am ignored."
def ignoredMethod(self):
"I am also ignored."
rootLevelMachine = MethodicalMachine()
ignoredPythonObject = PythonClass()
anotherIgnoredPythonObject = "I am ignored."
"""
def setUp(self):
super(FindMachinesViaWrapperTests, self).setUp()
from .._discover import findMachinesViaWrapper
self.findMachinesViaWrapper = findMachinesViaWrapper
def test_yieldsMachine(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py')
rootMachine = moduleDict['root.rootMachine']
self.assertIn(('root.rootMachine', rootMachine.load()),
list(self.findMachinesViaWrapper(rootMachine)))
def test_yieldsMachineInClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class that contains a L{MethodicalMachine} as a class
variable, L{findMachinesViaWrapper} yields that machine and
its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py')
PythonClass = moduleDict['clsmod.PythonClass']
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass.load()._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInNestedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a nested class that contains a L{MethodicalMachine} as a
class variable, L{findMachinesViaWrapper} yields that machine
and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'nestedcls.py')
PythonClass = moduleDict['nestedcls.PythonClass']
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.load().NestedClass._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
a module that contains a L{MethodicalMachine},
L{findMachinesViaWrapper} yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'root.py')
rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load()
self.assertIn(('root.rootMachine', rootMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'clsmod.py')
PythonClass = self.loadModuleAsDict(
module)['clsmod.PythonClass'].load()
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInNestedClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a nested class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'nestedcls.py')
PythonClass = self.loadModuleAsDict(
module)['nestedcls.PythonClass'].load()
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.NestedClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_ignoresImportedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class imported from another module, any
L{MethodicalMachine}s on that class are ignored.
This behavior ensures that a machine is only discovered on a
class when visiting the module where that class was defined.
"""
originalSource = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
importingSource = """
from original import PythonClass
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingModule = self.makeModule(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
def test_descendsIntoPackages(self):
"""
L{findMachinesViaWrapper} descends into packages to discover
machines.
"""
pythonPath = self.PythonPath([self.pathDir])
package = self.FilePath(self.pathDir).child("test_package")
package.makedirs()
package.child('__init__.py').touch()
source = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
rootMachine = MethodicalMachine()
"""
self.makeModule(source, package.path, 'module.py')
test_package = pythonPath['test_package']
machines = sorted(self.findMachinesViaWrapper(test_package),
key=operator.itemgetter(0))
moduleDict = self.loadModuleAsDict(test_package['module'])
rootMachine = moduleDict['test_package.module.rootMachine'].load()
PythonClass = moduleDict['test_package.module.PythonClass'].load()
expectedMachines = sorted(
[('test_package.module.rootMachine',
rootMachine),
('test_package.module.PythonClass._classMachine',
PythonClass._classMachine)], key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)
def test_infiniteLoop(self):
"""
L{findMachinesViaWrapper} ignores infinite loops.
Note this test can't fail - it can only run forever!
"""
source = """
class InfiniteLoop(object):
pass
InfiniteLoop.loop = InfiniteLoop
"""
module = self.makeModule(source, self.pathDir, 'loop.py')
self.assertFalse(list(self.findMachinesViaWrapper(module)))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class WrapFQPNTests(TestCase):
"""
Tests that ensure L{wrapFQPN} loads the
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute} for a given FQPN.
"""
def setUp(self):
from twisted.python.modules import PythonModule, PythonAttribute
from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
self.PythonModule = PythonModule
self.PythonAttribute = PythonAttribute
self.wrapFQPN = wrapFQPN
self.InvalidFQPN = InvalidFQPN
self.NoModule = NoModule
self.NoObject = NoObject
def assertModuleWrapperRefersTo(self, moduleWrapper, module):
"""
Assert that a L{twisted.python.modules.PythonModule} refers to a
particular Python module.
"""
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertEqual(moduleWrapper.name, module.__name__)
self.assertIs(moduleWrapper.load(), module)
def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
"""
Assert that a L{twisted.python.modules.PythonAttribute} refers to a
particular Python object.
"""
self.assertIsInstance(attributeWrapper, self.PythonAttribute)
self.assertEqual(attributeWrapper.name, fqpn)
self.assertIs(attributeWrapper.load(), obj)
def test_failsWithEmptyFQPN(self):
"""
L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
"""
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN('')
def test_failsWithBadDotting(self):
""""
L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
FQPN. (e.g., x..y).
"""
for bad in ('.fails', 'fails.', 'this..fails'):
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN(bad)
def test_singleModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single module a dotless FQPN describes.
"""
import os
moduleWrapper = self.wrapFQPN('os')
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertIs(moduleWrapper.load(), os)
def test_failsWithMissingSingleModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
not refer to a module or package.
"""
with self.assertRaises(self.NoModule):
self.wrapFQPN("this is not an acceptable name!")
def test_singlePackage(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single package a dotless FQPN describes.
"""
import xml
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml)
def test_multiplePackages(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest package described by dotted FQPN.
"""
import xml.etree
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree)
def test_multiplePackagesFinalModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest module described by dotted FQPN.
"""
import xml.etree.ElementTree
self.assertModuleWrapperRefersTo(
self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree)
def test_singleModuleObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object an FQPN names, traversing one module.
"""
import os
self.assertAttributeWrapperRefersTo(
self.wrapFQPN('os.path'), 'os.path', os.path)
def test_multiplePackagesObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object described by an FQPN,
descending through several packages.
"""
import xml.etree.ElementTree
import automat
for fqpn, obj in [('xml.etree.ElementTree.fromstring',
xml.etree.ElementTree.fromstring),
('automat.MethodicalMachine.__doc__',
automat.MethodicalMachine.__doc__)]:
self.assertAttributeWrapperRefersTo(
self.wrapFQPN(fqpn), fqpn, obj)
def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
missing attribute, module, or package.
"""
for bad in ('xml.etree.nope!',
'xml.etree.nope!.but.the.rest.is.believable'):
with self.assertRaises(self.NoObject):
self.wrapFQPN(bad)
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesIntegrationTests(_WritesPythonModules):
"""
Integration tests to check that L{findMachines} yields all
machines discoverable at or below an FQPN.
"""
SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_machine = MethodicalMachine()
ignored = "i am ignored"
rootLevel = MethodicalMachine()
ignored = "i am ignored"
"""
def setUp(self):
super(FindMachinesIntegrationTests, self).setUp()
from .._discover import findMachines
self.findMachines = findMachines
packageDir = self.FilePath(self.pathDir).child("test_package")
packageDir.makedirs()
self.pythonPath = self.PythonPath([self.pathDir])
self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py')
subPackageDir = packageDir.child('subpackage')
subPackageDir.makedirs()
subPackageDir.child('__init__.py').touch()
self.makeModule(self.SOURCE, subPackageDir.path, 'module.py')
self.packageDict = self.loadModuleAsDict(
self.pythonPath['test_package'])
self.moduleDict = self.loadModuleAsDict(
self.pythonPath['test_package']['subpackage']['module'])
def test_discoverAll(self):
"""
Given a top-level package FQPN, L{findMachines} discovers all
L{MethodicalMachine} instances in and below it.
"""
machines = sorted(self.findMachines('test_package'),
key=operator.itemgetter(0))
tpRootLevel = self.packageDict['test_package.rootLevel'].load()
tpPythonClass = self.packageDict['test_package.PythonClass'].load()
mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel']
mRootLevel = mRLAttr.load()
mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass']
mPythonClass = mPCAttr.load()
expectedMachines = sorted(
[('test_package.rootLevel', tpRootLevel),
('test_package.PythonClass._machine', tpPythonClass._machine),
('test_package.subpackage.module.rootLevel', mRootLevel),
('test_package.subpackage.module.PythonClass._machine',
mPythonClass._machine)],
key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)
Automat-0.6.0/automat/_test/test_methodical.py 0000644 0001750 0001750 00000040105 13106675015 021101 0 ustar mrw mrw 0000000 0000000
"""
Tests for the public interface of Automat.
"""
from functools import reduce
from unittest import TestCase
from .. import MethodicalMachine, NoTransition
from .. import _methodical
class MethodicalTests(TestCase):
"""
Tests for L{MethodicalMachine}.
"""
def test_oneTransition(self):
"""
L{MethodicalMachine} provides a way for you to declare a state machine
with inputs, outputs, and states as methods. When you have declared an
input, an output, and a state, calling the input method in that state
will produce the specified output.
"""
class Machination(object):
machine = MethodicalMachine()
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
"an output"
return "an-output-value"
@machine.output()
def anotherOutput(self):
"another output"
return "another-output-value"
@machine.state(initial=True)
def anState(self):
"a state"
@machine.state()
def anotherState(self):
"another state"
anState.upon(anInput, enter=anotherState, outputs=[anOutput])
anotherState.upon(anInput, enter=anotherState,
outputs=[anotherOutput])
m = Machination()
self.assertEqual(m.anInput(), ["an-output-value"])
self.assertEqual(m.anInput(), ["another-output-value"])
def test_machineItselfIsPrivate(self):
"""
L{MethodicalMachine} is an implementation detail. If you attempt to
access it on an instance of your class, you will get an exception.
However, since tools may need to access it for the purposes of, for
example, visualization, you may access it on the class itself.
"""
expectedMachine = MethodicalMachine()
class Machination(object):
machine = expectedMachine
machination = Machination()
with self.assertRaises(AttributeError) as cm:
machination.machine
self.assertIn("MethodicalMachine is an implementation detail",
str(cm.exception))
self.assertIs(Machination.machine, expectedMachine)
def test_outputsArePrivate(self):
"""
One of the benefits of using a state machine is that your output method
implementations don't need to take invalid state transitions into
account - the methods simply won't be called. This property would be
broken if client code called output methods directly, so output methods
are not directly visible under their names.
"""
class Machination(object):
machine = MethodicalMachine()
counter = 0
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
self.counter += 1
@machine.state(initial=True)
def state(self):
"a machine state"
state.upon(anInput, enter=state, outputs=[anOutput])
mach1 = Machination()
mach1.anInput()
self.assertEqual(mach1.counter, 1)
mach2 = Machination()
with self.assertRaises(AttributeError) as cm:
mach2.anOutput
self.assertEqual(mach2.counter, 0)
self.assertIn(
"Machination.anOutput is a state-machine output method; to "
"produce this output, call an input method instead.",
str(cm.exception)
)
def test_multipleMachines(self):
"""
Two machines may co-exist happily on the same instance; they don't
interfere with each other.
"""
class MultiMach(object):
a = MethodicalMachine()
b = MethodicalMachine()
@a.input()
def inputA(self):
"input A"
@b.input()
def inputB(self):
"input B"
@a.state(initial=True)
def initialA(self):
"initial A"
@b.state(initial=True)
def initialB(self):
"initial B"
@a.output()
def outputA(self):
return "A"
@b.output()
def outputB(self):
return "B"
initialA.upon(inputA, initialA, [outputA])
initialB.upon(inputB, initialB, [outputB])
mm = MultiMach()
self.assertEqual(mm.inputA(), ["A"])
self.assertEqual(mm.inputB(), ["B"])
def test_collectOutputs(self):
"""
Outputs can be combined with the "collector" argument to "upon".
"""
import operator
class Machine(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.output()
def outputA(self):
return "A"
@m.output()
def outputB(self):
return "B"
@m.state(initial=True)
def state(self):
"a state"
state.upon(input, state, [outputA, outputB],
collector=lambda x: reduce(operator.add, x))
m = Machine()
self.assertEqual(m.input(), "AB")
def test_methodName(self):
"""
Input methods preserve their declared names.
"""
class Mech(object):
m = MethodicalMachine()
@m.input()
def declaredInputName(self):
"an input"
@m.state(initial=True)
def aState(self):
"state"
m = Mech()
with self.assertRaises(TypeError) as cm:
m.declaredInputName("too", "many", "arguments")
self.assertIn("declaredInputName", str(cm.exception))
def test_inputWithArguments(self):
"""
If an input takes an argument, it will pass that along to its output.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self, x, y=1):
"an input"
@m.state(initial=True)
def state(self):
"a state"
@m.output()
def output(self, x, y=1):
self._x = x
return x + y
state.upon(input, state, [output])
m = Mechanism()
self.assertEqual(m.input(3), [4])
self.assertEqual(m._x, 3)
def test_inputFunctionsMustBeEmpty(self):
"""
The wrapped input function must have an empty body.
"""
# input functions are executed to assert that the signature matches,
# but their body must be empty
_methodical._empty() # chase coverage
_methodical._docstring()
class Mechanism(object):
m = MethodicalMachine()
with self.assertRaises(ValueError) as cm:
@m.input()
def input(self):
"an input"
list() # pragma: no cover
self.assertEqual(str(cm.exception), "function body must be empty")
# all three of these cases should be valid. Functions/methods with
# docstrings produce slightly different bytecode than ones without.
class MechanismWithDocstring(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstring().input()
class MechanismWithPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithPass().input()
class MechanismWithDocstringAndPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndPass().input()
class MechanismReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismReturnsNone().input()
class MechanismWithDocstringAndReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndReturnsNone().input()
def test_inputOutputMismatch(self):
"""
All the argument lists of the outputs for a given input must match; if
one does not the call to C{upon} will raise a C{TypeError}.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def nameOfInput(self, a):
"an input"
@m.output()
def outputThatMatches(self, a):
"an output that matches"
@m.output()
def outputThatDoesntMatch(self, b):
"an output that doesn't match"
@m.state()
def state(self):
"a state"
with self.assertRaises(TypeError) as cm:
state.upon(nameOfInput, state, [outputThatMatches,
outputThatDoesntMatch])
self.assertIn("nameOfInput", str(cm.exception))
self.assertIn("outputThatDoesntMatch", str(cm.exception))
def test_multipleInitialStatesFailure(self):
"""
A L{MethodicalMachine} can only have one initial state.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def firstInitialState(self):
"The first initial state -- this is OK."
with self.assertRaises(ValueError):
@m.state(initial=True)
def secondInitialState(self):
"The second initial state -- results in a ValueError."
def test_multipleTransitionsFailure(self):
"""
A L{MethodicalMachine} can only have one transition per start/event
pair.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"We start here."
@m.state()
def end(self):
"Rainbows end."
@m.input()
def event(self):
"An event."
start.upon(event, enter=end, outputs=[])
with self.assertRaises(ValueError):
start.upon(event, enter=end, outputs=[])
def test_badTransitionForCurrentState(self):
"""
Calling any input method that lacks a transition for the machine's
current state raises an informative L{NoTransition}.
"""
class OnlyOnePath(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"Start state."
@m.state()
def end(self):
"End state."
@m.input()
def advance(self):
"Move from start to end."
@m.input()
def deadEnd(self):
"A transition from nowhere to nowhere."
start.upon(advance, end, [])
machine = OnlyOnePath()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("start", str(cm.exception))
machine.advance()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("end", str(cm.exception))
def test_saveState(self):
"""
L{MethodicalMachine.serializer} is a decorator that modifies its
decoratee's signature to take a "state" object as its first argument,
which is the "serialized" argument to the L{MethodicalMachine.state}
decorator.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.serializer()
def save(self, state):
return {
'machine-state': state,
'some-value': self.value,
}
self.assertEqual(
Mechanism().save(),
{
"machine-state": "first-state",
"some-value": 1,
}
)
def test_restoreState(self):
"""
L{MethodicalMachine.unserializer} decorates a function that becomes a
machine-state unserializer; its return value is mapped to the
C{serialized} parameter to C{state}, and the L{MethodicalMachine}
associated with that instance's state is updated to that state.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
self.ranOutput = False
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.input()
def input(self):
"an input"
@m.output()
def output(self):
self.value = 2
self.ranOutput = True
return 1
@m.output()
def output2(self):
return 2
first.upon(input, second, [output],
collector=lambda x: list(x)[0])
second.upon(input, second, [output2],
collector=lambda x: list(x)[0])
@m.serializer()
def save(self, state):
return {
'machine-state': state,
'some-value': self.value,
}
@m.unserializer()
def _restore(self, blob):
self.value = blob['some-value']
return blob['machine-state']
@classmethod
def fromBlob(cls, blob):
self = cls()
self._restore(blob)
return self
m1 = Mechanism()
m1.input()
blob = m1.save()
m2 = Mechanism.fromBlob(blob)
self.assertEqual(m2.ranOutput, False)
self.assertEqual(m2.input(), 2)
self.assertEqual(
m2.save(),
{
'machine-state': 'second-state',
'some-value': 2,
}
)
# FIXME: error for wrong types on any call to _oneTransition
# FIXME: better public API for .upon; maybe a context manager?
# FIXME: when transitions are defined, validate that we can always get to
# terminal? do we care about this?
# FIXME: implementation (and use-case/example) for passing args from in to out
# FIXME: possibly these need some kind of support from core
# FIXME: wildcard state (in all states, when input X, emit Y and go to Z)
# FIXME: wildcard input (in state X, when any input, emit Y and go to Z)
# FIXME: combined wildcards (in any state for any input, emit Y go to Z)
Automat-0.6.0/automat/_test/test_trace.py 0000644 0001750 0001750 00000006317 13106675015 020075 0 ustar mrw mrw 0000000 0000000 from unittest import TestCase
from .._methodical import MethodicalMachine
class SampleObject(object):
mm = MethodicalMachine()
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def middle(self):
"middle state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go1(self):
"sample input"
@mm.input()
def go2(self):
"sample input"
@mm.input()
def back(self):
"sample input"
@mm.output()
def out(self):
"sample output"
setTrace = mm._setTrace
begin.upon(go1, middle, [out])
middle.upon(go2, end, [out])
end.upon(back, middle, [])
middle.upon(back, begin, [])
class TraceTests(TestCase):
def test_only_inputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state))
return None # "I only care about inputs, not outputs"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(traces, [("begin", "go1", "middle"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
s.setTrace(None)
s.back()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
def test_inputs_and_outputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state, None))
def trace_outputs(output):
traces.append((old_state, input, new_state, output))
return trace_outputs # "I care about outputs too"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])
s.setTrace(None)
s.back()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])
Automat-0.6.0/automat/_test/test_visualize.py 0000644 0001750 0001750 00000032660 13072723343 021012 0 ustar mrw mrw 0000000 0000000 from __future__ import print_function
import functools
import os
import subprocess
from unittest import TestCase, skipIf
import attr
from .._methodical import MethodicalMachine
from .test_discover import isTwistedInstalled
def isGraphvizModuleInstalled():
"""
Is the graphviz Python module installed?
"""
try:
__import__("graphviz")
except ImportError:
return False
else:
return True
def isGraphvizInstalled():
"""
Are the graphviz tools installed?
"""
r, w = os.pipe()
os.close(w)
try:
return not subprocess.call("dot", stdin=r, shell=True)
finally:
os.close(r)
def sampleMachine():
"""
Create a sample L{MethodicalMachine} with some sample states.
"""
mm = MethodicalMachine()
class SampleObject(object):
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go(self):
"sample input"
@mm.output()
def out(self):
"sample output"
begin.upon(go, end, [out])
so = SampleObject()
so.go()
return mm
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class ElementMakerTests(TestCase):
"""
L{elementMaker} generates HTML representing the specified element.
"""
def setUp(self):
from .._visualize import elementMaker
self.elementMaker = elementMaker
def test_sortsAttrs(self):
"""
L{elementMaker} orders HTML attributes lexicographically.
"""
expected = r''
self.assertEqual(expected,
self.elementMaker("div",
b='2',
a='1',
c='3'))
def test_quotesAttrs(self):
"""
L{elementMaker} quotes HTML attributes according to DOT's quoting rule.
See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1.
"""
expected = r''
self.assertEqual(expected,
self.elementMaker("div",
b='a " quote',
a=1,
c="a string"))
def test_noAttrs(self):
"""
L{elementMaker} should render an element with no attributes.
"""
expected = r''
self.assertEqual(expected, self.elementMaker("div"))
@attr.s
class HTMLElement(object):
"""Holds an HTML element, as created by elementMaker."""
name = attr.ib()
children = attr.ib()
attributes = attr.ib()
def findElements(element, predicate):
"""
Recursively collect all elements in an L{HTMLElement} tree that
match the optional predicate.
"""
if predicate(element):
return [element]
elif isLeaf(element):
return []
return [result
for child in element.children
for result in findElements(child, predicate)]
def isLeaf(element):
"""
This HTML element is actually leaf node.
"""
return not isinstance(element, HTMLElement)
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class TableMakerTests(TestCase):
"""
Tests that ensure L{tableMaker} generates HTML tables usable as
labels in DOT graphs.
For more information, read the "HTML-Like Labels" section of
U{http://www.graphviz.org/doc/info/shapes.html}.
"""
def fakeElementMaker(self, name, *children, **attributes):
return HTMLElement(name=name, children=children, attributes=attributes)
def setUp(self):
from .._visualize import tableMaker
self.inputLabel = "input label"
self.port = "the port"
self.tableMaker = functools.partial(tableMaker,
_E=self.fakeElementMaker)
def test_inputLabelRow(self):
"""
The table returned by L{tableMaker} always contains the input
symbol label in its first row, and that row contains one cell
with a port attribute set to the provided port.
"""
def hasPort(element):
return (not isLeaf(element)
and element.attributes.get("port") == self.port)
for outputLabels in ([], ["an output label"]):
table = self.tableMaker(self.inputLabel, outputLabels,
port=self.port)
self.assertGreater(len(table.children), 0)
inputLabelRow = table.children[0]
portCandidates = findElements(table, hasPort)
self.assertEqual(len(portCandidates), 1)
self.assertEqual(portCandidates[0].name, "td")
self.assertEqual(findElements(inputLabelRow, isLeaf),
[self.inputLabel])
def test_noOutputLabels(self):
"""
L{tableMaker} does not add a colspan attribute to the input
label's cell or a second row if there no output labels.
"""
table = self.tableMaker("input label", (), port=self.port)
self.assertEqual(len(table.children), 1)
(inputLabelRow,) = table.children
self.assertNotIn("colspan", inputLabelRow.attributes)
def test_withOutputLabels(self):
"""
L{tableMaker} adds a colspan attribute to the input label's cell
equal to the number of output labels and a second row that
contains the output labels.
"""
table = self.tableMaker(self.inputLabel, ("output label 1",
"output label 2"),
port=self.port)
self.assertEqual(len(table.children), 2)
inputRow, outputRow = table.children
def hasCorrectColspan(element):
return (not isLeaf(element)
and element.name == "td"
and element.attributes.get('colspan') == "2")
self.assertEqual(len(findElements(inputRow, hasCorrectColspan)),
1)
self.assertEqual(findElements(outputRow, isLeaf), ["output label 1",
"output label 2"])
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
class IntegrationTests(TestCase):
"""
Tests which make sure Graphviz can understand the output produced by
Automat.
"""
def test_validGraphviz(self):
"""
L{graphviz} emits valid graphviz data.
"""
p = subprocess.Popen("dot", stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
out, err = p.communicate("".join(sampleMachine().asDigraph())
.encode("utf-8"))
self.assertEqual(p.returncode, 0)
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class SpotChecks(TestCase):
"""
Tests to make sure that the output contains salient features of the machine
being generated.
"""
def test_containsMachineFeatures(self):
"""
The output of L{graphviz} should contain the names of the states,
inputs, outputs in the state machine.
"""
gvout = "".join(sampleMachine().asDigraph())
self.assertIn("begin", gvout)
self.assertIn("end", gvout)
self.assertIn("go", gvout)
self.assertIn("out", gvout)
class RecordsDigraphActions(object):
"""
Records calls made to L{FakeDigraph}.
"""
def __init__(self):
self.reset()
def reset(self):
self.renderCalls = []
self.saveCalls = []
class FakeDigraph(object):
"""
A fake L{graphviz.Digraph}. Instantiate it with a
L{RecordsDigraphActions}.
"""
def __init__(self, recorder):
self._recorder = recorder
def render(self, **kwargs):
self._recorder.renderCalls.append(kwargs)
def save(self, **kwargs):
self._recorder.saveCalls.append(kwargs)
class FakeMethodicalMachine(object):
"""
A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph}
"""
def __init__(self, digraph):
self._digraph = digraph
def asDigraph(self):
return self._digraph
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class VisualizeToolTests(TestCase):
def setUp(self):
self.digraphRecorder = RecordsDigraphActions()
self.fakeDigraph = FakeDigraph(self.digraphRecorder)
self.fakeProgname = 'tool-test'
self.fakeSysPath = ['ignored']
self.collectedOutput = []
self.fakeFQPN = 'fake.fqpn'
def collectPrints(self, *args):
self.collectedOutput.append(' '.join(args))
def fakeFindMachines(self, fqpn):
yield fqpn, FakeMethodicalMachine(self.fakeDigraph)
def tool(self,
progname=None,
argv=None,
syspath=None,
findMachines=None,
print=None):
from .._visualize import tool
return tool(
_progname=progname or self.fakeProgname,
_argv=argv or [self.fakeFQPN],
_syspath=syspath or self.fakeSysPath,
_findMachines=findMachines or self.fakeFindMachines,
_print=print or self.collectPrints)
def test_checksCurrentDirectory(self):
"""
L{tool} adds '' to sys.path to ensure
L{automat._discover.findMachines} searches the current
directory.
"""
self.tool(argv=[self.fakeFQPN])
self.assertEqual(self.fakeSysPath[0], '')
def test_quietHidesOutput(self):
"""
Passing -q/--quiet hides all output.
"""
self.tool(argv=[self.fakeFQPN, '--quiet'])
self.assertFalse(self.collectedOutput)
self.tool(argv=[self.fakeFQPN, '-q'])
self.assertFalse(self.collectedOutput)
def test_onlySaveDot(self):
"""
Passing an empty string for --image-directory/-i disables
rendering images.
"""
for arg in ('--image-directory', '-i'):
self.digraphRecorder.reset()
self.collectedOutput = []
self.tool(argv=[self.fakeFQPN, arg, ''])
self.assertFalse(any("image" in line
for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
(call,) = self.digraphRecorder.saveCalls
self.assertEqual("{}.dot".format(self.fakeFQPN),
call['filename'])
self.assertFalse(self.digraphRecorder.renderCalls)
def test_saveOnlyImage(self):
"""
Passing an empty string for --dot-directory/-d disables saving dot
files.
"""
for arg in ('--dot-directory', '-d'):
self.digraphRecorder.reset()
self.collectedOutput = []
self.tool(argv=[self.fakeFQPN, arg, ''])
self.assertFalse(any("dot" in line
for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(call,) = self.digraphRecorder.renderCalls
self.assertEqual("{}.dot".format(self.fakeFQPN),
call['filename'])
self.assertTrue(call['cleanup'])
self.assertFalse(self.digraphRecorder.saveCalls)
def test_saveDotAndImagesInDifferentDirectories(self):
"""
Passing different directories to --image-directory and --dot-directory
writes images and dot files to those directories.
"""
imageDirectory = 'image'
dotDirectory = 'dot'
self.tool(argv=[self.fakeFQPN,
'--image-directory', imageDirectory,
'--dot-directory', dotDirectory])
self.assertTrue(any("image" in line
for line in self.collectedOutput))
self.assertTrue(any("dot" in line
for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(renderCall,) = self.digraphRecorder.renderCalls
self.assertEqual(renderCall["directory"], imageDirectory)
self.assertTrue(renderCall['cleanup'])
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
(saveCall,) = self.digraphRecorder.saveCalls
self.assertEqual(saveCall["directory"], dotDirectory)
def test_saveDotAndImagesInSameDirectory(self):
"""
Passing the same directory to --image-directory and --dot-directory
writes images and dot files to that one directory.
"""
directory = 'imagesAndDot'
self.tool(argv=[self.fakeFQPN,
'--image-directory', directory,
'--dot-directory', directory])
self.assertTrue(any("image and dot" in line
for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(renderCall,) = self.digraphRecorder.renderCalls
self.assertEqual(renderCall["directory"], directory)
self.assertFalse(renderCall['cleanup'])
self.assertFalse(len(self.digraphRecorder.saveCalls))
Automat-0.6.0/automat/_visualize.py 0000644 0001750 0001750 00000014277 13106701777 017005 0 ustar mrw mrw 0000000 0000000 from __future__ import print_function
import argparse
import sys
import graphviz
from ._discover import findMachines
def _gvquote(s):
return '"{}"'.format(s.replace('"', r'\"'))
def _gvhtml(s):
return '<{}>'.format(s)
def elementMaker(name, *children, **attrs):
"""
Construct a string from the HTML element description.
"""
formattedAttrs = ' '.join('{}={}'.format(key, _gvquote(str(value)))
for key, value in sorted(attrs.items()))
formattedChildren = ''.join(children)
return u'<{name} {attrs}>{children}{name}>'.format(
name=name,
attrs=formattedAttrs,
children=formattedChildren)
def tableMaker(inputLabel, outputLabels, port, _E=elementMaker):
"""
Construct an HTML table to label a state transition.
"""
colspan = {}
if outputLabels:
colspan['colspan'] = str(len(outputLabels))
inputLabelCell = _E("td",
_E("font",
inputLabel,
face="menlo-italic"),
color="purple",
port=port,
**colspan)
pointSize = {"point-size": "9"}
outputLabelCells = [_E("td",
_E("font",
outputLabel,
**pointSize),
color="pink")
for outputLabel in outputLabels]
rows = [_E("tr", inputLabelCell)]
if outputLabels:
rows.append(_E("tr", *outputLabelCells))
return _E("table", *rows)
def makeDigraph(automaton, inputAsString=repr,
outputAsString=repr,
stateAsString=repr):
"""
Produce a L{graphviz.Digraph} object from an automaton.
"""
digraph = graphviz.Digraph(graph_attr={'pack': 'true',
'dpi': '100'},
node_attr={'fontname': 'Menlo'},
edge_attr={'fontname': 'Menlo'})
for state in automaton.states():
if state is automaton.initialState:
stateShape = "bold"
fontName = "Menlo-Bold"
else:
stateShape = ""
fontName = "Menlo"
digraph.node(stateAsString(state),
fontame=fontName,
shape="ellipse",
style=stateShape,
color="blue")
for n, eachTransition in enumerate(automaton.allTransitions()):
inState, inputSymbol, outState, outputSymbols = eachTransition
thisTransition = "t{}".format(n)
inputLabel = inputAsString(inputSymbol)
port = "tableport"
table = tableMaker(inputLabel, [outputAsString(outputSymbol)
for outputSymbol in outputSymbols],
port=port)
digraph.node(thisTransition,
label=_gvhtml(table), margin="0.2", shape="none")
digraph.edge(stateAsString(inState),
'{}:{}:w'.format(thisTransition, port),
arrowhead="none")
digraph.edge('{}:{}:e'.format(thisTransition, port),
stateAsString(outState))
return digraph
def tool(_progname=sys.argv[0],
_argv=sys.argv[1:],
_syspath=sys.path,
_findMachines=findMachines,
_print=print):
"""
Entry point for command line utility.
"""
DESCRIPTION = """
Visualize automat.MethodicalMachines as graphviz graphs.
"""
EPILOG = """
You must have the graphviz tool suite installed. Please visit
http://www.graphviz.org for more information.
"""
if _syspath[0]:
_syspath.insert(0, '')
argumentParser = argparse.ArgumentParser(
prog=_progname,
description=DESCRIPTION,
epilog=EPILOG)
argumentParser.add_argument('fqpn',
help="A Fully Qualified Path name"
" representing where to find machines.")
argumentParser.add_argument('--quiet', '-q',
help="suppress output",
default=False,
action="store_true")
argumentParser.add_argument('--dot-directory', '-d',
help="Where to write out .dot files.",
default=".automat_visualize")
argumentParser.add_argument('--image-directory', '-i',
help="Where to write out image files.",
default=".automat_visualize")
argumentParser.add_argument('--image-type', '-t',
help="The image format.",
choices=graphviz.FORMATS,
default='png')
argumentParser.add_argument('--view', '-v',
help="View rendered graphs with"
" default image viewer",
default=False,
action="store_true")
args = argumentParser.parse_args(_argv)
explicitlySaveDot = (args.dot_directory
and (not args.image_directory
or args.image_directory != args.dot_directory))
if args.quiet:
def _print(*args):
pass
for fqpn, machine in _findMachines(args.fqpn):
_print(fqpn, '...discovered')
digraph = machine.asDigraph()
if explicitlySaveDot:
digraph.save(filename="{}.dot".format(fqpn),
directory=args.dot_directory)
_print(fqpn, "...wrote dot into", args.dot_directory)
if args.image_directory:
deleteDot = not args.dot_directory or explicitlySaveDot
digraph.format = args.image_type
digraph.render(filename="{}.dot".format(fqpn),
directory=args.image_directory,
view=args.view,
cleanup=deleteDot)
if deleteDot:
msg = "...wrote image into"
else:
msg = "...wrote image and dot into"
_print(fqpn, msg, args.image_directory)
Automat-0.6.0/docs/ 0000755 0001750 0001750 00000000000 13106704143 013512 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/docs/debugging.md 0000644 0001750 0001750 00000012050 13106675015 015772 0 ustar mrw mrw 0000000 0000000 # Tracing API
(NOTE: the Tracing API is currently private and unstable. Use it for
local debugging, but if you think you need to commit code that
references it, you should either pin your dependency on the current
version of Automat, or at least be prepared for your application to
break when this API is changed or removed).
The tracing API lets you assign a callback function that will be invoked each
time an input event causes the state machine to move from one state to
another. This can help you figure out problems caused by events occurring in
the wrong order, or not happening at all. Your callback function can print a
message to stdout, write something to a logfile, or deliver the information
in any application-specific way you like. The only restriction is that the
function must not touch the state machine at all.
To prepare the state machine for tracing, you must assign a name to the
"_setTrace" method in your class. In this example, we use
`setTheTracingFunction`, but the name can be anything you like:
```python
class Sample(object):
mm = MethodicalMachine()
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go(self):
"event that moves us from begin to end"
@mm.output()
def doThing1(self):
"first thing to do"
@mm.output()
def doThing2(self):
"second thing to do"
setTheTracingFunction = mm._setTrace
begin.upon(go, enter=end, outputs=[doThing1, doThing2])
```
Later, after you instantiate the `Sample` object, you can set the tracing
callback for that particular instance by calling the
`setTheTracingFunction()` method on it:
```python
s = Sample()
def tracer(oldState, input, newState):
pass
s.setTheTracingFunction(tracer)
```
Note that you cannot shortcut the name-assignment step:
`s.mm._setTrace(tracer)` will not work, because Automat goes to great
lengths to hide that `mm` object from external access. And you cannot
set the tracing function at class-definition time (e.g. a class-level
`mm._setTrace(tracer)`) because the state machine has merely been
*defined* at that point, not instantiated (you might eventually have
multiple instances of the Sample class, each with their own independent
state machine), and each one can be traced separately.
Since this is a private API, consider using a tolerant `getattr` when
retrieving the `_getTrace` method. This way, if you do commit code which
references it, but you only *call* that code during debugging, then at
least your application or tests won't crash when the API is removed
entirely:
```
mm = MethodicalMachine()
setTheTracingFunction = getattr(mm, "_setTrace", lambda self, f: None)
```
## The Tracer Callback Function
When the input event is received, before any transitions are made, the tracer
function is called with three positional arguments:
* `oldState`: a string with the name of the current state
* `input`: a string with the name of the input event
* `newState`: a string with the name of the new state
If your tracer function returns None, then you will only be notified about
the input events. But, if your tracer function returns a callable, then just
before each output function is executed (if any), that callable will be
executed with a single `output` argument (as a string).
So if you only care about the transitions, your tracing function can just do:
```python
s = Sample()
def tracer(oldState, input, newState):
print("%s.%s -> %s" % (oldState, input, newState))
s.setTheTracingFunction(tracer)
s.go()
# prints:
# begin.go -> end
```
But if you want to know when each output is invoked (perhaps to compare
against other log messages emitted from inside those output functions), you
can do:
```python
s = Sample()
def tracer(oldState, input, newState):
def traceOutputs(output):
print("%s.%s -> %s: %s()" % (oldState, input, newState, output))
print("%s.%s -> %s" % (oldState, input, newState))
return traceOutputs
s.setTheTracingFunction(tracer)
s.go()
# prints:
# begin.go -> end
# begin.go -> end: doThing1()
# begin.go -> end: doThing2()
```
## Tracing Multiple State Machines
If you have multiple state machines in your application, you will probably
want to pass a different tracing function to each, so your logs can
distinguish between the transitions of MachineFoo vs those of MachineBar.
This is particularly important if your application involves network
communication, where an instance of MachineFoo (e.g. in a client) is
communication with a sibling instance of MachineFoo (in a server). When
exercising both sides of this connection in a single process, perhaps in an
automated test, you will need to clearly mark the first as "foo1" and the
second as "foo2" to avoid confusion.
```python
s1 = Sample()
s2 = Sample()
def tracer1(oldState, input, newState):
print("S1: %s.%s -> %s" % (oldState, input, newState))
s1.setTheTracingFunction(tracer1)
def tracer2(oldState, input, newState):
print("S2: %s.%s -> %s" % (oldState, input, newState))
s2.setTheTracingFunction(tracer2)
```
Automat-0.6.0/docs/examples/ 0000755 0001750 0001750 00000000000 13106704143 015330 5 ustar mrw mrw 0000000 0000000 Automat-0.6.0/docs/examples/automat_example.py 0000644 0001750 0001750 00000006502 13072723343 021077 0 ustar mrw mrw 0000000 0000000
from automat import MethodicalMachine
class Door(object):
def unlock(self):
print("Opening the door so you can get your food.")
def lock(self):
print("Locking the door so you can't steal the food.")
class Light(object):
def on(self):
print("Need some food over here.")
def off(self):
print("We're good on food for now.")
class FoodSlot(object):
"""
Automats were a popular kind of business in the 1950s and 60s; a sort of
restaurant-sized vending machine that served cooked food out of a
coin-operated dispenser.
This class represents the logic associated with a single food slot.
"""
machine = MethodicalMachine()
def __init__(self, door, light):
self._door = door
self._light = light
self.start()
@machine.state(initial=True)
def initial(self):
"""
The initial state when we are constructed.
Note that applications never see this state, because the constructor
provides an input to transition out of it immediately.
"""
@machine.state()
def empty(self):
"""
The machine is empty (and the light asking for food is on).
"""
@machine.input()
def start(self):
"""
A private input, for transitioning to the initial blank state to
'empty', making sure the door and light are properly configured.
"""
@machine.state()
def ready(self):
"""
We've got some food and we're ready to serve it.
"""
@machine.state()
def serving(self):
"""
The door is open, we're serving food.
"""
@machine.input()
def coin(self):
"""
A coin (of the appropriate denomination) was inserted.
"""
@machine.input()
def food(self):
"""
Food was prepared and inserted into the back of the machine.
"""
@machine.output()
def turnOnFoodLight(self):
"""
Turn on the 'we need food' light.
"""
self._light.on()
@machine.output()
def turnOffFoodLight(self):
"""
Turn off the 'we need food' light.
"""
self._light.off()
@machine.output()
def lockDoor(self):
"""
Lock the door, we don't need food.
"""
self._door.lock()
@machine.output()
def unlockDoor(self):
"""
Lock the door, we don't need food.
"""
self._door.unlock()
@machine.input()
def closeDoor(self):
"""
The door was closed.
"""
initial.upon(start, enter=empty, outputs=[lockDoor, turnOnFoodLight])
empty.upon(food, enter=ready, outputs=[turnOffFoodLight])
ready.upon(coin, enter=serving, outputs=[unlockDoor])
serving.upon(closeDoor, enter=empty, outputs=[lockDoor,
turnOnFoodLight])
slot = FoodSlot(Door(), Light())
if __name__ == '__main__':
import sys
sys.stdout.writelines(FoodSlot.machine.asDigraph())
# raw_input("Hit enter to make some food and put it in the slot: ")
# slot.food()
# raw_input("Hit enter to insert a coin: ")
# slot.coin()
# raw_input("Hit enter to retrieve the food and close the door: ")
# slot.closeDoor()
# raw_input("Hit enter to make some more food: ")
# slot.food()
Automat-0.6.0/docs/examples/io_coffee_example.py 0000644 0001750 0001750 00000002524 12641273151 021341 0 ustar mrw mrw 0000000 0000000 from automat import MethodicalMachine
class CoffeeBrewer(object):
_machine = MethodicalMachine()
@_machine.input()
def brew_button(self):
"The user pressed the 'brew' button."
@_machine.output()
def _heat_the_heating_element(self):
"Heat up the heating element, which should cause coffee to happen."
# self._heating_element.turn_on()
@_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."
@_machine.input()
def put_in_beans(self, beans):
"The user put in some beans."
@_machine.output()
def _save_beans(self, beans):
"The beans are now in the machine; save them."
self._beans = beans
@_machine.output()
def _describe_coffee(self):
return "A cup of coffee made with {}.".format(self._beans)
dont_have_beans.upon(put_in_beans, enter=have_beans,
outputs=[_save_beans])
have_beans.upon(brew_button, enter=dont_have_beans,
outputs=[_heat_the_heating_element,
_describe_coffee],
collector=lambda iterable: list(iterable)[-1]
)
cb = CoffeeBrewer()
cb.put_in_beans("real good beans")
print(cb.brew_button())
Automat-0.6.0/docs/examples/lightswitch.py 0000644 0001750 0001750 00000002270 12641273151 020237 0 ustar mrw mrw 0000000 0000000 from automat import MethodicalMachine
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=[])
@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)
@machine.serializer()
def save(self, state):
return {"is-it-on": state}
@machine.unserializer()
def _restore(self, blob):
return blob["is-it-on"]
@classmethod
def from_blob(cls, blob):
self = cls()
self._restore(blob)
return self
Automat-0.6.0/docs/examples/turnstile_example.py 0000644 0001750 0001750 00000002312 12641273151 021447 0 ustar mrw mrw 0000000 0000000 from automat import MethodicalMachine
class Turnstile(object):
machine = MethodicalMachine()
def __init__(self, lock):
self.lock = lock
@machine.input()
def arm_turned(self):
"The arm was turned."
@machine.input()
def fare_paid(self):
"The fare was paid."
@machine.output()
def _engage_lock(self):
self.lock.engage()
@machine.output()
def _disengage_lock(self):
self.lock.disengage()
@machine.output()
def _nope(self):
print("**Clunk!** The turnstile doesn't move.")
@machine.state(initial=True)
def _locked(self):
"The turnstile is locked."
@machine.state()
def _unlocked(self):
"The turnstile is unlocked."
_locked.upon(fare_paid, enter=_unlocked, outputs=[_disengage_lock])
_unlocked.upon(arm_turned, enter=_locked, outputs=[_engage_lock])
_locked.upon(arm_turned, enter=_locked, outputs=[_nope])
class Lock(object):
"A sample I/O device."
def engage(self):
print("Locked.")
def disengage(self):
print("Unlocked.")
turner = Turnstile(Lock())
turner.fare_paid()
turner.arm_turned()
turner.arm_turned()
turner.fare_paid()
turner.arm_turned()
Automat-0.6.0/setup.cfg 0000644 0001750 0001750 00000000130 13106704143 014375 0 ustar mrw mrw 0000000 0000000 [bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
Automat-0.6.0/setup.py 0000644 0001750 0001750 00000002320 13106676046 014303 0 ustar mrw mrw 0000000 0000000 """
Setup file for automat
"""
from setuptools import setup, find_packages
try:
from m2r import parse_from_file
long_description = parse_from_file('README.md')
except(IOError, ImportError):
print("\n\n!!! m2r not found, long_description is bad, don't upload this to PyPI !!!\n\n")
import io
long_description = io.open('README.md', encoding="utf-8").read()
setup(
name='Automat',
use_scm_version=True,
url='https://github.com/glyph/Automat',
description="""
Self-service finite-state machines for the programmer on the go.
""".strip(),
long_description=long_description,
packages=find_packages(exclude=[]),
package_dir={'automat': 'automat'},
setup_requires=[
'setuptools-scm',
'm2r',
],
install_requires=[
"attrs",
"six",
],
extras_require={
"visualize": ["graphviz>0.5.1",
"Twisted>=16.1.1"],
},
entry_points={
"console_scripts": [
"automat-visualize = automat._visualize:tool"
],
},
author='Glyph',
author_email='glyph@twistedmatrix.com',
include_package_data=True,
license="MIT",
keywords='fsm finite state machine automata',
)
Automat-0.6.0/tox.ini 0000644 0001750 0001750 00000000743 13106675015 014106 0 ustar mrw mrw 0000000 0000000 [tox]
envlist = coverage-clean,{py27,pypy,py33,py34,py35,py36}-{extras,noextras},coverage-report
[testenv]
deps =
extras: graphviz>=0.4.9
extras: Twisted>=16.2.0
coverage
pytest
commands =
coverage run --parallel --source automat -m py.test automat/_test
[testenv:coverage-clean]
deps = coverage
skip_install = true
commands = coverage erase
[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
coverage combine
coverage report -m