aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Automat/py2
diff options
context:
space:
mode:
authorshmel1k <shmel1k@ydb.tech>2023-11-26 18:16:14 +0300
committershmel1k <shmel1k@ydb.tech>2023-11-26 18:43:30 +0300
commitb8cf9e88f4c5c64d9406af533d8948deb050d695 (patch)
tree218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Automat/py2
parent523f645a83a0ec97a0332dbc3863bb354c92a328 (diff)
downloadydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz
add kikimr_configure
Diffstat (limited to 'contrib/python/Automat/py2')
-rw-r--r--contrib/python/Automat/py2/.dist-info/METADATA487
-rw-r--r--contrib/python/Automat/py2/.dist-info/entry_points.txt3
-rw-r--r--contrib/python/Automat/py2/.dist-info/top_level.txt1
-rw-r--r--contrib/python/Automat/py2/LICENSE21
-rw-r--r--contrib/python/Automat/py2/README.md430
-rw-r--r--contrib/python/Automat/py2/automat/__init__.py8
-rw-r--r--contrib/python/Automat/py2/automat/_core.py165
-rw-r--r--contrib/python/Automat/py2/automat/_discover.py144
-rw-r--r--contrib/python/Automat/py2/automat/_introspection.py45
-rw-r--r--contrib/python/Automat/py2/automat/_methodical.py474
-rw-r--r--contrib/python/Automat/py2/automat/_visualize.py182
-rw-r--r--contrib/python/Automat/py2/ya.make38
12 files changed, 1998 insertions, 0 deletions
diff --git a/contrib/python/Automat/py2/.dist-info/METADATA b/contrib/python/Automat/py2/.dist-info/METADATA
new file mode 100644
index 0000000000..8b4c9cecf7
--- /dev/null
+++ b/contrib/python/Automat/py2/.dist-info/METADATA
@@ -0,0 +1,487 @@
+Metadata-Version: 2.1
+Name: Automat
+Version: 20.2.0
+Summary: Self-service finite-state machines for the programmer on the go.
+Home-page: https://github.com/glyph/Automat
+Author: Glyph
+Author-email: glyph@twistedmatrix.com
+License: MIT
+Keywords: fsm finite state machine automata
+Platform: UNKNOWN
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Requires-Dist: attrs (>=19.2.0)
+Requires-Dist: six
+Provides-Extra: visualize
+Requires-Dist: graphviz (>0.5.1); extra == 'visualize'
+Requires-Dist: Twisted (>=16.1.1); extra == 'visualize'
+
+
+Automat
+=======
+
+
+.. image:: https://readthedocs.org/projects/automat/badge/?version=latest
+ :target: http://automat.readthedocs.io/en/latest/
+ :alt: Documentation Status
+
+
+.. image:: https://travis-ci.org/glyph/automat.svg?branch=master
+ :target: https://travis-ci.org/glyph/automat
+ :alt: Build Status
+
+
+.. image:: https://coveralls.io/repos/glyph/automat/badge.png
+ :target: https://coveralls.io/r/glyph/automat
+ :alt: Coverage Status
+
+
+Self-service finite-state machines for the programmer on the go.
+----------------------------------------------------------------
+
+Automat is a library for concise, idiomatic Python expression of finite-state
+automata (particularly deterministic finite-state transducers).
+
+Read more here, or on `Read the Docs <https://automat.readthedocs.io/>`_\ , or watch the following videos for an overview and presentation
+
+Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017:
+
+.. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg
+ :target: https://www.youtube.com/watch?v=0wOZBpD1VVk
+ :alt: Glyph Lefkowitz - Automat - Pyninsula #0
+
+
+Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017:
+
+.. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg
+ :target: https://www.youtube.com/watch?v=TedUKXhu9kE
+ :alt: Clinton Roy - State Machines - Pycon Australia 2017
+
+
+Why use state machines?
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Sometimes you have to create an object whose behavior varies with its state,
+but still wishes to present a consistent interface to its callers.
+
+For example, let's say you're writing the software for a coffee machine. It
+has a lid that can be opened or closed, a chamber for water, a chamber for
+coffee beans, and a button for "brew".
+
+There are a number of possible states for the coffee machine. It might or
+might not have water. It might or might not have beans. The lid might be open
+or closed. The "brew" button should only actually attempt to brew coffee in
+one of these configurations, and the "open lid" button should only work if the
+coffee is not, in fact, brewing.
+
+With diligence and attention to detail, you can implement this correctly using
+a collection of attributes on an object; ``has_water``\ , ``has_beans``\ ,
+``is_lid_open`` and so on. However, you have to keep all these attributes
+consistent. As the coffee maker becomes more complex - perhaps you add an
+additional chamber for flavorings so you can make hazelnut coffee, for
+example - you have to keep adding more and more checks and more and more
+reasoning about which combinations of states are allowed.
+
+Rather than adding tedious 'if' checks to every single method to make sure that
+each of these flags are exactly what you expect, you can use a state machine to
+ensure that if your code runs at all, it will be run with all the required
+values initialized, because they have to be called in the order you declare
+them.
+
+You can read about state machines and their advantages for Python programmers
+in considerably more detail
+`in this excellent series of articles from ClusterHQ <https://clusterhq.com/blog/what-is-a-state-machine/>`_.
+
+What makes Automat different?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+There are
+`dozens of libraries on PyPI implementing state machines <https://pypi.org/search/?q=finite+state+machine>`_.
+So it behooves me to say why yet another one would be a good idea.
+
+Automat is designed around this principle: while organizing your code around
+state machines is a good idea, your callers don't, and shouldn't have to, care
+that you've done so. In Python, the "input" to a stateful system is a method
+call; the "output" may be a method call, if you need to invoke a side effect,
+or a return value, if you are just performing a computation in memory. Most
+other state-machine libraries require you to explicitly create an input object,
+provide that object to a generic "input" method, and then receive results,
+sometimes in terms of that library's interfaces and sometimes in terms of
+classes you define yourself.
+
+For example, a snippet of the coffee-machine example above might be implemented
+as follows in naive Python:
+
+.. code-block:: python
+
+ class CoffeeMachine(object):
+ def brew_button(self):
+ if self.has_water and self.has_beans and not self.is_lid_open:
+ self.heat_the_heating_element()
+ # ...
+
+With Automat, you'd create a class with a ``MethodicalMachine`` attribute:
+
+.. code-block:: python
+
+ from automat import MethodicalMachine
+
+ class CoffeeBrewer(object):
+ _machine = MethodicalMachine()
+
+and then you would break the above logic into two pieces - the ``brew_button``
+*input*\ , declared like so:
+
+.. code-block:: python
+
+ @_machine.input()
+ def brew_button(self):
+ "The user pressed the 'brew' button."
+
+It wouldn't do any good to declare a method *body* on this, however, because
+input methods don't actually execute their bodies when called; doing actual
+work is the *output*\ 's job:
+
+.. code-block:: python
+
+ @_machine.output()
+ def _heat_the_heating_element(self):
+ "Heat up the heating element, which should cause coffee to happen."
+ self._heating_element.turn_on()
+
+As well as a couple of *states* - and for simplicity's sake let's say that the
+only two states are ``have_beans`` and ``dont_have_beans``\ :
+
+.. code-block:: python
+
+ @_machine.state()
+ def have_beans(self):
+ "In this state, you have some beans."
+ @_machine.state(initial=True)
+ def dont_have_beans(self):
+ "In this state, you don't have any beans."
+
+``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans
+in it.
+
+(And another input to put some beans in:)
+
+.. code-block:: python
+
+ @_machine.input()
+ def put_in_beans(self):
+ "The user put in some beans."
+
+Finally, you hook everything together with the ``upon`` method of the functions
+decorated with ``_machine.state``\ :
+
+.. code-block:: python
+
+
+ # When we don't have beans, upon putting in beans, we will then have beans
+ # (and produce no output)
+ dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[])
+
+ # When we have beans, upon pressing the brew button, we will then not have
+ # beans any more (as they have been entered into the brewing chamber) and
+ # our output will be heating the heating element.
+ have_beans.upon(brew_button, enter=dont_have_beans,
+ outputs=[_heat_the_heating_element])
+
+To *users* of this coffee machine class though, it still looks like a POPO
+(Plain Old Python Object):
+
+.. code-block:: python
+
+ >>> coffee_machine = CoffeeMachine()
+ >>> coffee_machine.put_in_beans()
+ >>> coffee_machine.brew_button()
+
+All of the *inputs* are provided by calling them like methods, all of the
+*outputs* are automatically invoked when they are produced according to the
+outputs specified to ``upon`` and all of the states are simply opaque tokens -
+although the fact that they're defined as methods like inputs and outputs
+allows you to put docstrings on them easily to document them.
+
+How do I get the current state of a state machine?
+--------------------------------------------------
+
+Don't do that.
+
+One major reason for having a state machine is that you want the callers of the
+state machine to just provide the appropriate input to the machine at the
+appropriate time, and *not have to check themselves* what state the machine is
+in. So if you are tempted to write some code like this:
+
+.. code-block:: python
+
+ if connection_state_machine.state == "CONNECTED":
+ connection_state_machine.send_message()
+ else:
+ print("not connected")
+
+Instead, just make your calling code do this:
+
+.. code-block:: python
+
+ connection_state_machine.send_message()
+
+and then change your state machine to look like this:
+
+.. code-block:: python
+
+ @_machine.state()
+ def connected(self):
+ "connected"
+ @_machine.state()
+ def not_connected(self):
+ "not connected"
+ @_machine.input()
+ def send_message(self):
+ "send a message"
+ @_machine.output()
+ def _actually_send_message(self):
+ self._transport.send(b"message")
+ @_machine.output()
+ def _report_sending_failure(self):
+ print("not connected")
+ connected.upon(send_message, enter=connected, [_actually_send_message])
+ not_connected.upon(send_message, enter=not_connected, [_report_sending_failure])
+
+so that the responsibility for knowing which state the state machine is in
+remains within the state machine itself.
+
+Input for Inputs and Output for Outputs
+---------------------------------------
+
+Quite often you want to be able to pass parameters to your methods, as well as
+inspecting their results. For example, when you brew the coffee, you might
+expect a cup of coffee to result, and you would like to see what kind of coffee
+it is. And if you were to put delicious hand-roasted small-batch artisanal
+beans into the machine, you would expect a *better* cup of coffee than if you
+were to use mass-produced beans. You would do this in plain old Python by
+adding a parameter, so that's how you do it in Automat as well.
+
+.. code-block:: python
+
+ @_machine.input()
+ def put_in_beans(self, beans):
+ "The user put in some beans."
+
+However, one important difference here is that *we can't add any
+implementation code to the input method*. Inputs are purely a declaration of
+the interface; the behavior must all come from outputs. Therefore, the change
+in the state of the coffee machine must be represented as an output. We can
+add an output method like this:
+
+.. code-block:: python
+
+ @_machine.output()
+ def _save_beans(self, beans):
+ "The beans are now in the machine; save them."
+ self._beans = beans
+
+and then connect it to the ``put_in_beans`` by changing the transition from
+``dont_have_beans`` to ``have_beans`` like so:
+
+.. code-block:: python
+
+ dont_have_beans.upon(put_in_beans, enter=have_beans,
+ outputs=[_save_beans])
+
+Now, when you call:
+
+.. code-block:: python
+
+ coffee_machine.put_in_beans("real good beans")
+
+the machine will remember the beans for later.
+
+So how do we get the beans back out again? One of our outputs needs to have a
+return value. It would make sense if our ``brew_button`` method returned the cup
+of coffee that it made, so we should add an output. So, in addition to heating
+the heating element, let's add a return value that describes the coffee. First
+a new output:
+
+.. code-block:: python
+
+ @_machine.output()
+ def _describe_coffee(self):
+ return "A cup of coffee made with {}.".format(self._beans)
+
+Note that we don't need to check first whether ``self._beans`` exists or not,
+because we can only reach this output method if the state machine says we've
+gone through a set of states that sets this attribute.
+
+Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change
+the brewing transition to:
+
+.. code-block:: python
+
+ have_beans.upon(brew_button, enter=dont_have_beans,
+ outputs=[_heat_the_heating_element,
+ _describe_coffee])
+
+Now, we can call it:
+
+.. code-block:: python
+
+ >>> coffee_machine.brew_button()
+ [None, 'A cup of coffee made with real good beans.']
+
+Except... wait a second, what's that ``None`` doing there?
+
+Since every input can produce multiple outputs, in automat, the default return
+value from every input invocation is a ``list``. In this case, we have both
+``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing
+both of their return values. However, this can be customized, with the
+``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an
+iterable of all the outputs' return values and "collects" a single return value
+to return to the caller of the state machine.
+
+In this case, we only care about the last output, so we can adjust the call to
+``upon`` like this:
+
+.. code-block:: python
+
+ have_beans.upon(brew_button, enter=dont_have_beans,
+ outputs=[_heat_the_heating_element,
+ _describe_coffee],
+ collector=lambda iterable: list(iterable)[-1]
+ )
+
+And now, we'll get just the return value we want:
+
+.. code-block:: python
+
+ >>> coffee_machine.brew_button()
+ 'A cup of coffee made with real good beans.'
+
+If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...)
+--------------------------------------------------------------------------------------------------------------------
+
+There are APIs for serializing the state machine.
+
+First, you have to decide on a persistent representation of each state, via the
+``serialized=`` argument to the ``MethodicalMachine.state()`` decorator.
+
+Let's take this very simple "light switch" state machine, which can be on or
+off, and flipped to reverse its state:
+
+.. code-block:: python
+
+ class LightSwitch(object):
+ _machine = MethodicalMachine()
+ @_machine.state(serialized="on")
+ def on_state(self):
+ "the switch is on"
+ @_machine.state(serialized="off", initial=True)
+ def off_state(self):
+ "the switch is off"
+ @_machine.input()
+ def flip(self):
+ "flip the switch"
+ on_state.upon(flip, enter=off_state, outputs=[])
+ off_state.upon(flip, enter=on_state, outputs=[])
+
+In this case, we've chosen a serialized representation for each state via the
+``serialized`` argument. The on state is represented by the string ``"on"``\ , and
+the off state is represented by the string ``"off"``.
+
+Now, let's just add an input that lets us tell if the switch is on or not.
+
+.. code-block:: python
+
+ @_machine.input()
+ def query_power(self):
+ "return True if powered, False otherwise"
+ @_machine.output()
+ def _is_powered(self):
+ return True
+ @_machine.output()
+ def _not_powered(self):
+ return False
+ on_state.upon(query_power, enter=on_state, outputs=[_is_powered],
+ collector=next)
+ off_state.upon(query_power, enter=off_state, outputs=[_not_powered],
+ collector=next)
+
+To save the state, we have the ``MethodicalMachine.serializer()`` method. A
+method decorated with ``@serializer()`` gets an extra argument injected at the
+beginning of its argument list: the serialized identifier for the state. In
+this case, either ``"on"`` or ``"off"``. Since state machine output methods can
+also affect other state on the object, a serializer method is expected to
+return *all* relevant state for serialization.
+
+For our simple light switch, such a method might look like this:
+
+.. code-block:: python
+
+ @_machine.serializer()
+ def save(self, state):
+ return {"is-it-on": state}
+
+Serializers can be public methods, and they can return whatever you like. If
+necessary, you can have different serializers - just multiple methods decorated
+with ``@_machine.serializer()`` - for different formats; return one data-structure
+for JSON, one for XML, one for a database row, and so on.
+
+When it comes time to unserialize, though, you generally want a private method,
+because an unserializer has to take a not-fully-initialized instance and
+populate it with state. It is expected to *return* the serialized machine
+state token that was passed to the serializer, but it can take whatever
+arguments you like. Of course, in order to return that, it probably has to
+take it somewhere in its arguments, so it will generally take whatever a paired
+serializer has returned as an argument.
+
+So our unserializer would look like this:
+
+.. code-block:: python
+
+ @_machine.unserializer()
+ def _restore(self, blob):
+ return blob["is-it-on"]
+
+Generally you will want a classmethod deserialization constructor which you
+write yourself to call this, so that you know how to create an instance of your
+own object, like so:
+
+.. code-block:: python
+
+ @classmethod
+ def from_blob(cls, blob):
+ self = cls()
+ self._restore(blob)
+ return self
+
+Saving and loading our ``LightSwitch`` along with its state-machine state can now
+be accomplished as follows:
+
+.. code-block:: python
+
+ >>> switch1 = LightSwitch()
+ >>> switch1.query_power()
+ False
+ >>> switch1.flip()
+ []
+ >>> switch1.query_power()
+ True
+ >>> blob = switch1.save()
+ >>> switch2 = LightSwitch.from_blob(blob)
+ >>> switch2.query_power()
+ True
+
+More comprehensive (tested, working) examples are present in ``docs/examples``.
+
+Go forth and machine all the state!
+
+
diff --git a/contrib/python/Automat/py2/.dist-info/entry_points.txt b/contrib/python/Automat/py2/.dist-info/entry_points.txt
new file mode 100644
index 0000000000..d79319995c
--- /dev/null
+++ b/contrib/python/Automat/py2/.dist-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+automat-visualize = automat._visualize:tool
+
diff --git a/contrib/python/Automat/py2/.dist-info/top_level.txt b/contrib/python/Automat/py2/.dist-info/top_level.txt
new file mode 100644
index 0000000000..b69387ba18
--- /dev/null
+++ b/contrib/python/Automat/py2/.dist-info/top_level.txt
@@ -0,0 +1 @@
+automat
diff --git a/contrib/python/Automat/py2/LICENSE b/contrib/python/Automat/py2/LICENSE
new file mode 100644
index 0000000000..9773501b17
--- /dev/null
+++ b/contrib/python/Automat/py2/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2014
+Rackspace
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/contrib/python/Automat/py2/README.md b/contrib/python/Automat/py2/README.md
new file mode 100644
index 0000000000..d830ee6621
--- /dev/null
+++ b/contrib/python/Automat/py2/README.md
@@ -0,0 +1,430 @@
+# Automat #
+
+[![Documentation Status](https://readthedocs.org/projects/automat/badge/?version=latest)](http://automat.readthedocs.io/en/latest/)
+[![Build Status](https://travis-ci.org/glyph/automat.svg?branch=master)](https://travis-ci.org/glyph/automat)
+[![Coverage Status](https://coveralls.io/repos/glyph/automat/badge.png)](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).
+
+Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation
+
+Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017:
+[![Glyph Lefkowitz - Automat - Pyninsula #0](https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg)](https://www.youtube.com/watch?v=0wOZBpD1VVk)
+
+Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017:
+[![Clinton Roy - State Machines - Pycon Australia 2017](https://img.youtube.com/vi/TedUKXhu9kE/0.jpg)](https://www.youtube.com/watch?v=TedUKXhu9kE)
+
+### 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."
+```
+
+`dont_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!
diff --git a/contrib/python/Automat/py2/automat/__init__.py b/contrib/python/Automat/py2/automat/__init__.py
new file mode 100644
index 0000000000..570b84f995
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/__init__.py
@@ -0,0 +1,8 @@
+# -*- test-case-name: automat -*-
+from ._methodical import MethodicalMachine
+from ._core import NoTransition
+
+__all__ = [
+ 'MethodicalMachine',
+ 'NoTransition',
+]
diff --git a/contrib/python/Automat/py2/automat/_core.py b/contrib/python/Automat/py2/automat/_core.py
new file mode 100644
index 0000000000..4118a4b070
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/_core.py
@@ -0,0 +1,165 @@
+# -*- 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 = "<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 {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)
diff --git a/contrib/python/Automat/py2/automat/_discover.py b/contrib/python/Automat/py2/automat/_discover.py
new file mode 100644
index 0000000000..c0d88baea4
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/_discover.py
@@ -0,0 +1,144 @@
+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))
diff --git a/contrib/python/Automat/py2/automat/_introspection.py b/contrib/python/Automat/py2/automat/_introspection.py
new file mode 100644
index 0000000000..3f7307d8df
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/_introspection.py
@@ -0,0 +1,45 @@
+"""
+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 hasattr(code, "co_kwonlyargcount"):
+ names.insert(1, "kwonlyargcount")
+ if hasattr(code, "co_posonlyargcount"):
+ # PEP 570 added "positional only arguments"
+ names.insert(1, "posonlyargcount")
+ 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
diff --git a/contrib/python/Automat/py2/automat/_methodical.py b/contrib/python/Automat/py2/automat/_methodical.py
new file mode 100644
index 0000000000..84fcd362a6
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/_methodical.py
@@ -0,0 +1,474 @@
+# -*- test-case-name: automat._test.test_methodical -*-
+
+import collections
+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
+import six
+
+from ._core import Transitioner, Automaton
+from ._introspection import preserveName
+
+
+ArgSpec = collections.namedtuple('ArgSpec', ['args', 'varargs', 'varkw',
+ 'defaults', 'kwonlyargs',
+ 'kwonlydefaults', 'annotations'])
+
+
+def _getArgSpec(func):
+ """
+ Normalize inspect.ArgSpec across python versions
+ and convert mutable attributes to immutable types.
+
+ :param Callable func: A function.
+ :return: The function's ArgSpec.
+ :rtype: ArgSpec
+ """
+ spec = getArgsSpec(func)
+ return ArgSpec(
+ args=tuple(spec.args),
+ varargs=spec.varargs,
+ varkw=spec.varkw if six.PY3 else spec.keywords,
+ defaults=spec.defaults if spec.defaults else (),
+ kwonlyargs=tuple(spec.kwonlyargs) if six.PY3 else (),
+ kwonlydefaults=(
+ tuple(spec.kwonlydefaults.items())
+ if spec.kwonlydefaults else ()
+ ) if six.PY3 else (),
+ annotations=tuple(spec.annotations.items()) if six.PY3 else (),
+ )
+
+
+def _getArgNames(spec):
+ """
+ Get the name of all arguments defined in a function signature.
+
+ The name of * and ** arguments is normalized to "*args" and "**kwargs".
+
+ :param ArgSpec spec: A function to interrogate for a signature.
+ :return: The set of all argument names in `func`s signature.
+ :rtype: Set[str]
+ """
+ return set(
+ spec.args
+ + spec.kwonlyargs
+ + (('*args',) if spec.varargs else ())
+ + (('**kwargs',) if spec.varkw else ())
+ + spec.annotations
+ )
+
+
+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 :class:`automat.MethodicalMachine`
+ associated with this :class:`automat.MethodicalState`:
+ upon the receipt of the `input`, enter the `state`,
+ emitting each output in `outputs`.
+
+ :param MethodicalInput input: The input triggering a state transition.
+ :param MethodicalState enter: The resulting state.
+ :param Iterable[MethodicalOutput] outputs: The outputs to be triggered
+ as a result of the declared state transition.
+ :param Callable collector: The function to be used when collecting
+ output return values.
+
+ :raises TypeError: if any of the `outputs` signatures do not match
+ the `inputs` signature.
+ :raises ValueError: if the state transition from `self` via `input`
+ has already been defined.
+ """
+ inputArgs = _getArgNames(input.argSpec)
+ for output in outputs:
+ outputArgs = _getArgNames(output.argSpec)
+ if not outputArgs.issubset(inputArgs):
+ raise TypeError(
+ "method {input} signature {inputSignature} "
+ "does not match output {output} "
+ "signature {outputSignature}".format(
+ input=input.method.__name__,
+ output=output.method.__name__,
+ inputSignature=getArgsSpec(input.method),
+ outputSignature=getArgsSpec(output.method),
+ ))
+ 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")
+
+
+def _filterArgs(args, kwargs, inputSpec, outputSpec):
+ """
+ Filter out arguments that were passed to input that output won't accept.
+
+ :param tuple args: The *args that input received.
+ :param dict kwargs: The **kwargs that input received.
+ :param ArgSpec inputSpec: The input's arg spec.
+ :param ArgSpec outputSpec: The output's arg spec.
+ :return: The args and kwargs that output will accept.
+ :rtype: Tuple[tuple, dict]
+ """
+ named_args = tuple(zip(inputSpec.args[1:], args))
+ if outputSpec.varargs:
+ # Only return all args if the output accepts *args.
+ return_args = args
+ else:
+ # Filter out arguments that don't appear
+ # in the output's method signature.
+ return_args = [v for n, v in named_args if n in outputSpec.args]
+
+ # Get any of input's default arguments that were not passed.
+ passed_arg_names = tuple(kwargs)
+ for name, value in named_args:
+ passed_arg_names += (name, value)
+ defaults = zip(inputSpec.args[::-1], inputSpec.defaults[::-1])
+ full_kwargs = {n: v for n, v in defaults if n not in passed_arg_names}
+ full_kwargs.update(kwargs)
+
+ if outputSpec.varkw:
+ # Only pass all kwargs if the output method accepts **kwargs.
+ return_kwargs = full_kwargs
+ else:
+ # Filter out names that the output method does not accept.
+ all_accepted_names = outputSpec.args[1:] + outputSpec.kwonlyargs
+ return_kwargs = {n: v for n, v in full_kwargs.items()
+ if n in all_accepted_names}
+
+ return return_args, return_kwargs
+
+
+@attr.s(eq=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)
+ argSpec = attr.ib(init=False, repr=False)
+
+ @argSpec.default
+ def _buildArgSpec(self):
+ return _getArgSpec(self.method)
+
+ 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())
+ a, k = _filterArgs(args, kwargs, self.argSpec, output.argSpec)
+ value = output(oself, *a, **k)
+ 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()
+ argSpec = attr.ib(init=False, repr=False)
+
+ @argSpec.default
+ def _buildArgSpec(self):
+ return _getArgSpec(self.method)
+
+ 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(eq=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 :class:`MethodicalMachine` is an interface to an `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 bool initial: is this state the initial state?
+ Only one state on this :class:`automat.MethodicalMachine`
+ may be an initial state; more than one is an error.
+
+ :param bool 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.)
+
+ :param Hashable serialized: a serializable value
+ to be used to represent this state to external systems.
+ This value should be hashable;
+ :py:func:`unicode` is a good type to use.
+ """
+ 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 decorated `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<https://graphviz.readthedocs.io/>}
+
+ """
+ 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__,
+ )
diff --git a/contrib/python/Automat/py2/automat/_visualize.py b/contrib/python/Automat/py2/automat/_visualize.py
new file mode 100644
index 0000000000..7a9c8c6eb5
--- /dev/null
+++ b/contrib/python/Automat/py2/automat/_visualize.py
@@ -0,0 +1,182 @@
+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)
diff --git a/contrib/python/Automat/py2/ya.make b/contrib/python/Automat/py2/ya.make
new file mode 100644
index 0000000000..f7abf294a0
--- /dev/null
+++ b/contrib/python/Automat/py2/ya.make
@@ -0,0 +1,38 @@
+# Generated by devtools/yamaker (pypi).
+
+PY2_LIBRARY()
+
+VERSION(20.2.0)
+
+LICENSE(MIT)
+
+PEERDIR(
+ contrib/python/attrs
+ contrib/python/six
+)
+
+NO_LINT()
+
+NO_CHECK_IMPORTS(
+ automat._discover
+ automat._visualize
+)
+
+PY_SRCS(
+ TOP_LEVEL
+ automat/__init__.py
+ automat/_core.py
+ automat/_discover.py
+ automat/_introspection.py
+ automat/_methodical.py
+ automat/_visualize.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/Automat/py2/
+ .dist-info/METADATA
+ .dist-info/entry_points.txt
+ .dist-info/top_level.txt
+)
+
+END()