Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Automat for openSUSE:Factory checked in at 2023-01-11 17:14:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Automat (Old) and /work/SRC/openSUSE:Factory/.python-Automat.new.32243 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Automat" Wed Jan 11 17:14:15 2023 rev:6 rq:1057654 version:22.10.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Automat/python-Automat.changes 2021-12-12 21:27:41.084344216 +0100 +++ /work/SRC/openSUSE:Factory/.python-Automat.new.32243/python-Automat.changes 2023-01-11 17:14:16.943557337 +0100 @@ -1,0 +2,8 @@ +Tue Jan 10 11:21:35 UTC 2023 - Daniel Garcia <daniel.gar...@suse.com> + +- Update to 22.10.0 + * Fix _test_visualize.py twisted import errors + * Fix #17: Allow enter to have a default + * Use CodeType.replace() in copycode for Python > 3.8 + +------------------------------------------------------------------- Old: ---- Automat-20.2.0.tar.gz New: ---- Automat-22.10.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Automat.spec ++++++ --- /var/tmp/diff_new_pack.CC2mah/_old 2023-01-11 17:14:17.623561293 +0100 +++ /var/tmp/diff_new_pack.CC2mah/_new 2023-01-11 17:14:17.627561316 +0100 @@ -1,7 +1,7 @@ # # spec file # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2023 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -25,10 +25,9 @@ %bcond_with test %endif -%{?!python_module:%define python_module() python-%{**} python3-%{**}} %bcond_without python2 Name: python-Automat%{psuffix} -Version: 20.2.0 +Version: 22.10.0 Release: 0 Summary: Self-service finite-state machines for the programmer on the go License: MIT @@ -36,10 +35,10 @@ Source: https://files.pythonhosted.org/packages/source/A/Automat/Automat-%{version}.tar.gz BuildRequires: %{python_module setuptools_scm} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-attrs >= 16.1.0 -Requires: python-six +Requires: python-attrs >= 19.2.0 Requires(post): update-alternatives Requires(preun):update-alternatives Suggests: python-Twisted >= 16.1.1 @@ -47,7 +46,7 @@ BuildArch: noarch %if %{with test} BuildRequires: %{python_module Twisted >= 16.1.1} -BuildRequires: %{python_module attrs >= 16.1.0} +BuildRequires: %{python_module attrs >= 19.2.0} BuildRequires: %{python_module graphviz >= 0.5.1} BuildRequires: %{python_module pytest} %if %{with python2} ++++++ Automat-20.2.0.tar.gz -> Automat-22.10.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/.github/workflows/ci.yml new/Automat-22.10.0/.github/workflows/ci.yml --- old/Automat-20.2.0/.github/workflows/ci.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/Automat-22.10.0/.github/workflows/ci.yml 2022-10-29 08:56:12.000000000 +0200 @@ -0,0 +1,41 @@ +name: ci + +on: + push: + branches: + - trunk + + pull_request: + branches: + - trunk + +jobs: + build: + name: ${{ matrix.TOX_ENV }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: ["3.10"] + TOX_ENV: ["py310-extras", "py310-noextras", "mypy"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + - name: Tox Run + run: | + pip install tox; + TOX_ENV="${{ matrix.TOX_ENV }}"; + echo "Starting: ${TOX_ENV} ${PUSH_DOCS}" + if [[ -n "${TOX_ENV}" ]]; then + tox -e "$TOX_ENV"; + if [[ "${TOX_ENV}" != "mypy" ]]; then + tox -e coverage-report; + fi; + fi; + - name: Upload coverage report + if: ${{ matrix.TOX_ENV != 'mypy' }} + uses: codecov/codecov-action@v3.1.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/.travis.yml new/Automat-22.10.0/.travis.yml --- old/Automat-20.2.0/.travis.yml 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/.travis.yml 2022-10-29 08:56:09.000000000 +0200 @@ -6,15 +6,10 @@ 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: pypy3 + env: TOX_ENV=pypy3-extras + - python: pypy3 + env: TOX_ENV=pypy3-noextras - python: 3.5 env: TOX_ENV=py35-extras diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/Automat.egg-info/PKG-INFO new/Automat-22.10.0/Automat.egg-info/PKG-INFO --- old/Automat-20.2.0/Automat.egg-info/PKG-INFO 2020-02-16 20:36:16.000000000 +0100 +++ new/Automat-22.10.0/Automat.egg-info/PKG-INFO 2022-10-29 09:03:31.000000000 +0200 @@ -1,472 +1,12 @@ Metadata-Version: 2.1 Name: Automat -Version: 20.2.0 +Version: 22.10.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: gl...@twistedmatrix.com License: MIT -Description: - Automat - ======= - - - .. image:: https://readthedocs.org/projects/automat/badge/?version=latest - :target: http://automat.readthedocs.io/en/latest/ - :alt: Documentation Status - - - .. image:: https://travis-ci.org/glyph/automat.svg?branch=master - :target: https://travis-ci.org/glyph/automat - :alt: Build Status - - - .. image:: https://coveralls.io/repos/glyph/automat/badge.png - :target: https://coveralls.io/r/glyph/automat - :alt: Coverage Status - - - Self-service finite-state machines for the programmer on the go. - ---------------------------------------------------------------- - - Automat is a library for concise, idiomatic Python expression of finite-state - automata (particularly deterministic finite-state transducers). - - Read more here, or on `Read the Docs <https://automat.readthedocs.io/>`_\ , or watch the following videos for an overview and presentation - - Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: - - .. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg - :target: https://www.youtube.com/watch?v=0wOZBpD1VVk - :alt: Glyph Lefkowitz - Automat - Pyninsula #0 - - - Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: - - .. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg - :target: https://www.youtube.com/watch?v=TedUKXhu9kE - :alt: Clinton Roy - State Machines - Pycon Australia 2017 - - - Why use state machines? - ^^^^^^^^^^^^^^^^^^^^^^^ - - Sometimes you have to create an object whose behavior varies with its state, - but still wishes to present a consistent interface to its callers. - - For example, let's say you're writing the software for a coffee machine. It - has a lid that can be opened or closed, a chamber for water, a chamber for - coffee beans, and a button for "brew". - - There are a number of possible states for the coffee machine. It might or - might not have water. It might or might not have beans. The lid might be open - or closed. The "brew" button should only actually attempt to brew coffee in - one of these configurations, and the "open lid" button should only work if the - coffee is not, in fact, brewing. - - With diligence and attention to detail, you can implement this correctly using - a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , - ``is_lid_open`` and so on. However, you have to keep all these attributes - consistent. As the coffee maker becomes more complex - perhaps you add an - additional chamber for flavorings so you can make hazelnut coffee, for - example - you have to keep adding more and more checks and more and more - reasoning about which combinations of states are allowed. - - Rather than adding tedious 'if' checks to every single method to make sure that - each of these flags are exactly what you expect, you can use a state machine to - ensure that if your code runs at all, it will be run with all the required - values initialized, because they have to be called in the order you declare - them. - - You can read about state machines and their advantages for Python programmers - in considerably more detail - `in this excellent series of articles from ClusterHQ <https://clusterhq.com/blog/what-is-a-state-machine/>`_. - - What makes Automat different? - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - There are - `dozens of libraries on PyPI implementing state machines <https://pypi.org/search/?q=finite+state+machine>`_. - So it behooves me to say why yet another one would be a good idea. - - Automat is designed around this principle: while organizing your code around - state machines is a good idea, your callers don't, and shouldn't have to, care - that you've done so. In Python, the "input" to a stateful system is a method - call; the "output" may be a method call, if you need to invoke a side effect, - or a return value, if you are just performing a computation in memory. Most - other state-machine libraries require you to explicitly create an input object, - provide that object to a generic "input" method, and then receive results, - sometimes in terms of that library's interfaces and sometimes in terms of - classes you define yourself. - - For example, a snippet of the coffee-machine example above might be implemented - as follows in naive Python: - - .. code-block:: python - - class CoffeeMachine(object): - def brew_button(self): - if self.has_water and self.has_beans and not self.is_lid_open: - self.heat_the_heating_element() - # ... - - With Automat, you'd create a class with a ``MethodicalMachine`` attribute: - - .. code-block:: python - - from automat import MethodicalMachine - - class CoffeeBrewer(object): - _machine = MethodicalMachine() - - and then you would break the above logic into two pieces - the ``brew_button`` - *input*\ , declared like so: - - .. code-block:: python - - @_machine.input() - def brew_button(self): - "The user pressed the 'brew' button." - - It wouldn't do any good to declare a method *body* on this, however, because - input methods don't actually execute their bodies when called; doing actual - work is the *output*\ 's job: - - .. code-block:: python - - @_machine.output() - def _heat_the_heating_element(self): - "Heat up the heating element, which should cause coffee to happen." - self._heating_element.turn_on() - - As well as a couple of *states* - and for simplicity's sake let's say that the - only two states are ``have_beans`` and ``dont_have_beans``\ : - - .. code-block:: python - - @_machine.state() - def have_beans(self): - "In this state, you have some beans." - @_machine.state(initial=True) - def dont_have_beans(self): - "In this state, you don't have any beans." - - ``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans - in it. - - (And another input to put some beans in:) - - .. code-block:: python - - @_machine.input() - def put_in_beans(self): - "The user put in some beans." - - Finally, you hook everything together with the ``upon`` method of the functions - decorated with ``_machine.state``\ : - - .. code-block:: python - - - # When we don't have beans, upon putting in beans, we will then have beans - # (and produce no output) - dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) - - # When we have beans, upon pressing the brew button, we will then not have - # beans any more (as they have been entered into the brewing chamber) and - # our output will be heating the heating element. - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element]) - - To *users* of this coffee machine class though, it still looks like a POPO - (Plain Old Python Object): - - .. code-block:: python - - >>> coffee_machine = CoffeeMachine() - >>> coffee_machine.put_in_beans() - >>> coffee_machine.brew_button() - - All of the *inputs* are provided by calling them like methods, all of the - *outputs* are automatically invoked when they are produced according to the - outputs specified to ``upon`` and all of the states are simply opaque tokens - - although the fact that they're defined as methods like inputs and outputs - allows you to put docstrings on them easily to document them. - - How do I get the current state of a state machine? - -------------------------------------------------- - - Don't do that. - - One major reason for having a state machine is that you want the callers of the - state machine to just provide the appropriate input to the machine at the - appropriate time, and *not have to check themselves* what state the machine is - in. So if you are tempted to write some code like this: - - .. code-block:: python - - if connection_state_machine.state == "CONNECTED": - connection_state_machine.send_message() - else: - print("not connected") - - Instead, just make your calling code do this: - - .. code-block:: python - - connection_state_machine.send_message() - - and then change your state machine to look like this: - - .. code-block:: python - - @_machine.state() - def connected(self): - "connected" - @_machine.state() - def not_connected(self): - "not connected" - @_machine.input() - def send_message(self): - "send a message" - @_machine.output() - def _actually_send_message(self): - self._transport.send(b"message") - @_machine.output() - def _report_sending_failure(self): - print("not connected") - connected.upon(send_message, enter=connected, [_actually_send_message]) - not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) - - so that the responsibility for knowing which state the state machine is in - remains within the state machine itself. - - Input for Inputs and Output for Outputs - --------------------------------------- - - Quite often you want to be able to pass parameters to your methods, as well as - inspecting their results. For example, when you brew the coffee, you might - expect a cup of coffee to result, and you would like to see what kind of coffee - it is. And if you were to put delicious hand-roasted small-batch artisanal - beans into the machine, you would expect a *better* cup of coffee than if you - were to use mass-produced beans. You would do this in plain old Python by - adding a parameter, so that's how you do it in Automat as well. - - .. code-block:: python - - @_machine.input() - def put_in_beans(self, beans): - "The user put in some beans." - - However, one important difference here is that *we can't add any - implementation code to the input method*. Inputs are purely a declaration of - the interface; the behavior must all come from outputs. Therefore, the change - in the state of the coffee machine must be represented as an output. We can - add an output method like this: - - .. code-block:: python - - @_machine.output() - def _save_beans(self, beans): - "The beans are now in the machine; save them." - self._beans = beans - - and then connect it to the ``put_in_beans`` by changing the transition from - ``dont_have_beans`` to ``have_beans`` like so: - - .. code-block:: python - - dont_have_beans.upon(put_in_beans, enter=have_beans, - outputs=[_save_beans]) - - Now, when you call: - - .. code-block:: python - - coffee_machine.put_in_beans("real good beans") - - the machine will remember the beans for later. - - So how do we get the beans back out again? One of our outputs needs to have a - return value. It would make sense if our ``brew_button`` method returned the cup - of coffee that it made, so we should add an output. So, in addition to heating - the heating element, let's add a return value that describes the coffee. First - a new output: - - .. code-block:: python - - @_machine.output() - def _describe_coffee(self): - return "A cup of coffee made with {}.".format(self._beans) - - Note that we don't need to check first whether ``self._beans`` exists or not, - because we can only reach this output method if the state machine says we've - gone through a set of states that sets this attribute. - - Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change - the brewing transition to: - - .. code-block:: python - - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element, - _describe_coffee]) - - Now, we can call it: - - .. code-block:: python - - >>> coffee_machine.brew_button() - [None, 'A cup of coffee made with real good beans.'] - - Except... wait a second, what's that ``None`` doing there? - - Since every input can produce multiple outputs, in automat, the default return - value from every input invocation is a ``list``. In this case, we have both - ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing - both of their return values. However, this can be customized, with the - ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an - iterable of all the outputs' return values and "collects" a single return value - to return to the caller of the state machine. - - In this case, we only care about the last output, so we can adjust the call to - ``upon`` like this: - - .. code-block:: python - - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element, - _describe_coffee], - collector=lambda iterable: list(iterable)[-1] - ) - - And now, we'll get just the return value we want: - - .. code-block:: python - - >>> coffee_machine.brew_button() - 'A cup of coffee made with real good beans.' - - If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) - -------------------------------------------------------------------------------------------------------------------- - - There are APIs for serializing the state machine. - - First, you have to decide on a persistent representation of each state, via the - ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. - - Let's take this very simple "light switch" state machine, which can be on or - off, and flipped to reverse its state: - - .. code-block:: python - - class LightSwitch(object): - _machine = MethodicalMachine() - @_machine.state(serialized="on") - def on_state(self): - "the switch is on" - @_machine.state(serialized="off", initial=True) - def off_state(self): - "the switch is off" - @_machine.input() - def flip(self): - "flip the switch" - on_state.upon(flip, enter=off_state, outputs=[]) - off_state.upon(flip, enter=on_state, outputs=[]) - - In this case, we've chosen a serialized representation for each state via the - ``serialized`` argument. The on state is represented by the string ``"on"``\ , and - the off state is represented by the string ``"off"``. - - Now, let's just add an input that lets us tell if the switch is on or not. - - .. code-block:: python - - @_machine.input() - def query_power(self): - "return True if powered, False otherwise" - @_machine.output() - def _is_powered(self): - return True - @_machine.output() - def _not_powered(self): - return False - on_state.upon(query_power, enter=on_state, outputs=[_is_powered], - collector=next) - off_state.upon(query_power, enter=off_state, outputs=[_not_powered], - collector=next) - - To save the state, we have the ``MethodicalMachine.serializer()`` method. A - method decorated with ``@serializer()`` gets an extra argument injected at the - beginning of its argument list: the serialized identifier for the state. In - this case, either ``"on"`` or ``"off"``. Since state machine output methods can - also affect other state on the object, a serializer method is expected to - return *all* relevant state for serialization. - - For our simple light switch, such a method might look like this: - - .. code-block:: python - - @_machine.serializer() - def save(self, state): - return {"is-it-on": state} - - Serializers can be public methods, and they can return whatever you like. If - necessary, you can have different serializers - just multiple methods decorated - with ``@_machine.serializer()`` - for different formats; return one data-structure - for JSON, one for XML, one for a database row, and so on. - - When it comes time to unserialize, though, you generally want a private method, - because an unserializer has to take a not-fully-initialized instance and - populate it with state. It is expected to *return* the serialized machine - state token that was passed to the serializer, but it can take whatever - arguments you like. Of course, in order to return that, it probably has to - take it somewhere in its arguments, so it will generally take whatever a paired - serializer has returned as an argument. - - So our unserializer would look like this: - - .. code-block:: python - - @_machine.unserializer() - def _restore(self, blob): - return blob["is-it-on"] - - Generally you will want a classmethod deserialization constructor which you - write yourself to call this, so that you know how to create an instance of your - own object, like so: - - .. code-block:: python - - @classmethod - def from_blob(cls, blob): - self = cls() - self._restore(blob) - return self - - Saving and loading our ``LightSwitch`` along with its state-machine state can now - be accomplished as follows: - - .. code-block:: python - - >>> switch1 = LightSwitch() - >>> switch1.query_power() - False - >>> switch1.flip() - [] - >>> switch1.query_power() - True - >>> blob = switch1.save() - >>> switch2 = LightSwitch.from_blob(blob) - >>> switch2.query_power() - True - - More comprehensive (tested, working) examples are present in ``docs/examples``. - - Go forth and machine all the state! - Keywords: fsm finite state machine automata -Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent @@ -479,3 +19,4 @@ Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides-Extra: visualize +License-File: LICENSE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/Automat.egg-info/SOURCES.txt new/Automat-22.10.0/Automat.egg-info/SOURCES.txt --- old/Automat-20.2.0/Automat.egg-info/SOURCES.txt 2020-02-16 20:36:16.000000000 +0100 +++ new/Automat-22.10.0/Automat.egg-info/SOURCES.txt 2022-10-29 09:03:31.000000000 +0200 @@ -2,9 +2,11 @@ .travis.yml LICENSE README.md +mypy.ini setup.cfg setup.py tox.ini +.github/workflows/ci.yml Automat.egg-info/PKG-INFO Automat.egg-info/SOURCES.txt Automat.egg-info/dependency_links.txt @@ -30,7 +32,7 @@ docs/conf.py docs/debugging.rst docs/index.rst -docs/make.bat +docs/typing.rst docs/visualize.rst docs/_static/mystate.machine.MyMachine._machine.dot.png docs/examples/automat_example.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/Automat.egg-info/entry_points.txt new/Automat-22.10.0/Automat.egg-info/entry_points.txt --- old/Automat-20.2.0/Automat.egg-info/entry_points.txt 2020-02-16 20:36:16.000000000 +0100 +++ new/Automat-22.10.0/Automat.egg-info/entry_points.txt 2022-10-29 09:03:31.000000000 +0200 @@ -1,3 +1,2 @@ [console_scripts] automat-visualize = automat._visualize:tool - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/PKG-INFO new/Automat-22.10.0/PKG-INFO --- old/Automat-20.2.0/PKG-INFO 2020-02-16 20:36:16.000000000 +0100 +++ new/Automat-22.10.0/PKG-INFO 2022-10-29 09:03:31.956159000 +0200 @@ -1,472 +1,12 @@ Metadata-Version: 2.1 Name: Automat -Version: 20.2.0 +Version: 22.10.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: gl...@twistedmatrix.com License: MIT -Description: - Automat - ======= - - - .. image:: https://readthedocs.org/projects/automat/badge/?version=latest - :target: http://automat.readthedocs.io/en/latest/ - :alt: Documentation Status - - - .. image:: https://travis-ci.org/glyph/automat.svg?branch=master - :target: https://travis-ci.org/glyph/automat - :alt: Build Status - - - .. image:: https://coveralls.io/repos/glyph/automat/badge.png - :target: https://coveralls.io/r/glyph/automat - :alt: Coverage Status - - - Self-service finite-state machines for the programmer on the go. - ---------------------------------------------------------------- - - Automat is a library for concise, idiomatic Python expression of finite-state - automata (particularly deterministic finite-state transducers). - - Read more here, or on `Read the Docs <https://automat.readthedocs.io/>`_\ , or watch the following videos for an overview and presentation - - Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: - - .. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg - :target: https://www.youtube.com/watch?v=0wOZBpD1VVk - :alt: Glyph Lefkowitz - Automat - Pyninsula #0 - - - Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: - - .. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg - :target: https://www.youtube.com/watch?v=TedUKXhu9kE - :alt: Clinton Roy - State Machines - Pycon Australia 2017 - - - Why use state machines? - ^^^^^^^^^^^^^^^^^^^^^^^ - - Sometimes you have to create an object whose behavior varies with its state, - but still wishes to present a consistent interface to its callers. - - For example, let's say you're writing the software for a coffee machine. It - has a lid that can be opened or closed, a chamber for water, a chamber for - coffee beans, and a button for "brew". - - There are a number of possible states for the coffee machine. It might or - might not have water. It might or might not have beans. The lid might be open - or closed. The "brew" button should only actually attempt to brew coffee in - one of these configurations, and the "open lid" button should only work if the - coffee is not, in fact, brewing. - - With diligence and attention to detail, you can implement this correctly using - a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , - ``is_lid_open`` and so on. However, you have to keep all these attributes - consistent. As the coffee maker becomes more complex - perhaps you add an - additional chamber for flavorings so you can make hazelnut coffee, for - example - you have to keep adding more and more checks and more and more - reasoning about which combinations of states are allowed. - - Rather than adding tedious 'if' checks to every single method to make sure that - each of these flags are exactly what you expect, you can use a state machine to - ensure that if your code runs at all, it will be run with all the required - values initialized, because they have to be called in the order you declare - them. - - You can read about state machines and their advantages for Python programmers - in considerably more detail - `in this excellent series of articles from ClusterHQ <https://clusterhq.com/blog/what-is-a-state-machine/>`_. - - What makes Automat different? - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - There are - `dozens of libraries on PyPI implementing state machines <https://pypi.org/search/?q=finite+state+machine>`_. - So it behooves me to say why yet another one would be a good idea. - - Automat is designed around this principle: while organizing your code around - state machines is a good idea, your callers don't, and shouldn't have to, care - that you've done so. In Python, the "input" to a stateful system is a method - call; the "output" may be a method call, if you need to invoke a side effect, - or a return value, if you are just performing a computation in memory. Most - other state-machine libraries require you to explicitly create an input object, - provide that object to a generic "input" method, and then receive results, - sometimes in terms of that library's interfaces and sometimes in terms of - classes you define yourself. - - For example, a snippet of the coffee-machine example above might be implemented - as follows in naive Python: - - .. code-block:: python - - class CoffeeMachine(object): - def brew_button(self): - if self.has_water and self.has_beans and not self.is_lid_open: - self.heat_the_heating_element() - # ... - - With Automat, you'd create a class with a ``MethodicalMachine`` attribute: - - .. code-block:: python - - from automat import MethodicalMachine - - class CoffeeBrewer(object): - _machine = MethodicalMachine() - - and then you would break the above logic into two pieces - the ``brew_button`` - *input*\ , declared like so: - - .. code-block:: python - - @_machine.input() - def brew_button(self): - "The user pressed the 'brew' button." - - It wouldn't do any good to declare a method *body* on this, however, because - input methods don't actually execute their bodies when called; doing actual - work is the *output*\ 's job: - - .. code-block:: python - - @_machine.output() - def _heat_the_heating_element(self): - "Heat up the heating element, which should cause coffee to happen." - self._heating_element.turn_on() - - As well as a couple of *states* - and for simplicity's sake let's say that the - only two states are ``have_beans`` and ``dont_have_beans``\ : - - .. code-block:: python - - @_machine.state() - def have_beans(self): - "In this state, you have some beans." - @_machine.state(initial=True) - def dont_have_beans(self): - "In this state, you don't have any beans." - - ``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans - in it. - - (And another input to put some beans in:) - - .. code-block:: python - - @_machine.input() - def put_in_beans(self): - "The user put in some beans." - - Finally, you hook everything together with the ``upon`` method of the functions - decorated with ``_machine.state``\ : - - .. code-block:: python - - - # When we don't have beans, upon putting in beans, we will then have beans - # (and produce no output) - dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) - - # When we have beans, upon pressing the brew button, we will then not have - # beans any more (as they have been entered into the brewing chamber) and - # our output will be heating the heating element. - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element]) - - To *users* of this coffee machine class though, it still looks like a POPO - (Plain Old Python Object): - - .. code-block:: python - - >>> coffee_machine = CoffeeMachine() - >>> coffee_machine.put_in_beans() - >>> coffee_machine.brew_button() - - All of the *inputs* are provided by calling them like methods, all of the - *outputs* are automatically invoked when they are produced according to the - outputs specified to ``upon`` and all of the states are simply opaque tokens - - although the fact that they're defined as methods like inputs and outputs - allows you to put docstrings on them easily to document them. - - How do I get the current state of a state machine? - -------------------------------------------------- - - Don't do that. - - One major reason for having a state machine is that you want the callers of the - state machine to just provide the appropriate input to the machine at the - appropriate time, and *not have to check themselves* what state the machine is - in. So if you are tempted to write some code like this: - - .. code-block:: python - - if connection_state_machine.state == "CONNECTED": - connection_state_machine.send_message() - else: - print("not connected") - - Instead, just make your calling code do this: - - .. code-block:: python - - connection_state_machine.send_message() - - and then change your state machine to look like this: - - .. code-block:: python - - @_machine.state() - def connected(self): - "connected" - @_machine.state() - def not_connected(self): - "not connected" - @_machine.input() - def send_message(self): - "send a message" - @_machine.output() - def _actually_send_message(self): - self._transport.send(b"message") - @_machine.output() - def _report_sending_failure(self): - print("not connected") - connected.upon(send_message, enter=connected, [_actually_send_message]) - not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) - - so that the responsibility for knowing which state the state machine is in - remains within the state machine itself. - - Input for Inputs and Output for Outputs - --------------------------------------- - - Quite often you want to be able to pass parameters to your methods, as well as - inspecting their results. For example, when you brew the coffee, you might - expect a cup of coffee to result, and you would like to see what kind of coffee - it is. And if you were to put delicious hand-roasted small-batch artisanal - beans into the machine, you would expect a *better* cup of coffee than if you - were to use mass-produced beans. You would do this in plain old Python by - adding a parameter, so that's how you do it in Automat as well. - - .. code-block:: python - - @_machine.input() - def put_in_beans(self, beans): - "The user put in some beans." - - However, one important difference here is that *we can't add any - implementation code to the input method*. Inputs are purely a declaration of - the interface; the behavior must all come from outputs. Therefore, the change - in the state of the coffee machine must be represented as an output. We can - add an output method like this: - - .. code-block:: python - - @_machine.output() - def _save_beans(self, beans): - "The beans are now in the machine; save them." - self._beans = beans - - and then connect it to the ``put_in_beans`` by changing the transition from - ``dont_have_beans`` to ``have_beans`` like so: - - .. code-block:: python - - dont_have_beans.upon(put_in_beans, enter=have_beans, - outputs=[_save_beans]) - - Now, when you call: - - .. code-block:: python - - coffee_machine.put_in_beans("real good beans") - - the machine will remember the beans for later. - - So how do we get the beans back out again? One of our outputs needs to have a - return value. It would make sense if our ``brew_button`` method returned the cup - of coffee that it made, so we should add an output. So, in addition to heating - the heating element, let's add a return value that describes the coffee. First - a new output: - - .. code-block:: python - - @_machine.output() - def _describe_coffee(self): - return "A cup of coffee made with {}.".format(self._beans) - - Note that we don't need to check first whether ``self._beans`` exists or not, - because we can only reach this output method if the state machine says we've - gone through a set of states that sets this attribute. - - Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change - the brewing transition to: - - .. code-block:: python - - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element, - _describe_coffee]) - - Now, we can call it: - - .. code-block:: python - - >>> coffee_machine.brew_button() - [None, 'A cup of coffee made with real good beans.'] - - Except... wait a second, what's that ``None`` doing there? - - Since every input can produce multiple outputs, in automat, the default return - value from every input invocation is a ``list``. In this case, we have both - ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing - both of their return values. However, this can be customized, with the - ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an - iterable of all the outputs' return values and "collects" a single return value - to return to the caller of the state machine. - - In this case, we only care about the last output, so we can adjust the call to - ``upon`` like this: - - .. code-block:: python - - have_beans.upon(brew_button, enter=dont_have_beans, - outputs=[_heat_the_heating_element, - _describe_coffee], - collector=lambda iterable: list(iterable)[-1] - ) - - And now, we'll get just the return value we want: - - .. code-block:: python - - >>> coffee_machine.brew_button() - 'A cup of coffee made with real good beans.' - - If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) - -------------------------------------------------------------------------------------------------------------------- - - There are APIs for serializing the state machine. - - First, you have to decide on a persistent representation of each state, via the - ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. - - Let's take this very simple "light switch" state machine, which can be on or - off, and flipped to reverse its state: - - .. code-block:: python - - class LightSwitch(object): - _machine = MethodicalMachine() - @_machine.state(serialized="on") - def on_state(self): - "the switch is on" - @_machine.state(serialized="off", initial=True) - def off_state(self): - "the switch is off" - @_machine.input() - def flip(self): - "flip the switch" - on_state.upon(flip, enter=off_state, outputs=[]) - off_state.upon(flip, enter=on_state, outputs=[]) - - In this case, we've chosen a serialized representation for each state via the - ``serialized`` argument. The on state is represented by the string ``"on"``\ , and - the off state is represented by the string ``"off"``. - - Now, let's just add an input that lets us tell if the switch is on or not. - - .. code-block:: python - - @_machine.input() - def query_power(self): - "return True if powered, False otherwise" - @_machine.output() - def _is_powered(self): - return True - @_machine.output() - def _not_powered(self): - return False - on_state.upon(query_power, enter=on_state, outputs=[_is_powered], - collector=next) - off_state.upon(query_power, enter=off_state, outputs=[_not_powered], - collector=next) - - To save the state, we have the ``MethodicalMachine.serializer()`` method. A - method decorated with ``@serializer()`` gets an extra argument injected at the - beginning of its argument list: the serialized identifier for the state. In - this case, either ``"on"`` or ``"off"``. Since state machine output methods can - also affect other state on the object, a serializer method is expected to - return *all* relevant state for serialization. - - For our simple light switch, such a method might look like this: - - .. code-block:: python - - @_machine.serializer() - def save(self, state): - return {"is-it-on": state} - - Serializers can be public methods, and they can return whatever you like. If - necessary, you can have different serializers - just multiple methods decorated - with ``@_machine.serializer()`` - for different formats; return one data-structure - for JSON, one for XML, one for a database row, and so on. - - When it comes time to unserialize, though, you generally want a private method, - because an unserializer has to take a not-fully-initialized instance and - populate it with state. It is expected to *return* the serialized machine - state token that was passed to the serializer, but it can take whatever - arguments you like. Of course, in order to return that, it probably has to - take it somewhere in its arguments, so it will generally take whatever a paired - serializer has returned as an argument. - - So our unserializer would look like this: - - .. code-block:: python - - @_machine.unserializer() - def _restore(self, blob): - return blob["is-it-on"] - - Generally you will want a classmethod deserialization constructor which you - write yourself to call this, so that you know how to create an instance of your - own object, like so: - - .. code-block:: python - - @classmethod - def from_blob(cls, blob): - self = cls() - self._restore(blob) - return self - - Saving and loading our ``LightSwitch`` along with its state-machine state can now - be accomplished as follows: - - .. code-block:: python - - >>> switch1 = LightSwitch() - >>> switch1.query_power() - False - >>> switch1.flip() - [] - >>> switch1.query_power() - True - >>> blob = switch1.save() - >>> switch2 = LightSwitch.from_blob(blob) - >>> switch2.query_power() - True - - More comprehensive (tested, working) examples are present in ``docs/examples``. - - Go forth and machine all the state! - Keywords: fsm finite state machine automata -Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent @@ -479,3 +19,4 @@ Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides-Extra: visualize +License-File: LICENSE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/README.md new/Automat-22.10.0/README.md --- old/Automat-20.2.0/README.md 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/README.md 2022-10-29 08:56:12.000000000 +0200 @@ -11,10 +11,10 @@ Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation -Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: +Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: [](https://www.youtube.com/watch?v=0wOZBpD1VVk) -Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: +Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: [](https://www.youtube.com/watch?v=TedUKXhu9kE) ### Why use state machines? ### @@ -47,8 +47,8 @@ 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/). +in more detail [in this excellent article by Jean-Paul +Calderone](https://web.archive.org/web/20160507053658/https://clusterhq.com/2013/12/05/what-is-a-state-machine/). ### What makes Automat different? ### diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/automat/_introspection.py new/Automat-22.10.0/automat/_introspection.py --- old/Automat-20.2.0/automat/_introspection.py 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/automat/_introspection.py 2022-06-11 01:31:24.000000000 +0200 @@ -6,6 +6,8 @@ def copycode(template, changes): + if hasattr(code, "replace"): + return template.replace(**{"co_" + k : v for k, v in changes.items()}) names = [ "argcount", "nlocals", "stacksize", "flags", "code", "consts", "names", "varnames", "filename", "name", "firstlineno", "lnotab", @@ -23,7 +25,6 @@ return code(*values) - def copyfunction(template, funcchanges, codechanges): names = [ "globals", "name", "defaults", "closure", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/automat/_methodical.py new/Automat-22.10.0/automat/_methodical.py --- old/Automat-20.2.0/automat/_methodical.py 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/automat/_methodical.py 2022-06-12 06:18:47.000000000 +0200 @@ -4,15 +4,9 @@ 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 +from inspect import getfullargspec as getArgsSpec import attr -import six from ._core import Transitioner, Automaton from ._introspection import preserveName @@ -36,14 +30,14 @@ return ArgSpec( args=tuple(spec.args), varargs=spec.varargs, - varkw=spec.varkw if six.PY3 else spec.keywords, + varkw=spec.varkw, defaults=spec.defaults if spec.defaults else (), - kwonlyargs=tuple(spec.kwonlyargs) if six.PY3 else (), + kwonlyargs=tuple(spec.kwonlyargs), kwonlydefaults=( tuple(spec.kwonlydefaults.items()) if spec.kwonlydefaults else () - ) if six.PY3 else (), - annotations=tuple(spec.annotations.items()) if six.PY3 else (), + ), + annotations=tuple(spec.annotations.items()), ) @@ -91,7 +85,7 @@ method = attr.ib() serialized = attr.ib(repr=False) - def upon(self, input, enter, outputs, collector=list): + def upon(self, input, enter=None, outputs=None, collector=list): """ Declare a state transition within the :class:`automat.MethodicalMachine` associated with this :class:`automat.MethodicalState`: @@ -110,6 +104,10 @@ :raises ValueError: if the state transition from `self` via `input` has already been defined. """ + if enter is None: + enter = self + if outputs is None: + outputs = [] inputArgs = _getArgNames(input.argSpec) for output in outputs: outputArgs = _getArgNames(output.argSpec) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/automat/_test/test_discover.py new/Automat-22.10.0/automat/_test/test_discover.py --- old/Automat-20.2.0/automat/_test/test_discover.py 2018-01-11 10:16:33.000000000 +0100 +++ new/Automat-22.10.0/automat/_test/test_discover.py 2022-06-12 06:18:47.000000000 +0200 @@ -6,9 +6,6 @@ import tempfile from unittest import skipIf, TestCase -import six - - def isTwistedInstalled(): try: __import__('twisted') @@ -43,7 +40,7 @@ super(_WritesPythonModules, self).tearDown() sys.path[:] = self.savedSysPath - modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules + modulesToDelete = sys.modules.keys() - self.originalSysModules for module in modulesToDelete: del sys.modules[module] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/automat/_test/test_methodical.py new/Automat-22.10.0/automat/_test/test_methodical.py --- old/Automat-20.2.0/automat/_test/test_methodical.py 2018-06-04 07:39:34.000000000 +0200 +++ new/Automat-22.10.0/automat/_test/test_methodical.py 2022-02-17 08:13:25.000000000 +0100 @@ -365,6 +365,50 @@ self.assertIn("nameOfInput", str(cm.exception)) self.assertIn("outputThatDoesntMatch", str(cm.exception)) + def test_stateLoop(self): + """ + It is possible to write a self-loop by omitting "enter" + """ + class Mechanism(object): + m = MethodicalMachine() + @m.input() + def input(self): + "an input" + @m.input() + def say_hi(self): + "an input" + @m.output() + def _start_say_hi(self): + return "hi" + @m.state(initial=True) + def start(self): + "a state" + def said_hi(self): + "a state with no inputs" + start.upon(input, outputs=[]) + start.upon(say_hi, outputs=[_start_say_hi]) + a_mechanism = Mechanism() + [a_greeting] = a_mechanism.say_hi() + self.assertEqual(a_greeting, "hi") + + + def test_defaultOutputs(self): + """ + It is possible to write a transition with no outputs + """ + class Mechanism(object): + m = MethodicalMachine() + @m.input() + def finish(self): + "final transition" + @m.state(initial=True) + def start(self): + "a start state" + @m.state() + def finished(self): + "a final state" + start.upon(finish, enter=finished) + Mechanism().finish() def test_getArgNames(self): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/automat/_test/test_visualize.py new/Automat-22.10.0/automat/_test/test_visualize.py --- old/Automat-20.2.0/automat/_test/test_visualize.py 2018-01-11 10:16:33.000000000 +0100 +++ new/Automat-22.10.0/automat/_test/test_visualize.py 2021-03-09 08:19:39.000000000 +0100 @@ -62,6 +62,7 @@ @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") +@skipIf(not isTwistedInstalled(), "Twisted is not installed.") class ElementMakerTests(TestCase): """ L{elementMaker} generates HTML representing the specified element. @@ -134,6 +135,7 @@ @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") +@skipIf(not isTwistedInstalled(), "Twisted is not installed.") class TableMakerTests(TestCase): """ Tests that ensure L{tableMaker} generates HTML tables usable as @@ -214,6 +216,7 @@ @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.") +@skipIf(not isTwistedInstalled(), "Twisted is not installed.") class IntegrationTests(TestCase): """ Tests which make sure Graphviz can understand the output produced by @@ -232,6 +235,7 @@ @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") +@skipIf(not isTwistedInstalled(), "Twisted is not installed.") class SpotChecks(TestCase): """ Tests to make sure that the output contains salient features of the machine diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/docs/about.rst new/Automat-22.10.0/docs/about.rst --- old/Automat-20.2.0/docs/about.rst 2018-06-04 07:39:34.000000000 +0200 +++ new/Automat-22.10.0/docs/about.rst 2022-02-17 08:13:25.000000000 +0100 @@ -410,6 +410,10 @@ .. code-block:: python + from operator import itemgetter + + first = itemgetter(0) + class LightSwitch(object): _machine = MethodicalMachine() @@ -418,16 +422,21 @@ @_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) + + on_state.upon( + query_power, enter=on_state, outputs=[_is_powered], collector=first + ) + off_state.upon( + query_power, enter=off_state, outputs=[_not_powered], collector=first + ) To save the state, we have the `MethodicalMachine.serializer()` method. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/docs/index.rst new/Automat-22.10.0/docs/index.rst --- old/Automat-20.2.0/docs/index.rst 2018-06-04 07:39:34.000000000 +0200 +++ new/Automat-22.10.0/docs/index.rst 2022-02-17 08:13:25.000000000 +0100 @@ -55,3 +55,4 @@ visualize api debugging + typing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/docs/make.bat new/Automat-22.10.0/docs/make.bat --- old/Automat-20.2.0/docs/make.bat 2018-01-11 10:16:33.000000000 +0100 +++ new/Automat-22.10.0/docs/make.bat 1970-01-01 01:00:00.000000000 +0100 @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=automat - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/docs/typing.rst new/Automat-22.10.0/docs/typing.rst --- old/Automat-20.2.0/docs/typing.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/Automat-22.10.0/docs/typing.rst 2022-02-17 08:13:25.000000000 +0100 @@ -0,0 +1,95 @@ +Static Typing +-------------- + +When writing an output for a given state, +you can assume the finite state machine will be in that state. +This might mean that specific object attributes will have values +of speciifc types. +Those attributes might, +in general, +be of some :code:`Union` type: +frequently, +an :code:`Option` type +(which is a :code:`Union[T, None]`). + +It is an *anti-pattern* to check for these things inside the output. +The reason for a state machine is for the outputs to avoid checking. +However, +if the output is type annotated, +often :code:`mypy` +will complain that it cannot validate the types. +The recommended solution is to +:code:`assert` +the types inside the code. +This aligns +the assumptions +:code:`mypy` +makes +with the assumptions +:code:`automat` +makes. + +For example, +consider the following: + +.. code:: + + import attr + import automat + from typing import Optional + + @attr.s(auto_attribs=True) + class MaybeValue: + _machine = automat.MethodicalMachine() + _value: Optional[float] = attr.ib(default=None) + + @_machine.input() + def set_value(self, value: float) -> None: + "The value has been measured" + + @_machine.input() + def get_value(self) -> float: + "Return the value if it has been measured" + + @_machine.output() + def _set_value_when_unset(self, value: float) -> None: + self._value = value + + @_machine.output() + def _get_value_when_set(self) -> float: + """mypy will complain here: + + Incompatible return value type + (got "Optional[float]", expected "float") + """ + return self._value + + @_machine.state() + def value_is_set(self): + "The value is set" + + @_machine.state(initial=True) + def value_is_unset(self): + "The value is not set" + + value_is_unset.upon( + set_value, + enter=value_is_set, + outputs=[_set_value_when_unset], + collector=lambda x: None, + ) + value_is_set.upon( + get_value, + enter=value_is_set, + outputs=[_get_value_when_set], + collector=lambda x: next(iter(x)), + ) + +In this case +starting +:code:`_get_value_when_set` +with a line +:code:`assert self._value is not None` +will satisfy +:code:`mypy`. + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/mypy.ini new/Automat-22.10.0/mypy.ini --- old/Automat-20.2.0/mypy.ini 1970-01-01 01:00:00.000000000 +0100 +++ new/Automat-22.10.0/mypy.ini 2022-10-29 08:56:09.000000000 +0200 @@ -0,0 +1,3 @@ +[mypy] +[mypy-graphviz.*] +ignore_missing_imports = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/setup.py new/Automat-22.10.0/setup.py --- old/Automat-20.2.0/setup.py 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/setup.py 2022-06-12 06:18:47.000000000 +0200 @@ -4,14 +4,6 @@ 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, @@ -19,12 +11,12 @@ description=""" Self-service finite-state machines for the programmer on the go. """.strip(), - long_description=long_description, + readme='README.md', packages=find_packages(exclude=[]), package_dir={'automat': 'automat'}, setup_requires=[ + 'wheel', 'setuptools-scm', - 'm2r', ], install_requires=[ "attrs>=19.2.0", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Automat-20.2.0/tox.ini new/Automat-22.10.0/tox.ini --- old/Automat-20.2.0/tox.ini 2020-02-16 20:33:10.000000000 +0100 +++ new/Automat-22.10.0/tox.ini 2022-10-29 08:56:12.000000000 +0200 @@ -1,5 +1,5 @@ [tox] -envlist = coverage-clean,{py27,pypy,py35,py36,py38}-{extras,noextras},coverage-report +envlist = lint,mypy,coverage-clean,{pypy3,py38,py310}-{extras,noextras},coverage-report [testenv] deps = @@ -10,35 +10,55 @@ commands = coverage run --parallel --source automat -m py.test automat/_test +depends = + coverage-clean [testenv:coverage-clean] deps = coverage skip_install = true commands = coverage erase +depends = [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine + coverage xml coverage report -m +depends = + {pypy3,py38,py310}-{extras,noextras} [testenv:benchmark] deps = pytest-benchmark commands = pytest --benchmark-only benchmark/ -[testenv:py27-benchmark] +[testenv:py310-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} -[testenv:py35-benchmark] -deps = {[testenv:benchmark]deps} -commands = {[testenv:benchmark]commands} +[testenv:lint] +deps = black +commands = black --check automat -[testenv:py36-benchmark] -deps = {[testenv:benchmark]deps} -commands = {[testenv:benchmark]commands} +[testenv:mypy] +deps = + mypy + graphviz>=0.4.9 + Twisted>=16.2.0 -[testenv:pypy-benchmark] +commands = mypy automat + +[testenv:pypy3-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} + +[testenv:docs] +usedevelop = True +changedir = docs +deps = + sphinx + sphinx_rtd_theme +commands = + sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html +basepython = python3.8