diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Automat/py2 | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Automat/py2')
-rw-r--r-- | contrib/python/Automat/py2/.dist-info/METADATA | 487 | ||||
-rw-r--r-- | contrib/python/Automat/py2/.dist-info/entry_points.txt | 3 | ||||
-rw-r--r-- | contrib/python/Automat/py2/.dist-info/top_level.txt | 1 | ||||
-rw-r--r-- | contrib/python/Automat/py2/LICENSE | 21 | ||||
-rw-r--r-- | contrib/python/Automat/py2/README.md | 430 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/__init__.py | 8 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/_core.py | 165 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/_discover.py | 144 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/_introspection.py | 45 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/_methodical.py | 474 | ||||
-rw-r--r-- | contrib/python/Automat/py2/automat/_visualize.py | 182 | ||||
-rw-r--r-- | contrib/python/Automat/py2/ya.make | 38 |
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() |