Automat-0.6.0/0000755000175000017500000000000013106704143012562 5ustar mrwmrw00000000000000Automat-0.6.0/.gitignore0000644000175000017500000000006313014133630014544 0ustar mrwmrw00000000000000.tox/ .coverage* *.egg-info/ *.py[co] build/ dist/ Automat-0.6.0/.travis.yml0000644000175000017500000000145513106675015014705 0ustar mrwmrw00000000000000language: python matrix: include: - python: 2.7 env: TOX_ENV=py27-extras - python: 2.7 env: TOX_ENV=py27-noextras - python: pypy env: TOX_ENV=pypy-extras - python: pypy env: TOX_ENV=pypy-noextras - python: 3.3 env: TOX_ENV=py33-extras - python: 3.3 env: TOX_ENV=py33-noextras - python: 3.4 env: TOX_ENV=py34-extras - python: 3.4 env: TOX_ENV=py34-noextras - python: 3.5 env: TOX_ENV=py35-extras - python: 3.5 env: TOX_ENV=py35-noextras - python: 3.6 env: TOX_ENV=py36-extras - python: 3.6 env: TOX_ENV=py36-noextras install: - sudo apt-get install graphviz - pip install tox coveralls script: - tox -e $TOX_ENV after_success: - tox -e coverage-report - coveralls Automat-0.6.0/Automat.egg-info/0000755000175000017500000000000013106704143015666 5ustar mrwmrw00000000000000Automat-0.6.0/Automat.egg-info/PKG-INFO0000644000175000017500000004661313106704143016775 0ustar mrwmrw00000000000000Metadata-Version: 1.0 Name: Automat Version: 0.6.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: glyph@twistedmatrix.com License: MIT Description: Automat ======= .. image:: https://travis-ci.org/glyph/automat.svg?branch=master :target: https://travis-ci.org/glyph/automat :alt: Build Status .. image:: https://coveralls.io/repos/glyph/automat/badge.png :target: https://coveralls.io/r/glyph/automat :alt: Coverage Status Self-service finite-state machines for the programmer on the go. ---------------------------------------------------------------- Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Why use state machines? ^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , ``is_lid_open`` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail `in this excellent series of articles from ClusterHQ `_. What makes Automat different? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are `dozens of libraries on PyPI implementing state machines `_. So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: .. code-block:: python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... With Automat, you'd create a class with a ``MethodicalMachine`` attribute: .. code-block:: python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() and then you would break the above logic into two pieces - the ``brew_button`` *input*\ , declared like so: .. code-block:: python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*\ 's job: .. code-block:: python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() As well as a couple of *states* - and for simplicity's sake let's say that the only two states are ``have_beans`` and ``dont_have_beans``\ : .. code-block:: python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans in it. (And another input to put some beans in:) .. code-block:: python @_machine.input() def put_in_beans(self): "The user put in some beans." Finally, you hook everything together with the ``upon`` method of the functions decorated with ``machine.state``\ : .. code-block:: python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): .. code-block:: python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to ``upon`` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. How do I get the current state of a state machine? -------------------------------------------------- Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: .. code-block:: python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") Instead, just make your calling code do this: .. code-block:: python connection_state_machine.send_message() and then change your state machine to look like this: .. code-block:: python @machine.state() def connected(self): "connected" @machine.state() def not_connected(self): "not connected" @machine.input() def send_message(self): "send a message" @machine.output() def _actually_send_message(self): self._transport.send(b"message") @machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) so that the responsibility for knowing which state the state machine is in remains within the state machine itself. Input for Inputs and Output for Outputs --------------------------------------- Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. .. code-block:: python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: .. code-block:: python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans and then connect it to the ``put_in_beans`` by changing the transition from ``dont_have_beans`` to ``have_beans`` like so: .. code-block:: python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) Now, when you call: .. code-block:: python coffee_machine.put_in_beans("real good beans") the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our ``brew_button`` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: .. code-block:: python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) Note that we don't need to check first whether ``self._beans`` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change the brewing transition to: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) Now, we can call it: .. code-block:: python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] Except... wait a second, what's that ``None`` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a ``list``. In this case, we have both ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing both of their return values. However, this can be customized, with the ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to ``upon`` like this: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) And now, we'll get just the return value we want: .. code-block:: python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) -------------------------------------------------------------------------------------------------------------------- There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: .. code-block:: python class LightSwitch(object): machine = MethodicalMachine() @machine.state(serialized="on") def on_state(self): "the switch is on" @machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) In this case, we've chosen a serialized representation for each state via the ``serialized`` argument. The on state is represented by the string ``"on"``\ , and the off state is represented by the string ``"off"``. Now, let's just add an input that lets us tell if the switch is on or not. .. code-block:: python @machine.input() def query_power(self): "return True if powered, False otherwise" @machine.output() def _is_powered(self): return True @machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) To save the state, we have the ``MethodicalMachine.serializer()`` method. A method decorated with ``@serializer()`` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either ``"on"`` or ``"off"``. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: .. code-block:: python @machine.serializer() def save(self, state): return {"is-it-on": state} Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with ``@machine.serializer()`` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: .. code-block:: python @machine.unserializer() def _restore(self, blob): return blob["is-it-on"] Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: .. code-block:: python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Saving and loading our ``LightSwitch`` along with its state-machine state can now be accomplished as follows: .. code-block:: python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True More comprehensive (tested, working) examples are present in ``docs/examples``. Go forth and machine all the state! Keywords: fsm finite state machine automata Platform: UNKNOWN Automat-0.6.0/Automat.egg-info/SOURCES.txt0000644000175000017500000000130713106704143017553 0ustar mrwmrw00000000000000.gitignore .travis.yml LICENSE README.md setup.cfg setup.py tox.ini Automat.egg-info/PKG-INFO Automat.egg-info/SOURCES.txt Automat.egg-info/dependency_links.txt Automat.egg-info/entry_points.txt Automat.egg-info/requires.txt Automat.egg-info/top_level.txt automat/__init__.py automat/_core.py automat/_discover.py automat/_introspection.py automat/_methodical.py automat/_visualize.py automat/_test/__init__.py automat/_test/test_core.py automat/_test/test_discover.py automat/_test/test_methodical.py automat/_test/test_trace.py automat/_test/test_visualize.py docs/debugging.md docs/examples/automat_example.py docs/examples/io_coffee_example.py docs/examples/lightswitch.py docs/examples/turnstile_example.pyAutomat-0.6.0/Automat.egg-info/dependency_links.txt0000644000175000017500000000000113106704143021734 0ustar mrwmrw00000000000000 Automat-0.6.0/Automat.egg-info/entry_points.txt0000644000175000017500000000007713106704143021170 0ustar mrwmrw00000000000000[console_scripts] automat-visualize = automat._visualize:tool Automat-0.6.0/Automat.egg-info/requires.txt0000644000175000017500000000006613106704143020270 0ustar mrwmrw00000000000000attrs six [visualize] graphviz>0.5.1 Twisted>=16.1.1 Automat-0.6.0/Automat.egg-info/top_level.txt0000644000175000017500000000001013106704143020407 0ustar mrwmrw00000000000000automat Automat-0.6.0/LICENSE0000644000175000017500000000203512641273151013572 0ustar mrwmrw00000000000000Copyright (c) 2014 Rackspace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Automat-0.6.0/PKG-INFO0000644000175000017500000004661313106704143013671 0ustar mrwmrw00000000000000Metadata-Version: 1.0 Name: Automat Version: 0.6.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: glyph@twistedmatrix.com License: MIT Description: Automat ======= .. image:: https://travis-ci.org/glyph/automat.svg?branch=master :target: https://travis-ci.org/glyph/automat :alt: Build Status .. image:: https://coveralls.io/repos/glyph/automat/badge.png :target: https://coveralls.io/r/glyph/automat :alt: Coverage Status Self-service finite-state machines for the programmer on the go. ---------------------------------------------------------------- Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Why use state machines? ^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , ``is_lid_open`` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail `in this excellent series of articles from ClusterHQ `_. What makes Automat different? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are `dozens of libraries on PyPI implementing state machines `_. So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: .. code-block:: python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... With Automat, you'd create a class with a ``MethodicalMachine`` attribute: .. code-block:: python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() and then you would break the above logic into two pieces - the ``brew_button`` *input*\ , declared like so: .. code-block:: python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*\ 's job: .. code-block:: python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() As well as a couple of *states* - and for simplicity's sake let's say that the only two states are ``have_beans`` and ``dont_have_beans``\ : .. code-block:: python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans in it. (And another input to put some beans in:) .. code-block:: python @_machine.input() def put_in_beans(self): "The user put in some beans." Finally, you hook everything together with the ``upon`` method of the functions decorated with ``machine.state``\ : .. code-block:: python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): .. code-block:: python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to ``upon`` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. How do I get the current state of a state machine? -------------------------------------------------- Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: .. code-block:: python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") Instead, just make your calling code do this: .. code-block:: python connection_state_machine.send_message() and then change your state machine to look like this: .. code-block:: python @machine.state() def connected(self): "connected" @machine.state() def not_connected(self): "not connected" @machine.input() def send_message(self): "send a message" @machine.output() def _actually_send_message(self): self._transport.send(b"message") @machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) so that the responsibility for knowing which state the state machine is in remains within the state machine itself. Input for Inputs and Output for Outputs --------------------------------------- Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. .. code-block:: python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: .. code-block:: python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans and then connect it to the ``put_in_beans`` by changing the transition from ``dont_have_beans`` to ``have_beans`` like so: .. code-block:: python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) Now, when you call: .. code-block:: python coffee_machine.put_in_beans("real good beans") the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our ``brew_button`` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: .. code-block:: python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) Note that we don't need to check first whether ``self._beans`` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change the brewing transition to: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) Now, we can call it: .. code-block:: python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] Except... wait a second, what's that ``None`` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a ``list``. In this case, we have both ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing both of their return values. However, this can be customized, with the ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to ``upon`` like this: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) And now, we'll get just the return value we want: .. code-block:: python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) -------------------------------------------------------------------------------------------------------------------- There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: .. code-block:: python class LightSwitch(object): machine = MethodicalMachine() @machine.state(serialized="on") def on_state(self): "the switch is on" @machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) In this case, we've chosen a serialized representation for each state via the ``serialized`` argument. The on state is represented by the string ``"on"``\ , and the off state is represented by the string ``"off"``. Now, let's just add an input that lets us tell if the switch is on or not. .. code-block:: python @machine.input() def query_power(self): "return True if powered, False otherwise" @machine.output() def _is_powered(self): return True @machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) To save the state, we have the ``MethodicalMachine.serializer()`` method. A method decorated with ``@serializer()`` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either ``"on"`` or ``"off"``. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: .. code-block:: python @machine.serializer() def save(self, state): return {"is-it-on": state} Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with ``@machine.serializer()`` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: .. code-block:: python @machine.unserializer() def _restore(self, blob): return blob["is-it-on"] Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: .. code-block:: python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Saving and loading our ``LightSwitch`` along with its state-machine state can now be accomplished as follows: .. code-block:: python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True More comprehensive (tested, working) examples are present in ``docs/examples``. Go forth and machine all the state! Keywords: fsm finite state machine automata Platform: UNKNOWN Automat-0.6.0/README.md0000644000175000017500000003513613037440020014043 0ustar mrwmrw00000000000000# Automat # [![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). ### Why use state machines? ### Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; `has_water`, `has_beans`, `is_lid_open` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail [in this excellent series of articles from ClusterHQ](https://clusterhq.com/blog/what-is-a-state-machine/). ### What makes Automat different? ### There are [dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine). So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: ```python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... ``` With Automat, you'd create a class with a `MethodicalMachine` attribute: ```python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() ``` and then you would break the above logic into two pieces - the `brew_button` *input*, declared like so: ```python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." ``` It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*'s job: ```python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() ``` As well as a couple of *states* - and for simplicity's sake let's say that the only two states are `have_beans` and `dont_have_beans`: ```python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``` `have_beans` is the `initial` state because `CoffeeBrewer` starts without beans in it. (And another input to put some beans in:) ```python @_machine.input() def put_in_beans(self): "The user put in some beans." ``` Finally, you hook everything together with the `upon` method of the functions decorated with `machine.state`: ```python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) ``` To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): ```python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() ``` All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to `upon` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. ## How do I get the current state of a state machine? Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: ```python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") ``` Instead, just make your calling code do this: ```python connection_state_machine.send_message() ``` and then change your state machine to look like this: ```python @machine.state() def connected(self): "connected" @machine.state() def not_connected(self): "not connected" @machine.input() def send_message(self): "send a message" @machine.output() def _actually_send_message(self): self._transport.send(b"message") @machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) ``` so that the responsibility for knowing which state the state machine is in remains within the state machine itself. ## Input for Inputs and Output for Outputs Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. ```python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." ``` However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: ```python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans ``` and then connect it to the `put_in_beans` by changing the transition from `dont_have_beans` to `have_beans` like so: ```python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) ``` Now, when you call: ```python coffee_machine.put_in_beans("real good beans") ``` the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our `brew_button` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: ```python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) ``` Note that we don't need to check first whether `self._beans` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up `_describe_coffee` to the process of brewing, so change the brewing transition to: ```python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) ``` Now, we can call it: ```python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] ``` Except... wait a second, what's that `None` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a `list`. In this case, we have both `_heat_the_heating_element` and `_describe_coffee` outputs, so we're seeing both of their return values. However, this can be customized, with the `collector` argument to `upon`; the `collector` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to `upon` like this: ```python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) ``` And now, we'll get just the return value we want: ```python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' ``` ## If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the `serialized=` argument to the `MethodicalMachine.state()` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: ```python class LightSwitch(object): machine = MethodicalMachine() @machine.state(serialized="on") def on_state(self): "the switch is on" @machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) ``` In this case, we've chosen a serialized representation for each state via the `serialized` argument. The on state is represented by the string `"on"`, and the off state is represented by the string `"off"`. Now, let's just add an input that lets us tell if the switch is on or not. ```python @machine.input() def query_power(self): "return True if powered, False otherwise" @machine.output() def _is_powered(self): return True @machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) ``` To save the state, we have the `MethodicalMachine.serializer()` method. A method decorated with `@serializer()` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either `"on"` or `"off"`. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: ```python @machine.serializer() def save(self, state): return {"is-it-on": state} ``` Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with `@machine.serializer()` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: ```python @machine.unserializer() def _restore(self, blob): return blob["is-it-on"] ``` Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: ```python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self ``` Saving and loading our `LightSwitch` along with its state-machine state can now be accomplished as follows: ```python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True ``` More comprehensive (tested, working) examples are present in `docs/examples`. Go forth and machine all the state! Automat-0.6.0/automat/0000755000175000017500000000000013106704143014234 5ustar mrwmrw00000000000000Automat-0.6.0/automat/__init__.py0000644000175000017500000000025112720705662016353 0ustar mrwmrw00000000000000# -*- test-case-name: automat -*- from ._methodical import MethodicalMachine from ._core import NoTransition __all__ = [ 'MethodicalMachine', 'NoTransition', ] Automat-0.6.0/automat/_core.py0000644000175000017500000001133113106675015015701 0ustar mrwmrw00000000000000# -*- test-case-name: automat._test.test_core -*- """ A core state-machine abstraction. Perhaps something that could be replaced with or integrated into machinist. """ from itertools import chain _NO_STATE = "" class NoTransition(Exception): """ A finite state machine in C{state} has no transition for C{symbol}. @param state: the finite state machine's state at the time of the illegal transition. @param symbol: the input symbol for which no transition exists. """ def __init__(self, state, symbol): self.state = state self.symbol = symbol super(Exception, self).__init__( "no transition for {} in {}".format(symbol, state) ) class Automaton(object): """ A declaration of a finite state machine. Note that this is not the machine itself; it is immutable. """ def __init__(self): """ Initialize the set of transitions and the initial state. """ self._initialState = _NO_STATE self._transitions = set() @property def initialState(self): """ Return this automaton's initial state. """ return self._initialState @initialState.setter def initialState(self, state): """ Set this automaton's initial state. Raises a ValueError if this automaton already has an initial state. """ if self._initialState is not _NO_STATE: raise ValueError( "initial state already set to {}".format(self._initialState)) self._initialState = state def addTransition(self, inState, inputSymbol, outState, outputSymbols): """ Add the given transition to the outputSymbol. Raise ValueError if there is already a transition with the same inState and inputSymbol. """ # keeping self._transitions in a flat list makes addTransition # O(n^2), but state machines don't tend to have hundreds of # transitions. for (anInState, anInputSymbol, anOutState, _) in self._transitions: if (anInState == inState and anInputSymbol == inputSymbol): raise ValueError( "already have transition from {} via {}".format(inState, inputSymbol)) self._transitions.add( (inState, inputSymbol, outState, tuple(outputSymbols)) ) def allTransitions(self): """ All transitions. """ return frozenset(self._transitions) def inputAlphabet(self): """ The full set of symbols acceptable to this automaton. """ return set(inputSymbol for (inState, inputSymbol, outState, outputSymbol) in self._transitions) def outputAlphabet(self): """ The full set of symbols which can be produced by this automaton. """ return set( chain.from_iterable( outputSymbols for (inState, inputSymbol, outState, outputSymbols) in self._transitions ) ) def states(self): """ All valid states; "Q" in the mathematical description of a state machine. """ return frozenset( chain.from_iterable( (inState, outState) for (inState, inputSymbol, outState, outputSymbol) in self._transitions ) ) def outputForInput(self, inState, inputSymbol): """ A 2-tuple of (outState, outputSymbols) for inputSymbol. """ for (anInState, anInputSymbol, outState, outputSymbols) in self._transitions: if (inState, inputSymbol) == (anInState, anInputSymbol): return (outState, list(outputSymbols)) raise NoTransition(state=inState, symbol=inputSymbol) class Transitioner(object): """ The combination of a current state and an L{Automaton}. """ def __init__(self, automaton, initialState): self._automaton = automaton self._state = initialState self._tracer = None def setTrace(self, tracer): self._tracer = tracer def transition(self, inputSymbol): """ Transition between states, returning any outputs. """ outState, outputSymbols = self._automaton.outputForInput(self._state, inputSymbol) outTracer = None if self._tracer: outTracer = self._tracer(self._state._name(), inputSymbol._name(), outState._name()) self._state = outState return (outputSymbols, outTracer) Automat-0.6.0/automat/_discover.py0000644000175000017500000001041713014133630016561 0ustar mrwmrw00000000000000import collections import inspect from automat import MethodicalMachine from twisted.python.modules import PythonModule, getModule def isOriginalLocation(attr): """ Attempt to discover if this appearance of a PythonAttribute representing a class refers to the module where that class was defined. """ sourceModule = inspect.getmodule(attr.load()) if sourceModule is None: return False currentModule = attr while not isinstance(currentModule, PythonModule): currentModule = currentModule.onObject return currentModule.name == sourceModule.__name__ def findMachinesViaWrapper(within): """ Recursively yield L{MethodicalMachine}s and their FQPNs within a L{PythonModule} or a L{twisted.python.modules.PythonAttribute} wrapper object. Note that L{PythonModule}s may refer to packages, as well. The discovery heuristic considers L{MethodicalMachine} instances that are module-level attributes or class-level attributes accessible from module scope. Machines inside nested classes will be discovered, but those returned from functions or methods will not be. @type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute} @param within: Where to start the search. @return: a generator which yields FQPN, L{MethodicalMachine} pairs. """ queue = collections.deque([within]) visited = set() while queue: attr = queue.pop() value = attr.load() if isinstance(value, MethodicalMachine) and value not in visited: visited.add(value) yield attr.name, value elif (inspect.isclass(value) and isOriginalLocation(attr) and value not in visited): visited.add(value) queue.extendleft(attr.iterAttributes()) elif isinstance(attr, PythonModule) and value not in visited: visited.add(value) queue.extendleft(attr.iterAttributes()) queue.extendleft(attr.iterModules()) class InvalidFQPN(Exception): """ The given FQPN was not a dot-separated list of Python objects. """ class NoModule(InvalidFQPN): """ A prefix of the FQPN was not an importable module or package. """ class NoObject(InvalidFQPN): """ A suffix of the FQPN was not an accessible object """ def wrapFQPN(fqpn): """ Given an FQPN, retrieve the object via the global Python module namespace and wrap it with a L{PythonModule} or a L{twisted.python.modules.PythonAttribute}. """ # largely cribbed from t.p.reflect.namedAny if not fqpn: raise InvalidFQPN("FQPN was empty") components = collections.deque(fqpn.split('.')) if '' in components: raise InvalidFQPN( "name must be a string giving a '.'-separated list of Python " "identifiers, not %r" % (fqpn,)) component = components.popleft() try: module = getModule(component) except KeyError: raise NoModule(component) # find the bottom-most module while components: component = components.popleft() try: module = module[component] except KeyError: components.appendleft(component) break else: module.load() else: return module # find the bottom-most attribute attribute = module for component in components: try: attribute = next(child for child in attribute.iterAttributes() if child.name.rsplit('.', 1)[-1] == component) except StopIteration: raise NoObject('{}.{}'.format(attribute.name, component)) return attribute def findMachines(fqpn): """ Recursively yield L{MethodicalMachine}s and their FQPNs in and under the a Python object specified by an FQPN. The discovery heuristic considers L{MethodicalMachine} instances that are module-level attributes or class-level attributes accessible from module scope. Machines inside nested classes will be discovered, but those returned from functions or methods will not be. @type within: an FQPN @param within: Where to start the search. @return: a generator which yields FQPN, L{MethodicalMachine} pairs. """ return findMachinesViaWrapper(wrapFQPN(fqpn)) Automat-0.6.0/automat/_introspection.py0000644000175000017500000000213412717000352017643 0ustar mrwmrw00000000000000""" Python introspection helpers. """ from types import CodeType as code, FunctionType as function def copycode(template, changes): names = [ "argcount", "nlocals", "stacksize", "flags", "code", "consts", "names", "varnames", "filename", "name", "firstlineno", "lnotab", "freevars", "cellvars" ] if str is not bytes: names.insert(1, "kwonlyargcount") values = [ changes.get(name, getattr(template, "co_" + name)) for name in names ] return code(*values) def copyfunction(template, funcchanges, codechanges): names = [ "globals", "name", "defaults", "closure", ] values = [ funcchanges.get(name, getattr(template, "__" + name + "__")) for name in names ] return function(copycode(template.__code__, codechanges), *values) def preserveName(f): """ Preserve the name of the given function on the decorated function. """ def decorator(decorated): return copyfunction(decorated, dict(name=f.__name__), dict(name=f.__name__)) return decorator Automat-0.6.0/automat/_methodical.py0000644000175000017500000002711613106703427017071 0ustar mrwmrw00000000000000# -*- test-case-name: automat._test.test_methodical -*- from functools import wraps from itertools import count try: # Python 3 from inspect import getfullargspec as getArgsSpec except ImportError: # Python 2 from inspect import getargspec as getArgsSpec import attr from ._core import Transitioner, Automaton from ._introspection import preserveName def _keywords_only(f): """ Decorate a function so all its arguments must be passed by keyword. A useful utility for decorators that take arguments so that they don't accidentally get passed the thing they're decorating as their first argument. Only works for methods right now. """ @wraps(f) def g(self, **kw): return f(self, **kw) return g @attr.s(frozen=True) class MethodicalState(object): """ A state for a L{MethodicalMachine}. """ machine = attr.ib(repr=False) method = attr.ib() serialized = attr.ib(repr=False) def upon(self, input, enter, outputs, collector=list): """ Declare a state transition within the L{MethodicalMachine} associated with this L{MethodicalState}: upon the receipt of the input C{input}, enter the state C{enter}, emitting each output in C{outputs}. """ inputSpec = getArgsSpec(input.method) for output in outputs: outputSpec = getArgsSpec(output.method) if inputSpec != outputSpec: raise TypeError( "method {input} signature {inputSignature} " "does not match output {output} " "signature {outputSignature}".format( input=input.method.__name__, output=output.method.__name__, inputSignature=inputSpec, outputSignature=outputSpec, )) self.machine._oneTransition(self, input, enter, outputs, collector) def _name(self): return self.method.__name__ def _transitionerFromInstance(oself, symbol, automaton): """ Get a L{Transitioner} """ transitioner = getattr(oself, symbol, None) if transitioner is None: transitioner = Transitioner( automaton, automaton.initialState, ) setattr(oself, symbol, transitioner) return transitioner def _empty(): pass def _docstring(): """docstring""" def assertNoCode(inst, attribute, f): # The function body must be empty, i.e. "pass" or "return None", which # both yield the same bytecode: LOAD_CONST (None), RETURN_VALUE. We also # accept functions with only a docstring, which yields slightly different # bytecode, because the "None" is put in a different constant slot. # Unfortunately, this does not catch function bodies that return a # constant value, e.g. "return 1", because their code is identical to a # "return None". They differ in the contents of their constant table, but # checking that would require us to parse the bytecode, find the index # being returned, then making sure the table has a None at that index. if f.__code__.co_code not in (_empty.__code__.co_code, _docstring.__code__.co_code): raise ValueError("function body must be empty") @attr.s(cmp=False, hash=False) class MethodicalInput(object): """ An input for a L{MethodicalMachine}. """ automaton = attr.ib(repr=False) method = attr.ib(validator=assertNoCode) symbol = attr.ib(repr=False) collectors = attr.ib(default=attr.Factory(dict), repr=False) def __get__(self, oself, type=None): """ Return a function that takes no arguments and returns values returned by output functions produced by the given L{MethodicalInput} in C{oself}'s current state. """ transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton) @preserveName(self.method) @wraps(self.method) def doInput(*args, **kwargs): self.method(oself, *args, **kwargs) previousState = transitioner._state (outputs, outTracer) = transitioner.transition(self) collector = self.collectors[previousState] values = [] for output in outputs: if outTracer: outTracer(output._name()) value = output(oself, *args, **kwargs) values.append(value) return collector(values) return doInput def _name(self): return self.method.__name__ @attr.s(frozen=True) class MethodicalOutput(object): """ An output for a L{MethodicalMachine}. """ machine = attr.ib(repr=False) method = attr.ib() def __get__(self, oself, type=None): """ Outputs are private, so raise an exception when we attempt to get one. """ raise AttributeError( "{cls}.{method} is a state-machine output method; " "to produce this output, call an input method instead.".format( cls=type.__name__, method=self.method.__name__ ) ) def __call__(self, oself, *args, **kwargs): """ Call the underlying method. """ return self.method(oself, *args, **kwargs) def _name(self): return self.method.__name__ @attr.s(cmp=False, hash=False) class MethodicalTracer(object): automaton = attr.ib(repr=False) symbol = attr.ib(repr=False) def __get__(self, oself, type=None): transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton) def setTrace(tracer): transitioner.setTrace(tracer) return setTrace counter = count() def gensym(): """ Create a unique Python identifier. """ return "_symbol_" + str(next(counter)) class MethodicalMachine(object): """ A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods on a class. """ def __init__(self): self._automaton = Automaton() self._reducers = {} self._symbol = gensym() def __get__(self, oself, type=None): """ L{MethodicalMachine} is an implementation detail for setting up class-level state; applications should never need to access it on an instance. """ if oself is not None: raise AttributeError( "MethodicalMachine is an implementation detail.") return self @_keywords_only def state(self, initial=False, terminal=False, serialized=None): """ Declare a state, possibly an initial state or a terminal state. This is a decorator for methods, but it will modify the method so as not to be callable any more. @param initial: is this state the initial state? Only one state on this L{MethodicalMachine} may be an initial state; more than one is an error. @type initial: L{bool} @param terminal: Is this state a terminal state, i.e. a state that the machine can end up in? (This is purely informational at this point.) @type terminal: L{bool} @param serialized: a serializable value to be used to represent this state to external systems. This value should be hashable; L{unicode} is a good type to use. @type serialized: a hashable (comparable) value """ def decorator(stateMethod): state = MethodicalState(machine=self, method=stateMethod, serialized=serialized) if initial: self._automaton.initialState = state return state return decorator @_keywords_only def input(self): """ Declare an input. This is a decorator for methods. """ def decorator(inputMethod): return MethodicalInput(automaton=self._automaton, method=inputMethod, symbol=self._symbol) return decorator @_keywords_only def output(self): """ Declare an output. This is a decorator for methods. This method will be called when the state machine transitions to this state as specified in the L{MethodicalMachine.output} method. """ def decorator(outputMethod): return MethodicalOutput(machine=self, method=outputMethod) return decorator def _oneTransition(self, startState, inputToken, endState, outputTokens, collector): """ See L{MethodicalState.upon}. """ # FIXME: tests for all of this (some of it is wrong) # if not isinstance(startState, MethodicalState): # raise NotImplementedError("start state {} isn't a state" # .format(startState)) # if not isinstance(inputToken, MethodicalInput): # raise NotImplementedError("start state {} isn't an input" # .format(inputToken)) # if not isinstance(endState, MethodicalState): # raise NotImplementedError("end state {} isn't a state" # .format(startState)) # for output in outputTokens: # if not isinstance(endState, MethodicalState): # raise NotImplementedError("output state {} isn't a state" # .format(endState)) self._automaton.addTransition(startState, inputToken, endState, tuple(outputTokens)) inputToken.collectors[startState] = collector @_keywords_only def serializer(self): """ """ def decorator(decoratee): @wraps(decoratee) def serialize(oself): transitioner = _transitionerFromInstance(oself, self._symbol, self._automaton) return decoratee(oself, transitioner._state.serialized) return serialize return decorator @_keywords_only def unserializer(self): """ """ def decorator(decoratee): @wraps(decoratee) def unserialize(oself, *args, **kwargs): state = decoratee(oself, *args, **kwargs) mapping = {} for eachState in self._automaton.states(): mapping[eachState.serialized] = eachState transitioner = _transitionerFromInstance( oself, self._symbol, self._automaton) transitioner._state = mapping[state] return None # it's on purpose return unserialize return decorator @property def _setTrace(self): return MethodicalTracer(self._automaton, self._symbol) def asDigraph(self): """ Generate a L{graphviz.Digraph} that represents this machine's states and transitions. @return: L{graphviz.Digraph} object; for more information, please see the documentation for U{graphviz} """ from ._visualize import makeDigraph return makeDigraph( self._automaton, stateAsString=lambda state: state.method.__name__, inputAsString=lambda input: input.method.__name__, outputAsString=lambda output: output.method.__name__, ) Automat-0.6.0/automat/_test/0000755000175000017500000000000013106704143015352 5ustar mrwmrw00000000000000Automat-0.6.0/automat/_test/__init__.py0000644000175000017500000000000012717000352017447 0ustar mrwmrw00000000000000Automat-0.6.0/automat/_test/test_core.py0000644000175000017500000000544012717000352017714 0ustar mrwmrw00000000000000 from .._core import Automaton, NoTransition from unittest import TestCase class CoreTests(TestCase): """ Tests for Automat's (currently private, implementation detail) core. """ def test_NoTransition(self): """ A L{NoTransition} exception describes the state and input symbol that caused it. """ # NoTransition requires two arguments with self.assertRaises(TypeError): NoTransition() state = "current-state" symbol = "transitionless-symbol" noTransitionException = NoTransition(state=state, symbol=symbol) self.assertIs(noTransitionException.symbol, symbol) self.assertIn(state, str(noTransitionException)) self.assertIn(symbol, str(noTransitionException)) def test_noOutputForInput(self): """ L{Automaton.outputForInput} raises L{NoTransition} if no transition for that input is defined. """ a = Automaton() self.assertRaises(NoTransition, a.outputForInput, "no-state", "no-symbol") def test_oneTransition(self): """ L{Automaton.addTransition} adds its input symbol to L{Automaton.inputAlphabet}, all its outputs to L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to start returning the new state and output symbols. """ a = Automaton() a.addTransition("beginning", "begin", "ending", ["end"]) self.assertEqual(a.inputAlphabet(), set(["begin"])) self.assertEqual(a.outputAlphabet(), set(["end"])) self.assertEqual(a.outputForInput("beginning", "begin"), ("ending", ["end"])) self.assertEqual(a.states(), set(["beginning", "ending"])) def test_oneTransition_nonIterableOutputs(self): """ L{Automaton.addTransition} raises a TypeError when given outputs that aren't iterable and doesn't add any transitions. """ a = Automaton() nonIterableOutputs = 1 self.assertRaises( TypeError, a.addTransition, "fromState", "viaSymbol", "toState", nonIterableOutputs) self.assertFalse(a.inputAlphabet()) self.assertFalse(a.outputAlphabet()) self.assertFalse(a.states()) self.assertFalse(a.allTransitions()) def test_initialState(self): """ L{Automaton.initialState} is a descriptor that sets the initial state if it's not yet set, and raises L{ValueError} if it is. """ a = Automaton() a.initialState = "a state" self.assertEqual(a.initialState, "a state") with self.assertRaises(ValueError): a.initialState = "another state" # FIXME: addTransition for transition that's been added before Automat-0.6.0/automat/_test/test_discover.py0000644000175000017500000005252613014133630020606 0ustar mrwmrw00000000000000import operator import os import shutil import sys import textwrap import tempfile from unittest import skipIf, TestCase import six def isTwistedInstalled(): try: __import__('twisted') except ImportError: return False else: return True class _WritesPythonModules(TestCase): """ A helper that enables generating Python module test fixtures. """ def setUp(self): super(_WritesPythonModules, self).setUp() from twisted.python.modules import getModule, PythonPath from twisted.python.filepath import FilePath self.getModule = getModule self.PythonPath = PythonPath self.FilePath = FilePath self.originalSysModules = set(sys.modules.keys()) self.savedSysPath = sys.path[:] self.pathDir = tempfile.mkdtemp() self.makeImportable(self.pathDir) def tearDown(self): super(_WritesPythonModules, self).tearDown() sys.path[:] = self.savedSysPath modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules for module in modulesToDelete: del sys.modules[module] shutil.rmtree(self.pathDir) def makeImportable(self, path): sys.path.append(path) def writeSourceInto(self, source, path, moduleName): directory = self.FilePath(path) module = directory.child(moduleName) # FilePath always opens a file in binary mode - but that will # break on Python 3 with open(module.path, 'w') as f: f.write(textwrap.dedent(source)) return self.PythonPath([directory.path]) def makeModule(self, source, path, moduleName): pythonModuleName, _ = os.path.splitext(moduleName) return self.writeSourceInto(source, path, moduleName)[pythonModuleName] def attributesAsDict(self, hasIterAttributes): return {attr.name: attr for attr in hasIterAttributes.iterAttributes()} def loadModuleAsDict(self, module): module.load() return self.attributesAsDict(module) def makeModuleAsDict(self, source, path, name): return self.loadModuleAsDict(self.makeModule(source, path, name)) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class OriginalLocationTests(_WritesPythonModules): """ Tests that L{isOriginalLocation} detects when a L{PythonAttribute}'s FQPN refers to an object inside the module where it was defined. For example: A L{twisted.python.modules.PythonAttribute} with a name of 'foo.bar' that refers to a 'bar' object defined in module 'baz' does *not* refer to bar's original location, while a L{PythonAttribute} with a name of 'baz.bar' does. """ def setUp(self): super(OriginalLocationTests, self).setUp() from .._discover import isOriginalLocation self.isOriginalLocation = isOriginalLocation def test_failsWithNoModule(self): """ L{isOriginalLocation} returns False when the attribute refers to an object whose source module cannot be determined. """ source = """\ class Fake(object): pass hasEmptyModule = Fake() hasEmptyModule.__module__ = None """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'empty_module_attr.py') self.assertFalse(self.isOriginalLocation( moduleDict['empty_module_attr.hasEmptyModule'])) def test_failsWithDifferentModule(self): """ L{isOriginalLocation} returns False when the attribute refers to an object outside of the module where that object was defined. """ originalSource = """\ class ImportThisClass(object): pass importThisObject = ImportThisClass() importThisNestingObject = ImportThisClass() importThisNestingObject.nestedObject = ImportThisClass() """ importingSource = """\ from original import (ImportThisClass, importThisObject, importThisNestingObject) """ self.makeModule(originalSource, self.pathDir, 'original.py') importingDict = self.makeModuleAsDict(importingSource, self.pathDir, 'importing.py') self.assertFalse( self.isOriginalLocation( importingDict['importing.ImportThisClass'])) self.assertFalse( self.isOriginalLocation( importingDict['importing.importThisObject'])) nestingObject = importingDict['importing.importThisNestingObject'] nestingObjectDict = self.attributesAsDict(nestingObject) nestedObject = nestingObjectDict[ 'importing.importThisNestingObject.nestedObject'] self.assertFalse(self.isOriginalLocation(nestedObject)) def test_succeedsWithSameModule(self): """ L{isOriginalLocation} returns True when the attribute refers to an object inside the module where that object was defined. """ mSource = textwrap.dedent(""" class ThisClassWasDefinedHere(object): pass anObject = ThisClassWasDefinedHere() aNestingObject = ThisClassWasDefinedHere() aNestingObject.nestedObject = ThisClassWasDefinedHere() """) mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py') self.assertTrue(self.isOriginalLocation( mDict['m.ThisClassWasDefinedHere'])) self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject'])) nestingObject = mDict['m.aNestingObject'] nestingObjectDict = self.attributesAsDict(nestingObject) nestedObject = nestingObjectDict['m.aNestingObject.nestedObject'] self.assertTrue(self.isOriginalLocation(nestedObject)) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class FindMachinesViaWrapperTests(_WritesPythonModules): """ L{findMachinesViaWrapper} recursively yields FQPN, L{MethodicalMachine} pairs in and under a given L{twisted.python.modules.PythonModule} or L{twisted.python.modules.PythonAttribute}. """ TEST_MODULE_SOURCE = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() class NestedClass(object): _nestedClassMachine = MethodicalMachine() ignoredAttribute = "I am ignored." def ignoredMethod(self): "I am also ignored." rootLevelMachine = MethodicalMachine() ignoredPythonObject = PythonClass() anotherIgnoredPythonObject = "I am ignored." """ def setUp(self): super(FindMachinesViaWrapperTests, self).setUp() from .._discover import findMachinesViaWrapper self.findMachinesViaWrapper = findMachinesViaWrapper def test_yieldsMachine(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers directly to a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine rootMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py') rootMachine = moduleDict['root.rootMachine'] self.assertIn(('root.rootMachine', rootMachine.load()), list(self.findMachinesViaWrapper(rootMachine))) def test_yieldsMachineInClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a class that contains a L{MethodicalMachine} as a class variable, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py') PythonClass = moduleDict['clsmod.PythonClass'] self.assertIn(('clsmod.PythonClass._classMachine', PythonClass.load()._classMachine), list(self.findMachinesViaWrapper(PythonClass))) def test_yieldsMachineInNestedClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a nested class that contains a L{MethodicalMachine} as a class variable, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): class NestedClass(object): _classMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'nestedcls.py') PythonClass = moduleDict['nestedcls.PythonClass'] self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine', PythonClass.load().NestedClass._classMachine), list(self.findMachinesViaWrapper(PythonClass))) def test_yieldsMachineInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to a module that contains a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine rootMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'root.py') rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load() self.assertIn(('root.rootMachine', rootMachine), list(self.findMachinesViaWrapper(module))) def test_yieldsMachineInClassInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to the original module of a class containing a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'clsmod.py') PythonClass = self.loadModuleAsDict( module)['clsmod.PythonClass'].load() self.assertIn(('clsmod.PythonClass._classMachine', PythonClass._classMachine), list(self.findMachinesViaWrapper(module))) def test_yieldsMachineInNestedClassInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to the original module of a nested class containing a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): class NestedClass(object): _classMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'nestedcls.py') PythonClass = self.loadModuleAsDict( module)['nestedcls.PythonClass'].load() self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine', PythonClass.NestedClass._classMachine), list(self.findMachinesViaWrapper(module))) def test_ignoresImportedClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a class imported from another module, any L{MethodicalMachine}s on that class are ignored. This behavior ensures that a machine is only discovered on a class when visiting the module where that class was defined. """ originalSource = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ importingSource = """ from original import PythonClass """ self.makeModule(originalSource, self.pathDir, 'original.py') importingModule = self.makeModule(importingSource, self.pathDir, 'importing.py') self.assertFalse(list(self.findMachinesViaWrapper(importingModule))) def test_descendsIntoPackages(self): """ L{findMachinesViaWrapper} descends into packages to discover machines. """ pythonPath = self.PythonPath([self.pathDir]) package = self.FilePath(self.pathDir).child("test_package") package.makedirs() package.child('__init__.py').touch() source = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() rootMachine = MethodicalMachine() """ self.makeModule(source, package.path, 'module.py') test_package = pythonPath['test_package'] machines = sorted(self.findMachinesViaWrapper(test_package), key=operator.itemgetter(0)) moduleDict = self.loadModuleAsDict(test_package['module']) rootMachine = moduleDict['test_package.module.rootMachine'].load() PythonClass = moduleDict['test_package.module.PythonClass'].load() expectedMachines = sorted( [('test_package.module.rootMachine', rootMachine), ('test_package.module.PythonClass._classMachine', PythonClass._classMachine)], key=operator.itemgetter(0)) self.assertEqual(expectedMachines, machines) def test_infiniteLoop(self): """ L{findMachinesViaWrapper} ignores infinite loops. Note this test can't fail - it can only run forever! """ source = """ class InfiniteLoop(object): pass InfiniteLoop.loop = InfiniteLoop """ module = self.makeModule(source, self.pathDir, 'loop.py') self.assertFalse(list(self.findMachinesViaWrapper(module))) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class WrapFQPNTests(TestCase): """ Tests that ensure L{wrapFQPN} loads the L{twisted.python.modules.PythonModule} or L{twisted.python.modules.PythonAttribute} for a given FQPN. """ def setUp(self): from twisted.python.modules import PythonModule, PythonAttribute from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject self.PythonModule = PythonModule self.PythonAttribute = PythonAttribute self.wrapFQPN = wrapFQPN self.InvalidFQPN = InvalidFQPN self.NoModule = NoModule self.NoObject = NoObject def assertModuleWrapperRefersTo(self, moduleWrapper, module): """ Assert that a L{twisted.python.modules.PythonModule} refers to a particular Python module. """ self.assertIsInstance(moduleWrapper, self.PythonModule) self.assertEqual(moduleWrapper.name, module.__name__) self.assertIs(moduleWrapper.load(), module) def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj): """ Assert that a L{twisted.python.modules.PythonAttribute} refers to a particular Python object. """ self.assertIsInstance(attributeWrapper, self.PythonAttribute) self.assertEqual(attributeWrapper.name, fqpn) self.assertIs(attributeWrapper.load(), obj) def test_failsWithEmptyFQPN(self): """ L{wrapFQPN} raises L{InvalidFQPN} when given an empty string. """ with self.assertRaises(self.InvalidFQPN): self.wrapFQPN('') def test_failsWithBadDotting(self): """" L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted FQPN. (e.g., x..y). """ for bad in ('.fails', 'fails.', 'this..fails'): with self.assertRaises(self.InvalidFQPN): self.wrapFQPN(bad) def test_singleModule(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the single module a dotless FQPN describes. """ import os moduleWrapper = self.wrapFQPN('os') self.assertIsInstance(moduleWrapper, self.PythonModule) self.assertIs(moduleWrapper.load(), os) def test_failsWithMissingSingleModuleOrPackage(self): """ L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does not refer to a module or package. """ with self.assertRaises(self.NoModule): self.wrapFQPN("this is not an acceptable name!") def test_singlePackage(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the single package a dotless FQPN describes. """ import xml self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml) def test_multiplePackages(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the deepest package described by dotted FQPN. """ import xml.etree self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree) def test_multiplePackagesFinalModule(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the deepest module described by dotted FQPN. """ import xml.etree.ElementTree self.assertModuleWrapperRefersTo( self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree) def test_singleModuleObject(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute} referring to the deepest object an FQPN names, traversing one module. """ import os self.assertAttributeWrapperRefersTo( self.wrapFQPN('os.path'), 'os.path', os.path) def test_multiplePackagesObject(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute} referring to the deepest object described by an FQPN, descending through several packages. """ import xml.etree.ElementTree import automat for fqpn, obj in [('xml.etree.ElementTree.fromstring', xml.etree.ElementTree.fromstring), ('automat.MethodicalMachine.__doc__', automat.MethodicalMachine.__doc__)]: self.assertAttributeWrapperRefersTo( self.wrapFQPN(fqpn), fqpn, obj) def test_failsWithMultiplePackagesMissingModuleOrPackage(self): """ L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a missing attribute, module, or package. """ for bad in ('xml.etree.nope!', 'xml.etree.nope!.but.the.rest.is.believable'): with self.assertRaises(self.NoObject): self.wrapFQPN(bad) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class FindMachinesIntegrationTests(_WritesPythonModules): """ Integration tests to check that L{findMachines} yields all machines discoverable at or below an FQPN. """ SOURCE = """ from automat import MethodicalMachine class PythonClass(object): _machine = MethodicalMachine() ignored = "i am ignored" rootLevel = MethodicalMachine() ignored = "i am ignored" """ def setUp(self): super(FindMachinesIntegrationTests, self).setUp() from .._discover import findMachines self.findMachines = findMachines packageDir = self.FilePath(self.pathDir).child("test_package") packageDir.makedirs() self.pythonPath = self.PythonPath([self.pathDir]) self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py') subPackageDir = packageDir.child('subpackage') subPackageDir.makedirs() subPackageDir.child('__init__.py').touch() self.makeModule(self.SOURCE, subPackageDir.path, 'module.py') self.packageDict = self.loadModuleAsDict( self.pythonPath['test_package']) self.moduleDict = self.loadModuleAsDict( self.pythonPath['test_package']['subpackage']['module']) def test_discoverAll(self): """ Given a top-level package FQPN, L{findMachines} discovers all L{MethodicalMachine} instances in and below it. """ machines = sorted(self.findMachines('test_package'), key=operator.itemgetter(0)) tpRootLevel = self.packageDict['test_package.rootLevel'].load() tpPythonClass = self.packageDict['test_package.PythonClass'].load() mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel'] mRootLevel = mRLAttr.load() mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass'] mPythonClass = mPCAttr.load() expectedMachines = sorted( [('test_package.rootLevel', tpRootLevel), ('test_package.PythonClass._machine', tpPythonClass._machine), ('test_package.subpackage.module.rootLevel', mRootLevel), ('test_package.subpackage.module.PythonClass._machine', mPythonClass._machine)], key=operator.itemgetter(0)) self.assertEqual(expectedMachines, machines) Automat-0.6.0/automat/_test/test_methodical.py0000644000175000017500000004010513106675015021101 0ustar mrwmrw00000000000000 """ Tests for the public interface of Automat. """ from functools import reduce from unittest import TestCase from .. import MethodicalMachine, NoTransition from .. import _methodical class MethodicalTests(TestCase): """ Tests for L{MethodicalMachine}. """ def test_oneTransition(self): """ L{MethodicalMachine} provides a way for you to declare a state machine with inputs, outputs, and states as methods. When you have declared an input, an output, and a state, calling the input method in that state will produce the specified output. """ class Machination(object): machine = MethodicalMachine() @machine.input() def anInput(self): "an input" @machine.output() def anOutput(self): "an output" return "an-output-value" @machine.output() def anotherOutput(self): "another output" return "another-output-value" @machine.state(initial=True) def anState(self): "a state" @machine.state() def anotherState(self): "another state" anState.upon(anInput, enter=anotherState, outputs=[anOutput]) anotherState.upon(anInput, enter=anotherState, outputs=[anotherOutput]) m = Machination() self.assertEqual(m.anInput(), ["an-output-value"]) self.assertEqual(m.anInput(), ["another-output-value"]) def test_machineItselfIsPrivate(self): """ L{MethodicalMachine} is an implementation detail. If you attempt to access it on an instance of your class, you will get an exception. However, since tools may need to access it for the purposes of, for example, visualization, you may access it on the class itself. """ expectedMachine = MethodicalMachine() class Machination(object): machine = expectedMachine machination = Machination() with self.assertRaises(AttributeError) as cm: machination.machine self.assertIn("MethodicalMachine is an implementation detail", str(cm.exception)) self.assertIs(Machination.machine, expectedMachine) def test_outputsArePrivate(self): """ One of the benefits of using a state machine is that your output method implementations don't need to take invalid state transitions into account - the methods simply won't be called. This property would be broken if client code called output methods directly, so output methods are not directly visible under their names. """ class Machination(object): machine = MethodicalMachine() counter = 0 @machine.input() def anInput(self): "an input" @machine.output() def anOutput(self): self.counter += 1 @machine.state(initial=True) def state(self): "a machine state" state.upon(anInput, enter=state, outputs=[anOutput]) mach1 = Machination() mach1.anInput() self.assertEqual(mach1.counter, 1) mach2 = Machination() with self.assertRaises(AttributeError) as cm: mach2.anOutput self.assertEqual(mach2.counter, 0) self.assertIn( "Machination.anOutput is a state-machine output method; to " "produce this output, call an input method instead.", str(cm.exception) ) def test_multipleMachines(self): """ Two machines may co-exist happily on the same instance; they don't interfere with each other. """ class MultiMach(object): a = MethodicalMachine() b = MethodicalMachine() @a.input() def inputA(self): "input A" @b.input() def inputB(self): "input B" @a.state(initial=True) def initialA(self): "initial A" @b.state(initial=True) def initialB(self): "initial B" @a.output() def outputA(self): return "A" @b.output() def outputB(self): return "B" initialA.upon(inputA, initialA, [outputA]) initialB.upon(inputB, initialB, [outputB]) mm = MultiMach() self.assertEqual(mm.inputA(), ["A"]) self.assertEqual(mm.inputB(), ["B"]) def test_collectOutputs(self): """ Outputs can be combined with the "collector" argument to "upon". """ import operator class Machine(object): m = MethodicalMachine() @m.input() def input(self): "an input" @m.output() def outputA(self): return "A" @m.output() def outputB(self): return "B" @m.state(initial=True) def state(self): "a state" state.upon(input, state, [outputA, outputB], collector=lambda x: reduce(operator.add, x)) m = Machine() self.assertEqual(m.input(), "AB") def test_methodName(self): """ Input methods preserve their declared names. """ class Mech(object): m = MethodicalMachine() @m.input() def declaredInputName(self): "an input" @m.state(initial=True) def aState(self): "state" m = Mech() with self.assertRaises(TypeError) as cm: m.declaredInputName("too", "many", "arguments") self.assertIn("declaredInputName", str(cm.exception)) def test_inputWithArguments(self): """ If an input takes an argument, it will pass that along to its output. """ class Mechanism(object): m = MethodicalMachine() @m.input() def input(self, x, y=1): "an input" @m.state(initial=True) def state(self): "a state" @m.output() def output(self, x, y=1): self._x = x return x + y state.upon(input, state, [output]) m = Mechanism() self.assertEqual(m.input(3), [4]) self.assertEqual(m._x, 3) def test_inputFunctionsMustBeEmpty(self): """ The wrapped input function must have an empty body. """ # input functions are executed to assert that the signature matches, # but their body must be empty _methodical._empty() # chase coverage _methodical._docstring() class Mechanism(object): m = MethodicalMachine() with self.assertRaises(ValueError) as cm: @m.input() def input(self): "an input" list() # pragma: no cover self.assertEqual(str(cm.exception), "function body must be empty") # all three of these cases should be valid. Functions/methods with # docstrings produce slightly different bytecode than ones without. class MechanismWithDocstring(object): m = MethodicalMachine() @m.input() def input(self): "an input" @m.state(initial=True) def start(self): "starting state" start.upon(input, enter=start, outputs=[]) MechanismWithDocstring().input() class MechanismWithPass(object): m = MethodicalMachine() @m.input() def input(self): pass @m.state(initial=True) def start(self): "starting state" start.upon(input, enter=start, outputs=[]) MechanismWithPass().input() class MechanismWithDocstringAndPass(object): m = MethodicalMachine() @m.input() def input(self): "an input" pass @m.state(initial=True) def start(self): "starting state" start.upon(input, enter=start, outputs=[]) MechanismWithDocstringAndPass().input() class MechanismReturnsNone(object): m = MethodicalMachine() @m.input() def input(self): return None @m.state(initial=True) def start(self): "starting state" start.upon(input, enter=start, outputs=[]) MechanismReturnsNone().input() class MechanismWithDocstringAndReturnsNone(object): m = MethodicalMachine() @m.input() def input(self): "an input" return None @m.state(initial=True) def start(self): "starting state" start.upon(input, enter=start, outputs=[]) MechanismWithDocstringAndReturnsNone().input() def test_inputOutputMismatch(self): """ All the argument lists of the outputs for a given input must match; if one does not the call to C{upon} will raise a C{TypeError}. """ class Mechanism(object): m = MethodicalMachine() @m.input() def nameOfInput(self, a): "an input" @m.output() def outputThatMatches(self, a): "an output that matches" @m.output() def outputThatDoesntMatch(self, b): "an output that doesn't match" @m.state() def state(self): "a state" with self.assertRaises(TypeError) as cm: state.upon(nameOfInput, state, [outputThatMatches, outputThatDoesntMatch]) self.assertIn("nameOfInput", str(cm.exception)) self.assertIn("outputThatDoesntMatch", str(cm.exception)) def test_multipleInitialStatesFailure(self): """ A L{MethodicalMachine} can only have one initial state. """ class WillFail(object): m = MethodicalMachine() @m.state(initial=True) def firstInitialState(self): "The first initial state -- this is OK." with self.assertRaises(ValueError): @m.state(initial=True) def secondInitialState(self): "The second initial state -- results in a ValueError." def test_multipleTransitionsFailure(self): """ A L{MethodicalMachine} can only have one transition per start/event pair. """ class WillFail(object): m = MethodicalMachine() @m.state(initial=True) def start(self): "We start here." @m.state() def end(self): "Rainbows end." @m.input() def event(self): "An event." start.upon(event, enter=end, outputs=[]) with self.assertRaises(ValueError): start.upon(event, enter=end, outputs=[]) def test_badTransitionForCurrentState(self): """ Calling any input method that lacks a transition for the machine's current state raises an informative L{NoTransition}. """ class OnlyOnePath(object): m = MethodicalMachine() @m.state(initial=True) def start(self): "Start state." @m.state() def end(self): "End state." @m.input() def advance(self): "Move from start to end." @m.input() def deadEnd(self): "A transition from nowhere to nowhere." start.upon(advance, end, []) machine = OnlyOnePath() with self.assertRaises(NoTransition) as cm: machine.deadEnd() self.assertIn("deadEnd", str(cm.exception)) self.assertIn("start", str(cm.exception)) machine.advance() with self.assertRaises(NoTransition) as cm: machine.deadEnd() self.assertIn("deadEnd", str(cm.exception)) self.assertIn("end", str(cm.exception)) def test_saveState(self): """ L{MethodicalMachine.serializer} is a decorator that modifies its decoratee's signature to take a "state" object as its first argument, which is the "serialized" argument to the L{MethodicalMachine.state} decorator. """ class Mechanism(object): m = MethodicalMachine() def __init__(self): self.value = 1 @m.state(serialized="first-state", initial=True) def first(self): "First state." @m.state(serialized="second-state") def second(self): "Second state." @m.serializer() def save(self, state): return { 'machine-state': state, 'some-value': self.value, } self.assertEqual( Mechanism().save(), { "machine-state": "first-state", "some-value": 1, } ) def test_restoreState(self): """ L{MethodicalMachine.unserializer} decorates a function that becomes a machine-state unserializer; its return value is mapped to the C{serialized} parameter to C{state}, and the L{MethodicalMachine} associated with that instance's state is updated to that state. """ class Mechanism(object): m = MethodicalMachine() def __init__(self): self.value = 1 self.ranOutput = False @m.state(serialized="first-state", initial=True) def first(self): "First state." @m.state(serialized="second-state") def second(self): "Second state." @m.input() def input(self): "an input" @m.output() def output(self): self.value = 2 self.ranOutput = True return 1 @m.output() def output2(self): return 2 first.upon(input, second, [output], collector=lambda x: list(x)[0]) second.upon(input, second, [output2], collector=lambda x: list(x)[0]) @m.serializer() def save(self, state): return { 'machine-state': state, 'some-value': self.value, } @m.unserializer() def _restore(self, blob): self.value = blob['some-value'] return blob['machine-state'] @classmethod def fromBlob(cls, blob): self = cls() self._restore(blob) return self m1 = Mechanism() m1.input() blob = m1.save() m2 = Mechanism.fromBlob(blob) self.assertEqual(m2.ranOutput, False) self.assertEqual(m2.input(), 2) self.assertEqual( m2.save(), { 'machine-state': 'second-state', 'some-value': 2, } ) # FIXME: error for wrong types on any call to _oneTransition # FIXME: better public API for .upon; maybe a context manager? # FIXME: when transitions are defined, validate that we can always get to # terminal? do we care about this? # FIXME: implementation (and use-case/example) for passing args from in to out # FIXME: possibly these need some kind of support from core # FIXME: wildcard state (in all states, when input X, emit Y and go to Z) # FIXME: wildcard input (in state X, when any input, emit Y and go to Z) # FIXME: combined wildcards (in any state for any input, emit Y go to Z) Automat-0.6.0/automat/_test/test_trace.py0000644000175000017500000000631713106675015020075 0ustar mrwmrw00000000000000from unittest import TestCase from .._methodical import MethodicalMachine class SampleObject(object): mm = MethodicalMachine() @mm.state(initial=True) def begin(self): "initial state" @mm.state() def middle(self): "middle state" @mm.state() def end(self): "end state" @mm.input() def go1(self): "sample input" @mm.input() def go2(self): "sample input" @mm.input() def back(self): "sample input" @mm.output() def out(self): "sample output" setTrace = mm._setTrace begin.upon(go1, middle, [out]) middle.upon(go2, end, [out]) end.upon(back, middle, []) middle.upon(back, begin, []) class TraceTests(TestCase): def test_only_inputs(self): traces = [] def tracer(old_state, input, new_state): traces.append((old_state, input, new_state)) return None # "I only care about inputs, not outputs" s = SampleObject() s.setTrace(tracer) s.go1() self.assertEqual(traces, [("begin", "go1", "middle"), ]) s.go2() self.assertEqual(traces, [("begin", "go1", "middle"), ("middle", "go2", "end"), ]) s.setTrace(None) s.back() self.assertEqual(traces, [("begin", "go1", "middle"), ("middle", "go2", "end"), ]) s.go2() self.assertEqual(traces, [("begin", "go1", "middle"), ("middle", "go2", "end"), ]) def test_inputs_and_outputs(self): traces = [] def tracer(old_state, input, new_state): traces.append((old_state, input, new_state, None)) def trace_outputs(output): traces.append((old_state, input, new_state, output)) return trace_outputs # "I care about outputs too" s = SampleObject() s.setTrace(tracer) s.go1() self.assertEqual(traces, [("begin", "go1", "middle", None), ("begin", "go1", "middle", "out"), ]) s.go2() self.assertEqual(traces, [("begin", "go1", "middle", None), ("begin", "go1", "middle", "out"), ("middle", "go2", "end", None), ("middle", "go2", "end", "out"), ]) s.setTrace(None) s.back() self.assertEqual(traces, [("begin", "go1", "middle", None), ("begin", "go1", "middle", "out"), ("middle", "go2", "end", None), ("middle", "go2", "end", "out"), ]) s.go2() self.assertEqual(traces, [("begin", "go1", "middle", None), ("begin", "go1", "middle", "out"), ("middle", "go2", "end", None), ("middle", "go2", "end", "out"), ]) Automat-0.6.0/automat/_test/test_visualize.py0000644000175000017500000003266013072723343021012 0ustar mrwmrw00000000000000from __future__ import print_function import functools import os import subprocess from unittest import TestCase, skipIf import attr from .._methodical import MethodicalMachine from .test_discover import isTwistedInstalled def isGraphvizModuleInstalled(): """ Is the graphviz Python module installed? """ try: __import__("graphviz") except ImportError: return False else: return True def isGraphvizInstalled(): """ Are the graphviz tools installed? """ r, w = os.pipe() os.close(w) try: return not subprocess.call("dot", stdin=r, shell=True) finally: os.close(r) def sampleMachine(): """ Create a sample L{MethodicalMachine} with some sample states. """ mm = MethodicalMachine() class SampleObject(object): @mm.state(initial=True) def begin(self): "initial state" @mm.state() def end(self): "end state" @mm.input() def go(self): "sample input" @mm.output() def out(self): "sample output" begin.upon(go, end, [out]) so = SampleObject() so.go() return mm @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class ElementMakerTests(TestCase): """ L{elementMaker} generates HTML representing the specified element. """ def setUp(self): from .._visualize import elementMaker self.elementMaker = elementMaker def test_sortsAttrs(self): """ L{elementMaker} orders HTML attributes lexicographically. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div", b='2', a='1', c='3')) def test_quotesAttrs(self): """ L{elementMaker} quotes HTML attributes according to DOT's quoting rule. See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div", b='a " quote', a=1, c="a string")) def test_noAttrs(self): """ L{elementMaker} should render an element with no attributes. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div")) @attr.s class HTMLElement(object): """Holds an HTML element, as created by elementMaker.""" name = attr.ib() children = attr.ib() attributes = attr.ib() def findElements(element, predicate): """ Recursively collect all elements in an L{HTMLElement} tree that match the optional predicate. """ if predicate(element): return [element] elif isLeaf(element): return [] return [result for child in element.children for result in findElements(child, predicate)] def isLeaf(element): """ This HTML element is actually leaf node. """ return not isinstance(element, HTMLElement) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class TableMakerTests(TestCase): """ Tests that ensure L{tableMaker} generates HTML tables usable as labels in DOT graphs. For more information, read the "HTML-Like Labels" section of U{http://www.graphviz.org/doc/info/shapes.html}. """ def fakeElementMaker(self, name, *children, **attributes): return HTMLElement(name=name, children=children, attributes=attributes) def setUp(self): from .._visualize import tableMaker self.inputLabel = "input label" self.port = "the port" self.tableMaker = functools.partial(tableMaker, _E=self.fakeElementMaker) def test_inputLabelRow(self): """ The table returned by L{tableMaker} always contains the input symbol label in its first row, and that row contains one cell with a port attribute set to the provided port. """ def hasPort(element): return (not isLeaf(element) and element.attributes.get("port") == self.port) for outputLabels in ([], ["an output label"]): table = self.tableMaker(self.inputLabel, outputLabels, port=self.port) self.assertGreater(len(table.children), 0) inputLabelRow = table.children[0] portCandidates = findElements(table, hasPort) self.assertEqual(len(portCandidates), 1) self.assertEqual(portCandidates[0].name, "td") self.assertEqual(findElements(inputLabelRow, isLeaf), [self.inputLabel]) def test_noOutputLabels(self): """ L{tableMaker} does not add a colspan attribute to the input label's cell or a second row if there no output labels. """ table = self.tableMaker("input label", (), port=self.port) self.assertEqual(len(table.children), 1) (inputLabelRow,) = table.children self.assertNotIn("colspan", inputLabelRow.attributes) def test_withOutputLabels(self): """ L{tableMaker} adds a colspan attribute to the input label's cell equal to the number of output labels and a second row that contains the output labels. """ table = self.tableMaker(self.inputLabel, ("output label 1", "output label 2"), port=self.port) self.assertEqual(len(table.children), 2) inputRow, outputRow = table.children def hasCorrectColspan(element): return (not isLeaf(element) and element.name == "td" and element.attributes.get('colspan') == "2") self.assertEqual(len(findElements(inputRow, hasCorrectColspan)), 1) self.assertEqual(findElements(outputRow, isLeaf), ["output label 1", "output label 2"]) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.") class IntegrationTests(TestCase): """ Tests which make sure Graphviz can understand the output produced by Automat. """ def test_validGraphviz(self): """ L{graphviz} emits valid graphviz data. """ p = subprocess.Popen("dot", stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, err = p.communicate("".join(sampleMachine().asDigraph()) .encode("utf-8")) self.assertEqual(p.returncode, 0) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class SpotChecks(TestCase): """ Tests to make sure that the output contains salient features of the machine being generated. """ def test_containsMachineFeatures(self): """ The output of L{graphviz} should contain the names of the states, inputs, outputs in the state machine. """ gvout = "".join(sampleMachine().asDigraph()) self.assertIn("begin", gvout) self.assertIn("end", gvout) self.assertIn("go", gvout) self.assertIn("out", gvout) class RecordsDigraphActions(object): """ Records calls made to L{FakeDigraph}. """ def __init__(self): self.reset() def reset(self): self.renderCalls = [] self.saveCalls = [] class FakeDigraph(object): """ A fake L{graphviz.Digraph}. Instantiate it with a L{RecordsDigraphActions}. """ def __init__(self, recorder): self._recorder = recorder def render(self, **kwargs): self._recorder.renderCalls.append(kwargs) def save(self, **kwargs): self._recorder.saveCalls.append(kwargs) class FakeMethodicalMachine(object): """ A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph} """ def __init__(self, digraph): self._digraph = digraph def asDigraph(self): return self._digraph @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.") @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class VisualizeToolTests(TestCase): def setUp(self): self.digraphRecorder = RecordsDigraphActions() self.fakeDigraph = FakeDigraph(self.digraphRecorder) self.fakeProgname = 'tool-test' self.fakeSysPath = ['ignored'] self.collectedOutput = [] self.fakeFQPN = 'fake.fqpn' def collectPrints(self, *args): self.collectedOutput.append(' '.join(args)) def fakeFindMachines(self, fqpn): yield fqpn, FakeMethodicalMachine(self.fakeDigraph) def tool(self, progname=None, argv=None, syspath=None, findMachines=None, print=None): from .._visualize import tool return tool( _progname=progname or self.fakeProgname, _argv=argv or [self.fakeFQPN], _syspath=syspath or self.fakeSysPath, _findMachines=findMachines or self.fakeFindMachines, _print=print or self.collectPrints) def test_checksCurrentDirectory(self): """ L{tool} adds '' to sys.path to ensure L{automat._discover.findMachines} searches the current directory. """ self.tool(argv=[self.fakeFQPN]) self.assertEqual(self.fakeSysPath[0], '') def test_quietHidesOutput(self): """ Passing -q/--quiet hides all output. """ self.tool(argv=[self.fakeFQPN, '--quiet']) self.assertFalse(self.collectedOutput) self.tool(argv=[self.fakeFQPN, '-q']) self.assertFalse(self.collectedOutput) def test_onlySaveDot(self): """ Passing an empty string for --image-directory/-i disables rendering images. """ for arg in ('--image-directory', '-i'): self.digraphRecorder.reset() self.collectedOutput = [] self.tool(argv=[self.fakeFQPN, arg, '']) self.assertFalse(any("image" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.saveCalls), 1) (call,) = self.digraphRecorder.saveCalls self.assertEqual("{}.dot".format(self.fakeFQPN), call['filename']) self.assertFalse(self.digraphRecorder.renderCalls) def test_saveOnlyImage(self): """ Passing an empty string for --dot-directory/-d disables saving dot files. """ for arg in ('--dot-directory', '-d'): self.digraphRecorder.reset() self.collectedOutput = [] self.tool(argv=[self.fakeFQPN, arg, '']) self.assertFalse(any("dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (call,) = self.digraphRecorder.renderCalls self.assertEqual("{}.dot".format(self.fakeFQPN), call['filename']) self.assertTrue(call['cleanup']) self.assertFalse(self.digraphRecorder.saveCalls) def test_saveDotAndImagesInDifferentDirectories(self): """ Passing different directories to --image-directory and --dot-directory writes images and dot files to those directories. """ imageDirectory = 'image' dotDirectory = 'dot' self.tool(argv=[self.fakeFQPN, '--image-directory', imageDirectory, '--dot-directory', dotDirectory]) self.assertTrue(any("image" in line for line in self.collectedOutput)) self.assertTrue(any("dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (renderCall,) = self.digraphRecorder.renderCalls self.assertEqual(renderCall["directory"], imageDirectory) self.assertTrue(renderCall['cleanup']) self.assertEqual(len(self.digraphRecorder.saveCalls), 1) (saveCall,) = self.digraphRecorder.saveCalls self.assertEqual(saveCall["directory"], dotDirectory) def test_saveDotAndImagesInSameDirectory(self): """ Passing the same directory to --image-directory and --dot-directory writes images and dot files to that one directory. """ directory = 'imagesAndDot' self.tool(argv=[self.fakeFQPN, '--image-directory', directory, '--dot-directory', directory]) self.assertTrue(any("image and dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (renderCall,) = self.digraphRecorder.renderCalls self.assertEqual(renderCall["directory"], directory) self.assertFalse(renderCall['cleanup']) self.assertFalse(len(self.digraphRecorder.saveCalls)) Automat-0.6.0/automat/_visualize.py0000644000175000017500000001427713106701777017005 0ustar mrwmrw00000000000000from __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}'.format( name=name, attrs=formattedAttrs, children=formattedChildren) def tableMaker(inputLabel, outputLabels, port, _E=elementMaker): """ Construct an HTML table to label a state transition. """ colspan = {} if outputLabels: colspan['colspan'] = str(len(outputLabels)) inputLabelCell = _E("td", _E("font", inputLabel, face="menlo-italic"), color="purple", port=port, **colspan) pointSize = {"point-size": "9"} outputLabelCells = [_E("td", _E("font", outputLabel, **pointSize), color="pink") for outputLabel in outputLabels] rows = [_E("tr", inputLabelCell)] if outputLabels: rows.append(_E("tr", *outputLabelCells)) return _E("table", *rows) def makeDigraph(automaton, inputAsString=repr, outputAsString=repr, stateAsString=repr): """ Produce a L{graphviz.Digraph} object from an automaton. """ digraph = graphviz.Digraph(graph_attr={'pack': 'true', 'dpi': '100'}, node_attr={'fontname': 'Menlo'}, edge_attr={'fontname': 'Menlo'}) for state in automaton.states(): if state is automaton.initialState: stateShape = "bold" fontName = "Menlo-Bold" else: stateShape = "" fontName = "Menlo" digraph.node(stateAsString(state), fontame=fontName, shape="ellipse", style=stateShape, color="blue") for n, eachTransition in enumerate(automaton.allTransitions()): inState, inputSymbol, outState, outputSymbols = eachTransition thisTransition = "t{}".format(n) inputLabel = inputAsString(inputSymbol) port = "tableport" table = tableMaker(inputLabel, [outputAsString(outputSymbol) for outputSymbol in outputSymbols], port=port) digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none") digraph.edge(stateAsString(inState), '{}:{}:w'.format(thisTransition, port), arrowhead="none") digraph.edge('{}:{}:e'.format(thisTransition, port), stateAsString(outState)) return digraph def tool(_progname=sys.argv[0], _argv=sys.argv[1:], _syspath=sys.path, _findMachines=findMachines, _print=print): """ Entry point for command line utility. """ DESCRIPTION = """ Visualize automat.MethodicalMachines as graphviz graphs. """ EPILOG = """ You must have the graphviz tool suite installed. Please visit http://www.graphviz.org for more information. """ if _syspath[0]: _syspath.insert(0, '') argumentParser = argparse.ArgumentParser( prog=_progname, description=DESCRIPTION, epilog=EPILOG) argumentParser.add_argument('fqpn', help="A Fully Qualified Path name" " representing where to find machines.") argumentParser.add_argument('--quiet', '-q', help="suppress output", default=False, action="store_true") argumentParser.add_argument('--dot-directory', '-d', help="Where to write out .dot files.", default=".automat_visualize") argumentParser.add_argument('--image-directory', '-i', help="Where to write out image files.", default=".automat_visualize") argumentParser.add_argument('--image-type', '-t', help="The image format.", choices=graphviz.FORMATS, default='png') argumentParser.add_argument('--view', '-v', help="View rendered graphs with" " default image viewer", default=False, action="store_true") args = argumentParser.parse_args(_argv) explicitlySaveDot = (args.dot_directory and (not args.image_directory or args.image_directory != args.dot_directory)) if args.quiet: def _print(*args): pass for fqpn, machine in _findMachines(args.fqpn): _print(fqpn, '...discovered') digraph = machine.asDigraph() if explicitlySaveDot: digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory) _print(fqpn, "...wrote dot into", args.dot_directory) if args.image_directory: deleteDot = not args.dot_directory or explicitlySaveDot digraph.format = args.image_type digraph.render(filename="{}.dot".format(fqpn), directory=args.image_directory, view=args.view, cleanup=deleteDot) if deleteDot: msg = "...wrote image into" else: msg = "...wrote image and dot into" _print(fqpn, msg, args.image_directory) Automat-0.6.0/docs/0000755000175000017500000000000013106704143013512 5ustar mrwmrw00000000000000Automat-0.6.0/docs/debugging.md0000644000175000017500000001205013106675015015772 0ustar mrwmrw00000000000000# Tracing API (NOTE: the Tracing API is currently private and unstable. Use it for local debugging, but if you think you need to commit code that references it, you should either pin your dependency on the current version of Automat, or at least be prepared for your application to break when this API is changed or removed). The tracing API lets you assign a callback function that will be invoked each time an input event causes the state machine to move from one state to another. This can help you figure out problems caused by events occurring in the wrong order, or not happening at all. Your callback function can print a message to stdout, write something to a logfile, or deliver the information in any application-specific way you like. The only restriction is that the function must not touch the state machine at all. To prepare the state machine for tracing, you must assign a name to the "_setTrace" method in your class. In this example, we use `setTheTracingFunction`, but the name can be anything you like: ```python class Sample(object): mm = MethodicalMachine() @mm.state(initial=True) def begin(self): "initial state" @mm.state() def end(self): "end state" @mm.input() def go(self): "event that moves us from begin to end" @mm.output() def doThing1(self): "first thing to do" @mm.output() def doThing2(self): "second thing to do" setTheTracingFunction = mm._setTrace begin.upon(go, enter=end, outputs=[doThing1, doThing2]) ``` Later, after you instantiate the `Sample` object, you can set the tracing callback for that particular instance by calling the `setTheTracingFunction()` method on it: ```python s = Sample() def tracer(oldState, input, newState): pass s.setTheTracingFunction(tracer) ``` Note that you cannot shortcut the name-assignment step: `s.mm._setTrace(tracer)` will not work, because Automat goes to great lengths to hide that `mm` object from external access. And you cannot set the tracing function at class-definition time (e.g. a class-level `mm._setTrace(tracer)`) because the state machine has merely been *defined* at that point, not instantiated (you might eventually have multiple instances of the Sample class, each with their own independent state machine), and each one can be traced separately. Since this is a private API, consider using a tolerant `getattr` when retrieving the `_getTrace` method. This way, if you do commit code which references it, but you only *call* that code during debugging, then at least your application or tests won't crash when the API is removed entirely: ``` mm = MethodicalMachine() setTheTracingFunction = getattr(mm, "_setTrace", lambda self, f: None) ``` ## The Tracer Callback Function When the input event is received, before any transitions are made, the tracer function is called with three positional arguments: * `oldState`: a string with the name of the current state * `input`: a string with the name of the input event * `newState`: a string with the name of the new state If your tracer function returns None, then you will only be notified about the input events. But, if your tracer function returns a callable, then just before each output function is executed (if any), that callable will be executed with a single `output` argument (as a string). So if you only care about the transitions, your tracing function can just do: ```python s = Sample() def tracer(oldState, input, newState): print("%s.%s -> %s" % (oldState, input, newState)) s.setTheTracingFunction(tracer) s.go() # prints: # begin.go -> end ``` But if you want to know when each output is invoked (perhaps to compare against other log messages emitted from inside those output functions), you can do: ```python s = Sample() def tracer(oldState, input, newState): def traceOutputs(output): print("%s.%s -> %s: %s()" % (oldState, input, newState, output)) print("%s.%s -> %s" % (oldState, input, newState)) return traceOutputs s.setTheTracingFunction(tracer) s.go() # prints: # begin.go -> end # begin.go -> end: doThing1() # begin.go -> end: doThing2() ``` ## Tracing Multiple State Machines If you have multiple state machines in your application, you will probably want to pass a different tracing function to each, so your logs can distinguish between the transitions of MachineFoo vs those of MachineBar. This is particularly important if your application involves network communication, where an instance of MachineFoo (e.g. in a client) is communication with a sibling instance of MachineFoo (in a server). When exercising both sides of this connection in a single process, perhaps in an automated test, you will need to clearly mark the first as "foo1" and the second as "foo2" to avoid confusion. ```python s1 = Sample() s2 = Sample() def tracer1(oldState, input, newState): print("S1: %s.%s -> %s" % (oldState, input, newState)) s1.setTheTracingFunction(tracer1) def tracer2(oldState, input, newState): print("S2: %s.%s -> %s" % (oldState, input, newState)) s2.setTheTracingFunction(tracer2) ``` Automat-0.6.0/docs/examples/0000755000175000017500000000000013106704143015330 5ustar mrwmrw00000000000000Automat-0.6.0/docs/examples/automat_example.py0000644000175000017500000000650213072723343021077 0ustar mrwmrw00000000000000 from automat import MethodicalMachine class Door(object): def unlock(self): print("Opening the door so you can get your food.") def lock(self): print("Locking the door so you can't steal the food.") class Light(object): def on(self): print("Need some food over here.") def off(self): print("We're good on food for now.") class FoodSlot(object): """ Automats were a popular kind of business in the 1950s and 60s; a sort of restaurant-sized vending machine that served cooked food out of a coin-operated dispenser. This class represents the logic associated with a single food slot. """ machine = MethodicalMachine() def __init__(self, door, light): self._door = door self._light = light self.start() @machine.state(initial=True) def initial(self): """ The initial state when we are constructed. Note that applications never see this state, because the constructor provides an input to transition out of it immediately. """ @machine.state() def empty(self): """ The machine is empty (and the light asking for food is on). """ @machine.input() def start(self): """ A private input, for transitioning to the initial blank state to 'empty', making sure the door and light are properly configured. """ @machine.state() def ready(self): """ We've got some food and we're ready to serve it. """ @machine.state() def serving(self): """ The door is open, we're serving food. """ @machine.input() def coin(self): """ A coin (of the appropriate denomination) was inserted. """ @machine.input() def food(self): """ Food was prepared and inserted into the back of the machine. """ @machine.output() def turnOnFoodLight(self): """ Turn on the 'we need food' light. """ self._light.on() @machine.output() def turnOffFoodLight(self): """ Turn off the 'we need food' light. """ self._light.off() @machine.output() def lockDoor(self): """ Lock the door, we don't need food. """ self._door.lock() @machine.output() def unlockDoor(self): """ Lock the door, we don't need food. """ self._door.unlock() @machine.input() def closeDoor(self): """ The door was closed. """ initial.upon(start, enter=empty, outputs=[lockDoor, turnOnFoodLight]) empty.upon(food, enter=ready, outputs=[turnOffFoodLight]) ready.upon(coin, enter=serving, outputs=[unlockDoor]) serving.upon(closeDoor, enter=empty, outputs=[lockDoor, turnOnFoodLight]) slot = FoodSlot(Door(), Light()) if __name__ == '__main__': import sys sys.stdout.writelines(FoodSlot.machine.asDigraph()) # raw_input("Hit enter to make some food and put it in the slot: ") # slot.food() # raw_input("Hit enter to insert a coin: ") # slot.coin() # raw_input("Hit enter to retrieve the food and close the door: ") # slot.closeDoor() # raw_input("Hit enter to make some more food: ") # slot.food() Automat-0.6.0/docs/examples/io_coffee_example.py0000644000175000017500000000252412641273151021341 0ustar mrwmrw00000000000000from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() @_machine.input() def brew_button(self): "The user pressed the 'brew' button." @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." # self._heating_element.turn_on() @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." @_machine.input() def put_in_beans(self, beans): "The user put in some beans." @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) cb = CoffeeBrewer() cb.put_in_beans("real good beans") print(cb.brew_button()) Automat-0.6.0/docs/examples/lightswitch.py0000644000175000017500000000227012641273151020237 0ustar mrwmrw00000000000000from automat import MethodicalMachine class LightSwitch(object): machine = MethodicalMachine() @machine.state(serialized="on") def on_state(self): "the switch is on" @machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) @machine.input() def query_power(self): "return True if powered, False otherwise" @machine.output() def _is_powered(self): return True @machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) @machine.serializer() def save(self, state): return {"is-it-on": state} @machine.unserializer() def _restore(self, blob): return blob["is-it-on"] @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Automat-0.6.0/docs/examples/turnstile_example.py0000644000175000017500000000231212641273151021447 0ustar mrwmrw00000000000000from automat import MethodicalMachine class Turnstile(object): machine = MethodicalMachine() def __init__(self, lock): self.lock = lock @machine.input() def arm_turned(self): "The arm was turned." @machine.input() def fare_paid(self): "The fare was paid." @machine.output() def _engage_lock(self): self.lock.engage() @machine.output() def _disengage_lock(self): self.lock.disengage() @machine.output() def _nope(self): print("**Clunk!** The turnstile doesn't move.") @machine.state(initial=True) def _locked(self): "The turnstile is locked." @machine.state() def _unlocked(self): "The turnstile is unlocked." _locked.upon(fare_paid, enter=_unlocked, outputs=[_disengage_lock]) _unlocked.upon(arm_turned, enter=_locked, outputs=[_engage_lock]) _locked.upon(arm_turned, enter=_locked, outputs=[_nope]) class Lock(object): "A sample I/O device." def engage(self): print("Locked.") def disengage(self): print("Unlocked.") turner = Turnstile(Lock()) turner.fare_paid() turner.arm_turned() turner.arm_turned() turner.fare_paid() turner.arm_turned() Automat-0.6.0/setup.cfg0000644000175000017500000000013013106704143014375 0ustar mrwmrw00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 Automat-0.6.0/setup.py0000644000175000017500000000232013106676046014303 0ustar mrwmrw00000000000000""" Setup file for automat """ from setuptools import setup, find_packages try: from m2r import parse_from_file long_description = parse_from_file('README.md') except(IOError, ImportError): print("\n\n!!! m2r not found, long_description is bad, don't upload this to PyPI !!!\n\n") import io long_description = io.open('README.md', encoding="utf-8").read() setup( name='Automat', use_scm_version=True, url='https://github.com/glyph/Automat', description=""" Self-service finite-state machines for the programmer on the go. """.strip(), long_description=long_description, packages=find_packages(exclude=[]), package_dir={'automat': 'automat'}, setup_requires=[ 'setuptools-scm', 'm2r', ], install_requires=[ "attrs", "six", ], extras_require={ "visualize": ["graphviz>0.5.1", "Twisted>=16.1.1"], }, entry_points={ "console_scripts": [ "automat-visualize = automat._visualize:tool" ], }, author='Glyph', author_email='glyph@twistedmatrix.com', include_package_data=True, license="MIT", keywords='fsm finite state machine automata', ) Automat-0.6.0/tox.ini0000644000175000017500000000074313106675015014106 0ustar mrwmrw00000000000000[tox] envlist = coverage-clean,{py27,pypy,py33,py34,py35,py36}-{extras,noextras},coverage-report [testenv] deps = extras: graphviz>=0.4.9 extras: Twisted>=16.2.0 coverage pytest commands = coverage run --parallel --source automat -m py.test automat/_test [testenv:coverage-clean] deps = coverage skip_install = true commands = coverage erase [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine coverage report -m