Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Id78bcb20d483466d39e68f1cfa2eb8b4645ca53f
This commit is contained in:
parent
d34d15737f
commit
8843bea315
|
@ -1,8 +0,0 @@
|
||||||
[run]
|
|
||||||
branch = True
|
|
||||||
source = automaton
|
|
||||||
omit = automaton/tests/*
|
|
||||||
|
|
||||||
[report]
|
|
||||||
ignore_errors = True
|
|
||||||
precision = 2
|
|
|
@ -1,60 +0,0 @@
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.testrepository/
|
|
||||||
.tox/
|
|
||||||
.coverage
|
|
||||||
cover
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
doc/build/
|
|
||||||
|
|
||||||
# pbr generates these
|
|
||||||
AUTHORS
|
|
||||||
ChangeLog
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
|
@ -1,4 +0,0 @@
|
||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=openstack/automaton.git
|
|
|
@ -1,8 +0,0 @@
|
||||||
[DEFAULT]
|
|
||||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
|
|
||||||
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
|
|
||||||
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
|
|
||||||
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
|
|
||||||
test_id_option=--load-list $IDFILE
|
|
||||||
test_list_option=--list
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
If you would like to contribute to the development of OpenStack, you must
|
|
||||||
follow the steps in this page:
|
|
||||||
|
|
||||||
http://docs.openstack.org/infra/manual/developers.html
|
|
||||||
|
|
||||||
If you already have a good understanding of how the system works and your
|
|
||||||
OpenStack accounts are set up, you can skip to the development workflow
|
|
||||||
section of this documentation to learn how changes to OpenStack should be
|
|
||||||
submitted for review via the Gerrit tool:
|
|
||||||
|
|
||||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
|
||||||
|
|
||||||
The code is hosted at:
|
|
||||||
|
|
||||||
http://git.openstack.org/cgit/openstack/automaton.
|
|
||||||
|
|
||||||
Pull requests submitted through GitHub will be ignored.
|
|
||||||
|
|
||||||
Bugs should be filed on Launchpad, not GitHub:
|
|
||||||
|
|
||||||
https://bugs.launchpad.net/automaton
|
|
||||||
|
|
||||||
The mailing list is (prefix subjects with "[Oslo][Automaton]"):
|
|
||||||
|
|
||||||
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
|
|
||||||
|
|
||||||
Questions and discussions take place in #openstack-state-management on
|
|
||||||
irc.freenode.net.
|
|
|
@ -1,5 +0,0 @@
|
||||||
Automaton Style Commandments
|
|
||||||
===============================================
|
|
||||||
|
|
||||||
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
|
|
||||||
|
|
202
LICENSE
202
LICENSE
|
@ -1,202 +0,0 @@
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright {yyyy} {name of copyright owner}
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
This project is no longer maintained.
|
||||||
|
|
||||||
|
The contents of this repository are still available in the Git
|
||||||
|
source code management system. To see the contents of this
|
||||||
|
repository before it reached its end of life, please check out the
|
||||||
|
previous commit with "git checkout HEAD^1".
|
||||||
|
|
||||||
|
For ongoing work on maintaining OpenStack packages in the Debian
|
||||||
|
distribution, please see the Debian OpenStack packaging team at
|
||||||
|
https://wiki.debian.org/OpenStack/.
|
||||||
|
|
||||||
|
For any further questions, please email
|
||||||
|
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||||
|
Freenode.
|
23
README.rst
23
README.rst
|
@ -1,23 +0,0 @@
|
||||||
=========
|
|
||||||
Automaton
|
|
||||||
=========
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/automaton.svg
|
|
||||||
:target: https://pypi.python.org/pypi/automaton/
|
|
||||||
:alt: Latest Version
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/dm/automaton.svg
|
|
||||||
:target: https://pypi.python.org/pypi/automaton/
|
|
||||||
:alt: Downloads
|
|
||||||
|
|
||||||
Friendly state machines for python. The goal of this library is to provide
|
|
||||||
well documented state machine classes and associated utilities. The state
|
|
||||||
machine pattern (or the implemented variation there-of) is a commonly
|
|
||||||
used pattern and has a multitude of various usages. Some of the usages
|
|
||||||
for this library include providing state & transition validation and
|
|
||||||
running/scheduling/analyzing the execution of tasks.
|
|
||||||
|
|
||||||
* Free software: Apache license
|
|
||||||
* Documentation: https://docs.openstack.org/automaton/latest/
|
|
||||||
* Source: https://git.openstack.org/cgit/openstack/automaton
|
|
||||||
* Bugs: https://bugs.launchpad.net/automaton
|
|
|
@ -1,48 +0,0 @@
|
||||||
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
def get_callback_name(cb):
|
|
||||||
"""Tries to get a callbacks fully-qualified name.
|
|
||||||
|
|
||||||
If no name can be produced ``repr(cb)`` is called and returned.
|
|
||||||
"""
|
|
||||||
segments = []
|
|
||||||
try:
|
|
||||||
segments.append(cb.__qualname__)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
segments.append(cb.__name__)
|
|
||||||
if inspect.ismethod(cb):
|
|
||||||
try:
|
|
||||||
# This attribute doesn't exist on py3.x or newer, so
|
|
||||||
# we optionally ignore it... (on those versions of
|
|
||||||
# python `__qualname__` should have been found anyway).
|
|
||||||
segments.insert(0, cb.im_class.__name__)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
if not segments:
|
|
||||||
return repr(cb)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
# When running under sphinx it appears this can be none?
|
|
||||||
if cb.__module__:
|
|
||||||
segments.insert(0, cb.__module__)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
return ".".join(segments)
|
|
|
@ -1,111 +0,0 @@
|
||||||
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pydot
|
|
||||||
PYDOT_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PYDOT_AVAILABLE = False
|
|
||||||
|
|
||||||
|
|
||||||
def convert(machine, graph_name,
|
|
||||||
graph_attrs=None, node_attrs_cb=None, edge_attrs_cb=None,
|
|
||||||
add_start_state=True, name_translations=None):
|
|
||||||
"""Translates the state machine into a pydot graph.
|
|
||||||
|
|
||||||
:param machine: state machine to convert
|
|
||||||
:type machine: FiniteMachine
|
|
||||||
:param graph_name: name of the graph to be created
|
|
||||||
:type graph_name: string
|
|
||||||
:param graph_attrs: any initial graph attributes to set
|
|
||||||
(see http://www.graphviz.org/doc/info/attrs.html for
|
|
||||||
what these can be)
|
|
||||||
:type graph_attrs: dict
|
|
||||||
:param node_attrs_cb: a callback that takes one argument ``state``
|
|
||||||
and is expected to return a dict of node attributes
|
|
||||||
(see http://www.graphviz.org/doc/info/attrs.html for
|
|
||||||
what these can be)
|
|
||||||
:type node_attrs_cb: callback
|
|
||||||
:param edge_attrs_cb: a callback that takes three arguments ``start_state,
|
|
||||||
event, end_state`` and is expected to return a dict
|
|
||||||
of edge attributes (see
|
|
||||||
http://www.graphviz.org/doc/info/attrs.html for
|
|
||||||
what these can be)
|
|
||||||
:type edge_attrs_cb: callback
|
|
||||||
:param add_start_state: when enabled this creates a *private* start state
|
|
||||||
with the name ``__start__`` that will be a point
|
|
||||||
node that will have a dotted edge to the
|
|
||||||
``default_start_state`` that your machine may have
|
|
||||||
defined (if your machine has no actively defined
|
|
||||||
``default_start_state`` then this does nothing,
|
|
||||||
even if enabled)
|
|
||||||
:type add_start_state: bool
|
|
||||||
:param name_translations: a dict that provides alternative ``state``
|
|
||||||
string names for each state
|
|
||||||
:type name_translations: dict
|
|
||||||
"""
|
|
||||||
if not PYDOT_AVAILABLE:
|
|
||||||
raise RuntimeError("pydot (or pydot2 or equivalent) is required"
|
|
||||||
" to convert a state machine into a pydot"
|
|
||||||
" graph")
|
|
||||||
if not name_translations:
|
|
||||||
name_translations = {}
|
|
||||||
graph_kwargs = {
|
|
||||||
'rankdir': 'LR',
|
|
||||||
'nodesep': '0.25',
|
|
||||||
'overlap': 'false',
|
|
||||||
'ranksep': '0.5',
|
|
||||||
'size': "11x8.5",
|
|
||||||
'splines': 'true',
|
|
||||||
'ordering': 'in',
|
|
||||||
}
|
|
||||||
if graph_attrs is not None:
|
|
||||||
graph_kwargs.update(graph_attrs)
|
|
||||||
graph_kwargs['graph_name'] = graph_name
|
|
||||||
g = pydot.Dot(**graph_kwargs)
|
|
||||||
node_attrs = {
|
|
||||||
'fontsize': '11',
|
|
||||||
}
|
|
||||||
nodes = {}
|
|
||||||
for (start_state, event, end_state) in machine:
|
|
||||||
if start_state not in nodes:
|
|
||||||
start_node_attrs = node_attrs.copy()
|
|
||||||
if node_attrs_cb is not None:
|
|
||||||
start_node_attrs.update(node_attrs_cb(start_state))
|
|
||||||
pretty_start_state = name_translations.get(start_state,
|
|
||||||
start_state)
|
|
||||||
nodes[start_state] = pydot.Node(pretty_start_state,
|
|
||||||
**start_node_attrs)
|
|
||||||
g.add_node(nodes[start_state])
|
|
||||||
if end_state not in nodes:
|
|
||||||
end_node_attrs = node_attrs.copy()
|
|
||||||
if node_attrs_cb is not None:
|
|
||||||
end_node_attrs.update(node_attrs_cb(end_state))
|
|
||||||
pretty_end_state = name_translations.get(end_state, end_state)
|
|
||||||
nodes[end_state] = pydot.Node(pretty_end_state, **end_node_attrs)
|
|
||||||
g.add_node(nodes[end_state])
|
|
||||||
edge_attrs = {}
|
|
||||||
if edge_attrs_cb is not None:
|
|
||||||
edge_attrs.update(edge_attrs_cb(start_state, event, end_state))
|
|
||||||
g.add_edge(pydot.Edge(nodes[start_state], nodes[end_state],
|
|
||||||
**edge_attrs))
|
|
||||||
if add_start_state and machine.default_start_state:
|
|
||||||
start = pydot.Node("__start__", shape="point", width="0.1",
|
|
||||||
xlabel='start', fontcolor='green', **node_attrs)
|
|
||||||
g.add_node(start)
|
|
||||||
g.add_edge(pydot.Edge(start, nodes[machine.default_start_state],
|
|
||||||
style='dotted'))
|
|
||||||
return g
|
|
|
@ -1,40 +0,0 @@
|
||||||
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
class AutomatonException(Exception):
|
|
||||||
"""Base class for *most* exceptions emitted from this library."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidState(AutomatonException):
|
|
||||||
"""Raised when a invalid state transition is attempted while executing."""
|
|
||||||
|
|
||||||
|
|
||||||
class NotInitialized(AutomatonException):
|
|
||||||
"""Error raised when an action is attempted on a not inited machine."""
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(AutomatonException):
|
|
||||||
"""Raised when some entry in some object doesn't exist."""
|
|
||||||
|
|
||||||
|
|
||||||
class Duplicate(AutomatonException):
|
|
||||||
"""Raised when a duplicate entry is found."""
|
|
||||||
|
|
||||||
|
|
||||||
class FrozenMachine(AutomatonException):
|
|
||||||
"""Exception raised when a frozen machine is modified."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(FrozenMachine, self).__init__("Frozen machine can't be modified")
|
|
|
@ -1,543 +0,0 @@
|
||||||
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
|
|
||||||
from debtcollector import removals
|
|
||||||
import prettytable
|
|
||||||
import six
|
|
||||||
|
|
||||||
from automaton import _utils as utils
|
|
||||||
from automaton import exceptions as excp
|
|
||||||
|
|
||||||
|
|
||||||
class State(object):
|
|
||||||
"""Container that defines needed components of a single state.
|
|
||||||
|
|
||||||
Usage of this and the :meth:`~.FiniteMachine.build` make creating finite
|
|
||||||
state machines that much easier.
|
|
||||||
|
|
||||||
:ivar name: The name of the state.
|
|
||||||
:ivar is_terminal: Whether this state is terminal (or not).
|
|
||||||
:ivar next_states: Dictionary of 'event' -> 'next state name' (or none).
|
|
||||||
:ivar on_enter: callback that will be called when the state is entered.
|
|
||||||
:ivar on_exit: callback that will be called when the state is exited.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name,
|
|
||||||
is_terminal=False, next_states=None,
|
|
||||||
on_enter=None, on_exit=None):
|
|
||||||
self.name = name
|
|
||||||
self.is_terminal = bool(is_terminal)
|
|
||||||
self.next_states = next_states
|
|
||||||
self.on_enter = on_enter
|
|
||||||
self.on_exit = on_exit
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_states(state_space):
|
|
||||||
# NOTE(harlowja): if provided dicts, convert them...
|
|
||||||
for state in state_space:
|
|
||||||
if isinstance(state, dict):
|
|
||||||
state = State(**state)
|
|
||||||
yield state
|
|
||||||
|
|
||||||
|
|
||||||
def _orderedkeys(data, sort=True):
|
|
||||||
if sort:
|
|
||||||
return sorted(six.iterkeys(data))
|
|
||||||
else:
|
|
||||||
return list(six.iterkeys(data))
|
|
||||||
|
|
||||||
|
|
||||||
class _Jump(object):
|
|
||||||
"""A FSM transition tracks this data while jumping."""
|
|
||||||
def __init__(self, name, on_enter, on_exit):
|
|
||||||
self.name = name
|
|
||||||
self.on_enter = on_enter
|
|
||||||
self.on_exit = on_exit
|
|
||||||
|
|
||||||
|
|
||||||
class FiniteMachine(object):
|
|
||||||
"""A finite state machine.
|
|
||||||
|
|
||||||
This state machine can be used to automatically run a given set of
|
|
||||||
transitions and states in response to events (either from callbacks or from
|
|
||||||
generator/iterator send() values, see PEP 342). On each triggered event, a
|
|
||||||
``on_enter`` and ``on_exit`` callback can also be provided which will be
|
|
||||||
called to perform some type of action on leaving a prior state and before
|
|
||||||
entering a new state.
|
|
||||||
|
|
||||||
NOTE(harlowja): reactions will *only* be called when the generator/iterator
|
|
||||||
from :py:meth:`~automaton.runners.Runner.run_iter` does *not* send
|
|
||||||
back a new event (they will always be called if the
|
|
||||||
:py:meth:`~automaton.runners.Runner.run` method is used). This allows
|
|
||||||
for two unique ways (these ways can also be intermixed) to use this state
|
|
||||||
machine when using :py:meth:`~automaton.runners.Runner.run`; one
|
|
||||||
where *external* event trigger the next state transition and one
|
|
||||||
where *internal* reaction callbacks trigger the next state
|
|
||||||
transition. The other way to use this
|
|
||||||
state machine is to skip using :py:meth:`~automaton.runners.Runner.run`
|
|
||||||
or :py:meth:`~automaton.runners.Runner.run_iter`
|
|
||||||
completely and use the :meth:`~.FiniteMachine.process_event` method
|
|
||||||
explicitly and trigger the events via
|
|
||||||
some *external* functionality/triggers...
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: The result of processing an event (cause and effect...)
|
|
||||||
Effect = collections.namedtuple('Effect', 'reaction,terminal')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _effect_builder(cls, new_state, event):
|
|
||||||
return cls.Effect(new_state['reactions'].get(event),
|
|
||||||
new_state["terminal"])
|
|
||||||
|
|
||||||
@removals.removed_kwarg('default_start_state',
|
|
||||||
message="The usage of 'default_start_state' via"
|
|
||||||
" the machine constructor is deprecated and will"
|
|
||||||
" be removed in a future version; usage of"
|
|
||||||
" the 'default_start_state' property setter is"
|
|
||||||
" recommended.")
|
|
||||||
def __init__(self, default_start_state=None):
|
|
||||||
self._transitions = {}
|
|
||||||
self._states = collections.OrderedDict()
|
|
||||||
self._default_start_state = default_start_state
|
|
||||||
self._current = None
|
|
||||||
self.frozen = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def default_start_state(self):
|
|
||||||
"""Sets the *default* start state that the machine should use.
|
|
||||||
|
|
||||||
NOTE(harlowja): this will be used by ``initialize`` but only if that
|
|
||||||
function is not given its own ``start_state`` that overrides this
|
|
||||||
default.
|
|
||||||
"""
|
|
||||||
return self._default_start_state
|
|
||||||
|
|
||||||
@default_start_state.setter
|
|
||||||
def default_start_state(self, state):
|
|
||||||
if self.frozen:
|
|
||||||
raise excp.FrozenMachine()
|
|
||||||
if state not in self._states:
|
|
||||||
raise excp.NotFound("Can not set the default start state to"
|
|
||||||
" undefined state '%s'" % (state))
|
|
||||||
self._default_start_state = state
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def build(cls, state_space):
|
|
||||||
"""Builds a machine from a state space listing.
|
|
||||||
|
|
||||||
Each element of this list must be an instance
|
|
||||||
of :py:class:`.State` or a ``dict`` with equivalent keys that
|
|
||||||
can be used to construct a :py:class:`.State` instance.
|
|
||||||
"""
|
|
||||||
state_space = list(_convert_to_states(state_space))
|
|
||||||
m = cls()
|
|
||||||
for state in state_space:
|
|
||||||
m.add_state(state.name,
|
|
||||||
terminal=state.is_terminal,
|
|
||||||
on_enter=state.on_enter,
|
|
||||||
on_exit=state.on_exit)
|
|
||||||
for state in state_space:
|
|
||||||
if state.next_states:
|
|
||||||
for event, next_state in state.next_states.items():
|
|
||||||
if isinstance(next_state, State):
|
|
||||||
next_state = next_state.name
|
|
||||||
m.add_transition(state.name, next_state, event)
|
|
||||||
return m
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_state(self):
|
|
||||||
"""The current state the machine is in (or none if not initialized)."""
|
|
||||||
if self._current is not None:
|
|
||||||
return self._current.name
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def terminated(self):
|
|
||||||
"""Returns whether the state machine is in a terminal state."""
|
|
||||||
if self._current is None:
|
|
||||||
return False
|
|
||||||
return self._states[self._current.name]['terminal']
|
|
||||||
|
|
||||||
def add_state(self, state, terminal=False, on_enter=None, on_exit=None):
|
|
||||||
"""Adds a given state to the state machine.
|
|
||||||
|
|
||||||
The ``on_enter`` and ``on_exit`` callbacks, if provided will be
|
|
||||||
expected to take two positional parameters, these being the state
|
|
||||||
being exited (for ``on_exit``) or the state being entered (for
|
|
||||||
``on_enter``) and a second parameter which is the event that is
|
|
||||||
being processed that caused the state transition.
|
|
||||||
"""
|
|
||||||
if self.frozen:
|
|
||||||
raise excp.FrozenMachine()
|
|
||||||
if state in self._states:
|
|
||||||
raise excp.Duplicate("State '%s' already defined" % state)
|
|
||||||
if on_enter is not None:
|
|
||||||
if not six.callable(on_enter):
|
|
||||||
raise ValueError("On enter callback must be callable")
|
|
||||||
if on_exit is not None:
|
|
||||||
if not six.callable(on_exit):
|
|
||||||
raise ValueError("On exit callback must be callable")
|
|
||||||
self._states[state] = {
|
|
||||||
'terminal': bool(terminal),
|
|
||||||
'reactions': {},
|
|
||||||
'on_enter': on_enter,
|
|
||||||
'on_exit': on_exit,
|
|
||||||
}
|
|
||||||
self._transitions[state] = collections.OrderedDict()
|
|
||||||
|
|
||||||
def is_actionable_event(self, event):
|
|
||||||
"""Check whether the event is actionable in the current state."""
|
|
||||||
current = self._current
|
|
||||||
if current is None:
|
|
||||||
return False
|
|
||||||
if event not in self._transitions[current.name]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_reaction(self, state, event, reaction, *args, **kwargs):
|
|
||||||
"""Adds a reaction that may get triggered by the given event & state.
|
|
||||||
|
|
||||||
Reaction callbacks may (depending on how the state machine is ran) be
|
|
||||||
used after an event is processed (and a transition occurs) to cause the
|
|
||||||
machine to react to the newly arrived at stable state.
|
|
||||||
|
|
||||||
These callbacks are expected to accept three default positional
|
|
||||||
parameters (although more can be passed in via *args and **kwargs,
|
|
||||||
these will automatically get provided to the callback when it is
|
|
||||||
activated *ontop* of the three default). The three default parameters
|
|
||||||
are the last stable state, the new stable state and the event that
|
|
||||||
caused the transition to this new stable state to be arrived at.
|
|
||||||
|
|
||||||
The expected result of a callback is expected to be a new event that
|
|
||||||
the callback wants the state machine to react to. This new event
|
|
||||||
may (depending on how the state machine is ran) get processed (and
|
|
||||||
this process typically repeats) until the state machine reaches a
|
|
||||||
terminal state.
|
|
||||||
"""
|
|
||||||
if self.frozen:
|
|
||||||
raise excp.FrozenMachine()
|
|
||||||
if state not in self._states:
|
|
||||||
raise excp.NotFound("Can not add a reaction to event '%s' for an"
|
|
||||||
" undefined state '%s'" % (event, state))
|
|
||||||
if not six.callable(reaction):
|
|
||||||
raise ValueError("Reaction callback must be callable")
|
|
||||||
if event not in self._states[state]['reactions']:
|
|
||||||
self._states[state]['reactions'][event] = (reaction, args, kwargs)
|
|
||||||
else:
|
|
||||||
raise excp.Duplicate("State '%s' reaction to event '%s'"
|
|
||||||
" already defined" % (state, event))
|
|
||||||
|
|
||||||
def add_transition(self, start, end, event, replace=False):
|
|
||||||
"""Adds an allowed transition from start -> end for the given event.
|
|
||||||
|
|
||||||
:param start: starting state
|
|
||||||
:param end: ending state
|
|
||||||
:param event: event that causes start state to
|
|
||||||
transition to end state
|
|
||||||
:param replace: replace existing event instead of raising a
|
|
||||||
:py:class:`~automaton.exceptions.Duplicate` exception
|
|
||||||
when the transition already exists.
|
|
||||||
"""
|
|
||||||
if self.frozen:
|
|
||||||
raise excp.FrozenMachine()
|
|
||||||
if start not in self._states:
|
|
||||||
raise excp.NotFound("Can not add a transition on event '%s' that"
|
|
||||||
" starts in a undefined state '%s'"
|
|
||||||
% (event, start))
|
|
||||||
if end not in self._states:
|
|
||||||
raise excp.NotFound("Can not add a transition on event '%s' that"
|
|
||||||
" ends in a undefined state '%s'"
|
|
||||||
% (event, end))
|
|
||||||
if self._states[start]['terminal']:
|
|
||||||
raise excp.InvalidState("Can not add a transition on event '%s'"
|
|
||||||
" that starts in the terminal state '%s'"
|
|
||||||
% (event, start))
|
|
||||||
if event in self._transitions[start] and not replace:
|
|
||||||
target = self._transitions[start][event]
|
|
||||||
if target.name != end:
|
|
||||||
raise excp.Duplicate("Cannot add transition from"
|
|
||||||
" '%(start_state)s' to '%(end_state)s'"
|
|
||||||
" on event '%(event)s' because a"
|
|
||||||
" transition from '%(start_state)s'"
|
|
||||||
" to '%(existing_end_state)s' on"
|
|
||||||
" event '%(event)s' already exists."
|
|
||||||
% {'existing_end_state': target.name,
|
|
||||||
'end_state': end, 'event': event,
|
|
||||||
'start_state': start})
|
|
||||||
else:
|
|
||||||
target = _Jump(end, self._states[end]['on_enter'],
|
|
||||||
self._states[start]['on_exit'])
|
|
||||||
self._transitions[start][event] = target
|
|
||||||
|
|
||||||
def _pre_process_event(self, event):
|
|
||||||
current = self._current
|
|
||||||
if current is None:
|
|
||||||
raise excp.NotInitialized("Can not process event '%s'; the state"
|
|
||||||
" machine hasn't been initialized"
|
|
||||||
% event)
|
|
||||||
if self._states[current.name]['terminal']:
|
|
||||||
raise excp.InvalidState("Can not transition from terminal"
|
|
||||||
" state '%s' on event '%s'"
|
|
||||||
% (current.name, event))
|
|
||||||
if event not in self._transitions[current.name]:
|
|
||||||
raise excp.NotFound("Can not transition from state '%s' on"
|
|
||||||
" event '%s' (no defined transition)"
|
|
||||||
% (current.name, event))
|
|
||||||
|
|
||||||
def _post_process_event(self, event, result):
|
|
||||||
return result
|
|
||||||
|
|
||||||
def process_event(self, event):
|
|
||||||
"""Trigger a state change in response to the provided event.
|
|
||||||
|
|
||||||
:returns: Effect this is either a :py:class:`.FiniteMachine.Effect` or
|
|
||||||
an ``Effect`` from a subclass of :py:class:`.FiniteMachine`.
|
|
||||||
See the appropriate named tuple for a description of the
|
|
||||||
actual items in the tuple. For
|
|
||||||
example, :py:class:`.FiniteMachine.Effect`'s
|
|
||||||
first item is ``reaction``: one could invoke this reaction's
|
|
||||||
callback to react to the new stable state.
|
|
||||||
:rtype: namedtuple
|
|
||||||
"""
|
|
||||||
self._pre_process_event(event)
|
|
||||||
current = self._current
|
|
||||||
replacement = self._transitions[current.name][event]
|
|
||||||
if current.on_exit is not None:
|
|
||||||
current.on_exit(current.name, event)
|
|
||||||
if replacement.on_enter is not None:
|
|
||||||
replacement.on_enter(replacement.name, event)
|
|
||||||
self._current = replacement
|
|
||||||
result = self._effect_builder(self._states[replacement.name], event)
|
|
||||||
return self._post_process_event(event, result)
|
|
||||||
|
|
||||||
def initialize(self, start_state=None):
|
|
||||||
"""Sets up the state machine (sets current state to start state...).
|
|
||||||
|
|
||||||
:param start_state: explicit start state to use to initialize the
|
|
||||||
state machine to. If ``None`` is provided then
|
|
||||||
the machine's default start state will be used
|
|
||||||
instead.
|
|
||||||
"""
|
|
||||||
if start_state is None:
|
|
||||||
start_state = self._default_start_state
|
|
||||||
if start_state not in self._states:
|
|
||||||
raise excp.NotFound("Can not start from a undefined"
|
|
||||||
" state '%s'" % (start_state))
|
|
||||||
if self._states[start_state]['terminal']:
|
|
||||||
raise excp.InvalidState("Can not start from a terminal"
|
|
||||||
" state '%s'" % (start_state))
|
|
||||||
# No on enter will be called, since we are priming the state machine
|
|
||||||
# and have not really transitioned from anything to get here, we will
|
|
||||||
# though allow on_exit to be called on the event that causes this
|
|
||||||
# to be moved from...
|
|
||||||
self._current = _Jump(start_state, None,
|
|
||||||
self._states[start_state]['on_exit'])
|
|
||||||
|
|
||||||
def copy(self, shallow=False, unfreeze=False):
|
|
||||||
"""Copies the current state machine.
|
|
||||||
|
|
||||||
NOTE(harlowja): the copy will be left in an *uninitialized* state.
|
|
||||||
|
|
||||||
NOTE(harlowja): when a shallow copy is requested the copy will share
|
|
||||||
the same transition table and state table as the
|
|
||||||
source; this can be advantageous if you have a machine
|
|
||||||
and transitions + states that is defined somewhere
|
|
||||||
and want to use copies to run with (the copies have
|
|
||||||
the current state that is different between machines).
|
|
||||||
"""
|
|
||||||
c = type(self)()
|
|
||||||
c._default_start_state = self._default_start_state
|
|
||||||
if unfreeze and self.frozen:
|
|
||||||
c.frozen = False
|
|
||||||
else:
|
|
||||||
c.frozen = self.frozen
|
|
||||||
if not shallow:
|
|
||||||
for state, data in self._states.items():
|
|
||||||
copied_data = data.copy()
|
|
||||||
copied_data['reactions'] = copied_data['reactions'].copy()
|
|
||||||
c._states[state] = copied_data
|
|
||||||
for state, data in self._transitions.items():
|
|
||||||
c._transitions[state] = data.copy()
|
|
||||||
else:
|
|
||||||
c._transitions = self._transitions
|
|
||||||
c._states = self._states
|
|
||||||
return c
|
|
||||||
|
|
||||||
def __contains__(self, state):
|
|
||||||
"""Returns if this state exists in the machines known states."""
|
|
||||||
return state in self._states
|
|
||||||
|
|
||||||
def freeze(self):
|
|
||||||
"""Freezes & stops addition of states, transitions, reactions..."""
|
|
||||||
self.frozen = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def states(self):
|
|
||||||
"""Returns the state names."""
|
|
||||||
return list(six.iterkeys(self._states))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def events(self):
|
|
||||||
"""Returns how many events exist."""
|
|
||||||
c = 0
|
|
||||||
for state in six.iterkeys(self._states):
|
|
||||||
c += len(self._transitions[state])
|
|
||||||
return c
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Iterates over (start, event, end) transition tuples."""
|
|
||||||
for state in six.iterkeys(self._states):
|
|
||||||
for event, target in self._transitions[state].items():
|
|
||||||
yield (state, event, target.name)
|
|
||||||
|
|
||||||
def pformat(self, sort=True, empty='.'):
|
|
||||||
"""Pretty formats the state + transition table into a string.
|
|
||||||
|
|
||||||
NOTE(harlowja): the sort parameter can be provided to sort the states
|
|
||||||
and transitions by sort order; with it being provided as false the rows
|
|
||||||
will be iterated in addition order instead.
|
|
||||||
"""
|
|
||||||
tbl = prettytable.PrettyTable(["Start", "Event", "End",
|
|
||||||
"On Enter", "On Exit"])
|
|
||||||
for state in _orderedkeys(self._states, sort=sort):
|
|
||||||
prefix_markings = []
|
|
||||||
if self.current_state == state:
|
|
||||||
prefix_markings.append("@")
|
|
||||||
postfix_markings = []
|
|
||||||
if self.default_start_state == state:
|
|
||||||
postfix_markings.append("^")
|
|
||||||
if self._states[state]['terminal']:
|
|
||||||
postfix_markings.append("$")
|
|
||||||
pretty_state = "%s%s" % ("".join(prefix_markings), state)
|
|
||||||
if postfix_markings:
|
|
||||||
pretty_state += "[%s]" % "".join(postfix_markings)
|
|
||||||
if self._transitions[state]:
|
|
||||||
for event in _orderedkeys(self._transitions[state],
|
|
||||||
sort=sort):
|
|
||||||
target = self._transitions[state][event]
|
|
||||||
row = [pretty_state, event, target.name]
|
|
||||||
if target.on_enter is not None:
|
|
||||||
row.append(utils.get_callback_name(target.on_enter))
|
|
||||||
else:
|
|
||||||
row.append(empty)
|
|
||||||
if target.on_exit is not None:
|
|
||||||
row.append(utils.get_callback_name(target.on_exit))
|
|
||||||
else:
|
|
||||||
row.append(empty)
|
|
||||||
tbl.add_row(row)
|
|
||||||
else:
|
|
||||||
on_enter = self._states[state]['on_enter']
|
|
||||||
if on_enter is not None:
|
|
||||||
on_enter = utils.get_callback_name(on_enter)
|
|
||||||
else:
|
|
||||||
on_enter = empty
|
|
||||||
on_exit = self._states[state]['on_exit']
|
|
||||||
if on_exit is not None:
|
|
||||||
on_exit = utils.get_callback_name(on_exit)
|
|
||||||
else:
|
|
||||||
on_exit = empty
|
|
||||||
tbl.add_row([pretty_state, empty, empty, on_enter, on_exit])
|
|
||||||
return tbl.get_string()
|
|
||||||
|
|
||||||
|
|
||||||
class HierarchicalFiniteMachine(FiniteMachine):
|
|
||||||
"""A fsm that understands how to run in a hierarchical mode."""
|
|
||||||
|
|
||||||
#: The result of processing an event (cause and effect...)
|
|
||||||
Effect = collections.namedtuple('Effect',
|
|
||||||
'reaction,terminal,machine')
|
|
||||||
|
|
||||||
def __init__(self, default_start_state=None):
|
|
||||||
super(HierarchicalFiniteMachine, self).__init__(
|
|
||||||
default_start_state=default_start_state)
|
|
||||||
self._nested_machines = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _effect_builder(cls, new_state, event):
|
|
||||||
return cls.Effect(new_state['reactions'].get(event),
|
|
||||||
new_state["terminal"], new_state.get('machine'))
|
|
||||||
|
|
||||||
def add_state(self, state,
|
|
||||||
terminal=False, on_enter=None, on_exit=None, machine=None):
|
|
||||||
"""Adds a given state to the state machine.
|
|
||||||
|
|
||||||
:param machine: the nested state machine that will be transitioned
|
|
||||||
into when this state is entered
|
|
||||||
:type machine: :py:class:`.FiniteMachine`
|
|
||||||
|
|
||||||
Further arguments are interpreted as
|
|
||||||
for :py:meth:`.FiniteMachine.add_state`.
|
|
||||||
"""
|
|
||||||
if machine is not None and not isinstance(machine, FiniteMachine):
|
|
||||||
raise ValueError(
|
|
||||||
"Nested state machines must themselves be state machines")
|
|
||||||
super(HierarchicalFiniteMachine, self).add_state(
|
|
||||||
state, terminal=terminal, on_enter=on_enter, on_exit=on_exit)
|
|
||||||
if machine is not None:
|
|
||||||
self._states[state]['machine'] = machine
|
|
||||||
self._nested_machines[state] = machine
|
|
||||||
|
|
||||||
def copy(self, shallow=False, unfreeze=False):
|
|
||||||
c = super(HierarchicalFiniteMachine, self).copy(shallow=shallow,
|
|
||||||
unfreeze=unfreeze)
|
|
||||||
if shallow:
|
|
||||||
c._nested_machines = self._nested_machines
|
|
||||||
else:
|
|
||||||
c._nested_machines = self._nested_machines.copy()
|
|
||||||
return c
|
|
||||||
|
|
||||||
def initialize(self, start_state=None,
|
|
||||||
nested_start_state_fetcher=None):
|
|
||||||
"""Sets up the state machine (sets current state to start state...).
|
|
||||||
|
|
||||||
:param start_state: explicit start state to use to initialize the
|
|
||||||
state machine to. If ``None`` is provided then the
|
|
||||||
machine's default start state will be used
|
|
||||||
instead.
|
|
||||||
:param nested_start_state_fetcher: A callback that can return start
|
|
||||||
states for any nested machines
|
|
||||||
**only**. If not ``None`` then it
|
|
||||||
will be provided a single argument,
|
|
||||||
the machine to provide a starting
|
|
||||||
state for and it is expected to
|
|
||||||
return a starting state (or
|
|
||||||
``None``) for each machine called
|
|
||||||
with. Do note that this callback
|
|
||||||
will also be passed to other nested
|
|
||||||
state machines as well, so it will
|
|
||||||
also be used to initialize any state
|
|
||||||
machines they contain (recursively).
|
|
||||||
"""
|
|
||||||
super(HierarchicalFiniteMachine, self).initialize(
|
|
||||||
start_state=start_state)
|
|
||||||
for data in six.itervalues(self._states):
|
|
||||||
if 'machine' in data:
|
|
||||||
nested_machine = data['machine']
|
|
||||||
nested_start_state = None
|
|
||||||
if nested_start_state_fetcher is not None:
|
|
||||||
nested_start_state = nested_start_state_fetcher(
|
|
||||||
nested_machine)
|
|
||||||
if isinstance(nested_machine, HierarchicalFiniteMachine):
|
|
||||||
nested_machine.initialize(
|
|
||||||
start_state=nested_start_state,
|
|
||||||
nested_start_state_fetcher=nested_start_state_fetcher)
|
|
||||||
else:
|
|
||||||
nested_machine.initialize(start_state=nested_start_state)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nested_machines(self):
|
|
||||||
"""Dictionary of **all** nested state machines this machine may use."""
|
|
||||||
return self._nested_machines
|
|
|
@ -1,188 +0,0 @@
|
||||||
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from automaton import exceptions as excp
|
|
||||||
from automaton import machines
|
|
||||||
|
|
||||||
|
|
||||||
_JUMPER_NOT_FOUND_TPL = ("Unable to progress since no reaction (or"
|
|
||||||
" sent event) has been made available in"
|
|
||||||
" new state '%s' (moved to from state '%s'"
|
|
||||||
" in response to event '%s')")
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class Runner(object):
|
|
||||||
"""Machine runner used to run a state machine.
|
|
||||||
|
|
||||||
Only **one** runner per machine should be active at the same time (aka
|
|
||||||
there should not be multiple runners using the same machine instance at
|
|
||||||
the same time).
|
|
||||||
"""
|
|
||||||
def __init__(self, machine):
|
|
||||||
self._machine = machine
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def run(self, event, initialize=True):
|
|
||||||
"""Runs the state machine, using reactions only."""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def run_iter(self, event, initialize=True):
|
|
||||||
"""Returns a iterator/generator that will run the state machine.
|
|
||||||
|
|
||||||
NOTE(harlowja): only one runner iterator/generator should be active for
|
|
||||||
a machine, if this is not observed then it is possible for
|
|
||||||
initialization and other local state to be corrupted and cause issues
|
|
||||||
when running...
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FiniteRunner(Runner):
|
|
||||||
"""Finite machine runner used to run a finite machine.
|
|
||||||
|
|
||||||
Only **one** runner per machine should be active at the same time (aka
|
|
||||||
there should not be multiple runners using the same machine instance at
|
|
||||||
the same time).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, machine):
|
|
||||||
"""Create a runner for the given machine."""
|
|
||||||
if not isinstance(machine, (machines.FiniteMachine,)):
|
|
||||||
raise TypeError("FiniteRunner only works with FiniteMachine(s)")
|
|
||||||
super(FiniteRunner, self).__init__(machine)
|
|
||||||
|
|
||||||
def run(self, event, initialize=True):
|
|
||||||
for transition in self.run_iter(event, initialize=initialize):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run_iter(self, event, initialize=True):
|
|
||||||
if initialize:
|
|
||||||
self._machine.initialize()
|
|
||||||
while True:
|
|
||||||
old_state = self._machine.current_state
|
|
||||||
reaction, terminal = self._machine.process_event(event)
|
|
||||||
new_state = self._machine.current_state
|
|
||||||
try:
|
|
||||||
sent_event = yield (old_state, new_state)
|
|
||||||
except GeneratorExit:
|
|
||||||
break
|
|
||||||
if terminal:
|
|
||||||
break
|
|
||||||
if reaction is None and sent_event is None:
|
|
||||||
raise excp.NotFound(_JUMPER_NOT_FOUND_TPL % (new_state,
|
|
||||||
old_state,
|
|
||||||
event))
|
|
||||||
elif sent_event is not None:
|
|
||||||
event = sent_event
|
|
||||||
else:
|
|
||||||
cb, args, kwargs = reaction
|
|
||||||
event = cb(old_state, new_state, event, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class HierarchicalRunner(Runner):
|
|
||||||
"""Hierarchical machine runner used to run a hierarchical machine.
|
|
||||||
|
|
||||||
Only **one** runner per machine should be active at the same time (aka
|
|
||||||
there should not be multiple runners using the same machine instance at
|
|
||||||
the same time).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, machine):
|
|
||||||
"""Create a runner for the given machine."""
|
|
||||||
if not isinstance(machine, (machines.HierarchicalFiniteMachine,)):
|
|
||||||
raise TypeError("HierarchicalRunner only works with"
|
|
||||||
" HierarchicalFiniteMachine(s)")
|
|
||||||
super(HierarchicalRunner, self).__init__(machine)
|
|
||||||
|
|
||||||
def run(self, event, initialize=True):
|
|
||||||
for transition in self.run_iter(event, initialize=initialize):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _process_event(machines, event):
|
|
||||||
"""Matches a event to the machine hierarchy.
|
|
||||||
|
|
||||||
If the lowest level machine does not handle the event, then the
|
|
||||||
parent machine is referred to and so on, until there is only one
|
|
||||||
machine left which *must* handle the event.
|
|
||||||
|
|
||||||
The machine whose ``process_event`` does not throw invalid state or
|
|
||||||
not found exceptions is expected to be the machine that should
|
|
||||||
continue handling events...
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
machine = machines[-1]
|
|
||||||
try:
|
|
||||||
result = machine.process_event(event)
|
|
||||||
except (excp.InvalidState, excp.NotFound):
|
|
||||||
if len(machines) == 1:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
current = machine._current
|
|
||||||
if current is not None and current.on_exit is not None:
|
|
||||||
current.on_exit(current.name, event)
|
|
||||||
machine._current = None
|
|
||||||
machines.pop()
|
|
||||||
else:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run_iter(self, event, initialize=True):
|
|
||||||
"""Returns a iterator/generator that will run the state machine.
|
|
||||||
|
|
||||||
This will keep a stack (hierarchy) of machines active and jumps through
|
|
||||||
them as needed (depending on which machine handles which event) during
|
|
||||||
the running lifecycle.
|
|
||||||
|
|
||||||
NOTE(harlowja): only one runner iterator/generator should be active for
|
|
||||||
a machine hierarchy, if this is not observed then it is possible for
|
|
||||||
initialization and other local state to be corrupted and causes issues
|
|
||||||
when running...
|
|
||||||
"""
|
|
||||||
machines = [self._machine]
|
|
||||||
if initialize:
|
|
||||||
machines[-1].initialize()
|
|
||||||
while True:
|
|
||||||
old_state = machines[-1].current_state
|
|
||||||
effect = self._process_event(machines, event)
|
|
||||||
new_state = machines[-1].current_state
|
|
||||||
try:
|
|
||||||
machine = effect.machine
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if machine is not None and machine is not machines[-1]:
|
|
||||||
machine.initialize()
|
|
||||||
machines.append(machine)
|
|
||||||
try:
|
|
||||||
sent_event = yield (old_state, new_state)
|
|
||||||
except GeneratorExit:
|
|
||||||
break
|
|
||||||
if len(machines) == 1 and effect.terminal:
|
|
||||||
# Only allow the top level machine to actually terminate the
|
|
||||||
# execution, the rest of the nested machines must not handle
|
|
||||||
# events if they wish to have the root machine terminate...
|
|
||||||
break
|
|
||||||
if effect.reaction is None and sent_event is None:
|
|
||||||
raise excp.NotFound(_JUMPER_NOT_FOUND_TPL % (new_state,
|
|
||||||
old_state,
|
|
||||||
event))
|
|
||||||
elif sent_event is not None:
|
|
||||||
event = sent_event
|
|
||||||
else:
|
|
||||||
cb, args, kwargs = effect.reaction
|
|
||||||
event = cb(old_state, new_state, event, *args, **kwargs)
|
|
|
@ -1,465 +0,0 @@
|
||||||
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import functools
|
|
||||||
import random
|
|
||||||
|
|
||||||
from automaton import exceptions as excp
|
|
||||||
from automaton import machines
|
|
||||||
from automaton import runners
|
|
||||||
|
|
||||||
import six
|
|
||||||
from testtools import testcase
|
|
||||||
|
|
||||||
|
|
||||||
class FSMTest(testcase.TestCase):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_fsm(start_state, add_start=True, add_states=None):
|
|
||||||
m = machines.FiniteMachine()
|
|
||||||
if add_start:
|
|
||||||
m.add_state(start_state)
|
|
||||||
m.default_start_state = start_state
|
|
||||||
if add_states:
|
|
||||||
for s in add_states:
|
|
||||||
if s in m:
|
|
||||||
continue
|
|
||||||
m.add_state(s)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(FSMTest, self).setUp()
|
|
||||||
# NOTE(harlowja): this state machine will never stop if run() is used.
|
|
||||||
self.jumper = self._create_fsm("down", add_states=['up', 'down'])
|
|
||||||
self.jumper.add_transition('down', 'up', 'jump')
|
|
||||||
self.jumper.add_transition('up', 'down', 'fall')
|
|
||||||
self.jumper.add_reaction('up', 'jump', lambda *args: 'fall')
|
|
||||||
self.jumper.add_reaction('down', 'fall', lambda *args: 'jump')
|
|
||||||
|
|
||||||
def test_build(self):
|
|
||||||
space = []
|
|
||||||
for a in 'abc':
|
|
||||||
space.append(machines.State(a))
|
|
||||||
m = machines.FiniteMachine.build(space)
|
|
||||||
for a in 'abc':
|
|
||||||
self.assertIn(a, m)
|
|
||||||
|
|
||||||
def test_build_transitions(self):
|
|
||||||
space = [
|
|
||||||
machines.State('down', is_terminal=False,
|
|
||||||
next_states={'jump': 'up'}),
|
|
||||||
machines.State('up', is_terminal=False,
|
|
||||||
next_states={'fall': 'down'}),
|
|
||||||
]
|
|
||||||
m = machines.FiniteMachine.build(space)
|
|
||||||
m.default_start_state = 'down'
|
|
||||||
expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')]
|
|
||||||
self.assertEqual(expected, list(m))
|
|
||||||
|
|
||||||
def test_build_transitions_with_callbacks(self):
|
|
||||||
entered = collections.defaultdict(list)
|
|
||||||
exitted = collections.defaultdict(list)
|
|
||||||
|
|
||||||
def on_enter(state, event):
|
|
||||||
entered[state].append(event)
|
|
||||||
|
|
||||||
def on_exit(state, event):
|
|
||||||
exitted[state].append(event)
|
|
||||||
|
|
||||||
space = [
|
|
||||||
machines.State('down', is_terminal=False,
|
|
||||||
next_states={'jump': 'up'},
|
|
||||||
on_enter=on_enter, on_exit=on_exit),
|
|
||||||
machines.State('up', is_terminal=False,
|
|
||||||
next_states={'fall': 'down'},
|
|
||||||
on_enter=on_enter, on_exit=on_exit),
|
|
||||||
]
|
|
||||||
m = machines.FiniteMachine.build(space)
|
|
||||||
m.default_start_state = 'down'
|
|
||||||
expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')]
|
|
||||||
self.assertEqual(expected, list(m))
|
|
||||||
|
|
||||||
m.initialize()
|
|
||||||
m.process_event('jump')
|
|
||||||
|
|
||||||
self.assertEqual({'down': ['jump']}, dict(exitted))
|
|
||||||
self.assertEqual({'up': ['jump']}, dict(entered))
|
|
||||||
|
|
||||||
m.process_event('fall')
|
|
||||||
|
|
||||||
self.assertEqual({'down': ['jump'], 'up': ['fall']}, dict(exitted))
|
|
||||||
self.assertEqual({'up': ['jump'], 'down': ['fall']}, dict(entered))
|
|
||||||
|
|
||||||
def test_build_transitions_dct(self):
|
|
||||||
space = [
|
|
||||||
{
|
|
||||||
'name': 'down', 'is_terminal': False,
|
|
||||||
'next_states': {'jump': 'up'},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'up', 'is_terminal': False,
|
|
||||||
'next_states': {'fall': 'down'},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
m = machines.FiniteMachine.build(space)
|
|
||||||
m.default_start_state = 'down'
|
|
||||||
expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')]
|
|
||||||
self.assertEqual(expected, list(m))
|
|
||||||
|
|
||||||
def test_build_terminal(self):
|
|
||||||
space = [
|
|
||||||
machines.State('down', is_terminal=False,
|
|
||||||
next_states={'jump': 'fell_over'}),
|
|
||||||
machines.State('fell_over', is_terminal=True),
|
|
||||||
]
|
|
||||||
m = machines.FiniteMachine.build(space)
|
|
||||||
m.default_start_state = 'down'
|
|
||||||
m.initialize()
|
|
||||||
m.process_event('jump')
|
|
||||||
self.assertTrue(m.terminated)
|
|
||||||
|
|
||||||
def test_actionable(self):
|
|
||||||
self.jumper.initialize()
|
|
||||||
self.assertTrue(self.jumper.is_actionable_event('jump'))
|
|
||||||
self.assertFalse(self.jumper.is_actionable_event('fall'))
|
|
||||||
|
|
||||||
def test_bad_start_state(self):
|
|
||||||
m = self._create_fsm('unknown', add_start=False)
|
|
||||||
r = runners.FiniteRunner(m)
|
|
||||||
self.assertRaises(excp.NotFound, r.run, 'unknown')
|
|
||||||
|
|
||||||
def test_contains(self):
|
|
||||||
m = self._create_fsm('unknown', add_start=False)
|
|
||||||
self.assertNotIn('unknown', m)
|
|
||||||
m.add_state('unknown')
|
|
||||||
self.assertIn('unknown', m)
|
|
||||||
|
|
||||||
def test_no_add_transition_terminal(self):
|
|
||||||
m = self._create_fsm('up')
|
|
||||||
m.add_state('down', terminal=True)
|
|
||||||
self.assertRaises(excp.InvalidState,
|
|
||||||
m.add_transition, 'down', 'up', 'jump')
|
|
||||||
|
|
||||||
def test_duplicate_state(self):
|
|
||||||
m = self._create_fsm('unknown')
|
|
||||||
self.assertRaises(excp.Duplicate, m.add_state, 'unknown')
|
|
||||||
|
|
||||||
def test_duplicate_transition(self):
|
|
||||||
m = self.jumper
|
|
||||||
m.add_state('side_ways')
|
|
||||||
self.assertRaises(excp.Duplicate,
|
|
||||||
m.add_transition, 'up', 'side_ways', 'fall')
|
|
||||||
|
|
||||||
def test_duplicate_transition_replace(self):
|
|
||||||
m = self.jumper
|
|
||||||
m.add_state('side_ways')
|
|
||||||
m.add_transition('up', 'side_ways', 'fall', replace=True)
|
|
||||||
|
|
||||||
def test_duplicate_transition_same_transition(self):
|
|
||||||
m = self.jumper
|
|
||||||
m.add_transition('up', 'down', 'fall')
|
|
||||||
|
|
||||||
def test_duplicate_reaction(self):
|
|
||||||
self.assertRaises(
|
|
||||||
# Currently duplicate reactions are not allowed...
|
|
||||||
excp.Duplicate,
|
|
||||||
self.jumper.add_reaction, 'down', 'fall', lambda *args: 'skate')
|
|
||||||
|
|
||||||
def test_bad_transition(self):
|
|
||||||
m = self._create_fsm('unknown')
|
|
||||||
m.add_state('fire')
|
|
||||||
self.assertRaises(excp.NotFound, m.add_transition,
|
|
||||||
'unknown', 'something', 'boom')
|
|
||||||
self.assertRaises(excp.NotFound, m.add_transition,
|
|
||||||
'something', 'unknown', 'boom')
|
|
||||||
|
|
||||||
def test_bad_reaction(self):
|
|
||||||
m = self._create_fsm('unknown')
|
|
||||||
self.assertRaises(excp.NotFound, m.add_reaction, 'something', 'boom',
|
|
||||||
lambda *args: 'cough')
|
|
||||||
|
|
||||||
def test_run(self):
|
|
||||||
m = self._create_fsm('down', add_states=['up', 'down'])
|
|
||||||
m.add_state('broken', terminal=True)
|
|
||||||
m.add_transition('down', 'up', 'jump')
|
|
||||||
m.add_transition('up', 'broken', 'hit-wall')
|
|
||||||
m.add_reaction('up', 'jump', lambda *args: 'hit-wall')
|
|
||||||
self.assertEqual(['broken', 'down', 'up'], sorted(m.states))
|
|
||||||
self.assertEqual(2, m.events)
|
|
||||||
m.initialize()
|
|
||||||
self.assertEqual('down', m.current_state)
|
|
||||||
self.assertFalse(m.terminated)
|
|
||||||
r = runners.FiniteRunner(m)
|
|
||||||
r.run('jump')
|
|
||||||
self.assertTrue(m.terminated)
|
|
||||||
self.assertEqual('broken', m.current_state)
|
|
||||||
self.assertRaises(excp.InvalidState, r.run,
|
|
||||||
'jump', initialize=False)
|
|
||||||
|
|
||||||
def test_on_enter_on_exit(self):
|
|
||||||
enter_transitions = []
|
|
||||||
exit_transitions = []
|
|
||||||
|
|
||||||
def on_exit(state, event):
|
|
||||||
exit_transitions.append((state, event))
|
|
||||||
|
|
||||||
def on_enter(state, event):
|
|
||||||
enter_transitions.append((state, event))
|
|
||||||
|
|
||||||
m = self._create_fsm('start', add_start=False)
|
|
||||||
m.add_state('start', on_exit=on_exit)
|
|
||||||
m.add_state('down', on_enter=on_enter, on_exit=on_exit)
|
|
||||||
m.add_state('up', on_enter=on_enter, on_exit=on_exit)
|
|
||||||
m.add_transition('start', 'down', 'beat')
|
|
||||||
m.add_transition('down', 'up', 'jump')
|
|
||||||
m.add_transition('up', 'down', 'fall')
|
|
||||||
|
|
||||||
m.initialize('start')
|
|
||||||
m.process_event('beat')
|
|
||||||
m.process_event('jump')
|
|
||||||
m.process_event('fall')
|
|
||||||
self.assertEqual([('down', 'beat'),
|
|
||||||
('up', 'jump'), ('down', 'fall')], enter_transitions)
|
|
||||||
self.assertEqual([('start', 'beat'), ('down', 'jump'), ('up', 'fall')],
|
|
||||||
exit_transitions)
|
|
||||||
|
|
||||||
def test_run_iter(self):
|
|
||||||
up_downs = []
|
|
||||||
runner = runners.FiniteRunner(self.jumper)
|
|
||||||
for (old_state, new_state) in runner.run_iter('jump'):
|
|
||||||
up_downs.append((old_state, new_state))
|
|
||||||
if len(up_downs) >= 3:
|
|
||||||
break
|
|
||||||
self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')],
|
|
||||||
up_downs)
|
|
||||||
self.assertFalse(self.jumper.terminated)
|
|
||||||
self.assertEqual('up', self.jumper.current_state)
|
|
||||||
self.jumper.process_event('fall')
|
|
||||||
self.assertEqual('down', self.jumper.current_state)
|
|
||||||
|
|
||||||
def test_run_send(self):
|
|
||||||
up_downs = []
|
|
||||||
runner = runners.FiniteRunner(self.jumper)
|
|
||||||
it = runner.run_iter('jump')
|
|
||||||
while True:
|
|
||||||
up_downs.append(it.send(None))
|
|
||||||
if len(up_downs) >= 3:
|
|
||||||
it.close()
|
|
||||||
break
|
|
||||||
self.assertEqual('up', self.jumper.current_state)
|
|
||||||
self.assertFalse(self.jumper.terminated)
|
|
||||||
self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')],
|
|
||||||
up_downs)
|
|
||||||
self.assertRaises(StopIteration, six.next, it)
|
|
||||||
|
|
||||||
def test_run_send_fail(self):
|
|
||||||
up_downs = []
|
|
||||||
runner = runners.FiniteRunner(self.jumper)
|
|
||||||
it = runner.run_iter('jump')
|
|
||||||
up_downs.append(six.next(it))
|
|
||||||
self.assertRaises(excp.NotFound, it.send, 'fail')
|
|
||||||
it.close()
|
|
||||||
self.assertEqual([('down', 'up')], up_downs)
|
|
||||||
|
|
||||||
def test_not_initialized(self):
|
|
||||||
self.assertRaises(excp.NotInitialized,
|
|
||||||
self.jumper.process_event, 'jump')
|
|
||||||
|
|
||||||
def test_copy_states(self):
|
|
||||||
c = self._create_fsm('down', add_start=False)
|
|
||||||
self.assertEqual(0, len(c.states))
|
|
||||||
d = c.copy()
|
|
||||||
c.add_state('up')
|
|
||||||
c.add_state('down')
|
|
||||||
self.assertEqual(2, len(c.states))
|
|
||||||
self.assertEqual(0, len(d.states))
|
|
||||||
|
|
||||||
def test_copy_reactions(self):
|
|
||||||
c = self._create_fsm('down', add_start=False)
|
|
||||||
d = c.copy()
|
|
||||||
|
|
||||||
c.add_state('down')
|
|
||||||
c.add_state('up')
|
|
||||||
c.add_reaction('down', 'jump', lambda *args: 'up')
|
|
||||||
c.add_transition('down', 'up', 'jump')
|
|
||||||
|
|
||||||
self.assertEqual(1, c.events)
|
|
||||||
self.assertEqual(0, d.events)
|
|
||||||
self.assertNotIn('down', d)
|
|
||||||
self.assertNotIn('up', d)
|
|
||||||
self.assertEqual([], list(d))
|
|
||||||
self.assertEqual([('down', 'jump', 'up')], list(c))
|
|
||||||
|
|
||||||
def test_copy_initialized(self):
|
|
||||||
j = self.jumper.copy()
|
|
||||||
self.assertIsNone(j.current_state)
|
|
||||||
r = runners.FiniteRunner(self.jumper)
|
|
||||||
|
|
||||||
for i, transition in enumerate(r.run_iter('jump')):
|
|
||||||
if i == 4:
|
|
||||||
break
|
|
||||||
|
|
||||||
self.assertIsNone(j.current_state)
|
|
||||||
self.assertIsNotNone(self.jumper.current_state)
|
|
||||||
|
|
||||||
def test_iter(self):
|
|
||||||
transitions = list(self.jumper)
|
|
||||||
self.assertEqual(2, len(transitions))
|
|
||||||
self.assertIn(('up', 'fall', 'down'), transitions)
|
|
||||||
self.assertIn(('down', 'jump', 'up'), transitions)
|
|
||||||
|
|
||||||
def test_freeze(self):
|
|
||||||
self.jumper.freeze()
|
|
||||||
self.assertRaises(excp.FrozenMachine, self.jumper.add_state, 'test')
|
|
||||||
self.assertRaises(excp.FrozenMachine,
|
|
||||||
self.jumper.add_transition, 'test', 'test', 'test')
|
|
||||||
self.assertRaises(excp.FrozenMachine,
|
|
||||||
self.jumper.add_reaction,
|
|
||||||
'test', 'test', lambda *args: 'test')
|
|
||||||
|
|
||||||
def test_freeze_copy_unfreeze(self):
|
|
||||||
self.jumper.freeze()
|
|
||||||
self.assertTrue(self.jumper.frozen)
|
|
||||||
cp = self.jumper.copy(unfreeze=True)
|
|
||||||
self.assertTrue(self.jumper.frozen)
|
|
||||||
self.assertFalse(cp.frozen)
|
|
||||||
|
|
||||||
def test_invalid_callbacks(self):
|
|
||||||
m = self._create_fsm('working', add_states=['working', 'broken'])
|
|
||||||
self.assertRaises(ValueError, m.add_state, 'b', on_enter=2)
|
|
||||||
self.assertRaises(ValueError, m.add_state, 'b', on_exit=2)
|
|
||||||
|
|
||||||
|
|
||||||
class HFSMTest(FSMTest):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_fsm(start_state,
|
|
||||||
add_start=True, hierarchical=False, add_states=None):
|
|
||||||
if hierarchical:
|
|
||||||
m = machines.HierarchicalFiniteMachine()
|
|
||||||
else:
|
|
||||||
m = machines.FiniteMachine()
|
|
||||||
if add_start:
|
|
||||||
m.add_state(start_state)
|
|
||||||
m.default_start_state = start_state
|
|
||||||
if add_states:
|
|
||||||
for s in add_states:
|
|
||||||
if s not in m:
|
|
||||||
m.add_state(s)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def _make_phone_call(self, talk_time=1.0):
|
|
||||||
|
|
||||||
def phone_reaction(old_state, new_state, event, chat_iter):
|
|
||||||
try:
|
|
||||||
six.next(chat_iter)
|
|
||||||
except StopIteration:
|
|
||||||
return 'finish'
|
|
||||||
else:
|
|
||||||
# Talk until the iterator expires...
|
|
||||||
return 'chat'
|
|
||||||
|
|
||||||
talker = self._create_fsm("talk")
|
|
||||||
talker.add_transition("talk", "talk", "pickup")
|
|
||||||
talker.add_transition("talk", "talk", "chat")
|
|
||||||
talker.add_reaction("talk", "pickup", lambda *args: 'chat')
|
|
||||||
chat_iter = iter(list(range(0, 10)))
|
|
||||||
talker.add_reaction("talk", "chat", phone_reaction, chat_iter)
|
|
||||||
|
|
||||||
handler = self._create_fsm('begin', hierarchical=True)
|
|
||||||
handler.add_state("phone", machine=talker)
|
|
||||||
handler.add_state('hangup', terminal=True)
|
|
||||||
handler.add_transition("begin", "phone", "call")
|
|
||||||
handler.add_reaction("phone", 'call', lambda *args: 'pickup')
|
|
||||||
handler.add_transition("phone", "hangup", "finish")
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
def _make_phone_dialer(self):
|
|
||||||
dialer = self._create_fsm("idle", hierarchical=True)
|
|
||||||
digits = self._create_fsm("idle")
|
|
||||||
|
|
||||||
dialer.add_state("pickup", machine=digits)
|
|
||||||
dialer.add_transition("idle", "pickup", "dial")
|
|
||||||
dialer.add_reaction("pickup", "dial", lambda *args: 'press')
|
|
||||||
dialer.add_state("hangup", terminal=True)
|
|
||||||
|
|
||||||
def react_to_press(last_state, new_state, event, number_calling):
|
|
||||||
if len(number_calling) >= 10:
|
|
||||||
return 'call'
|
|
||||||
else:
|
|
||||||
return 'press'
|
|
||||||
|
|
||||||
digit_maker = functools.partial(random.randint, 0, 9)
|
|
||||||
number_calling = []
|
|
||||||
digits.add_state(
|
|
||||||
"accumulate",
|
|
||||||
on_enter=lambda *args: number_calling.append(digit_maker()))
|
|
||||||
digits.add_transition("idle", "accumulate", "press")
|
|
||||||
digits.add_transition("accumulate", "accumulate", "press")
|
|
||||||
digits.add_reaction("accumulate", "press",
|
|
||||||
react_to_press, number_calling)
|
|
||||||
digits.add_state("dial", terminal=True)
|
|
||||||
digits.add_transition("accumulate", "dial", "call")
|
|
||||||
digits.add_reaction("dial", "call", lambda *args: 'ringing')
|
|
||||||
dialer.add_state("talk")
|
|
||||||
dialer.add_transition("pickup", "talk", "ringing")
|
|
||||||
dialer.add_reaction("talk", "ringing", lambda *args: 'hangup')
|
|
||||||
dialer.add_transition("talk", "hangup", 'hangup')
|
|
||||||
return dialer, number_calling
|
|
||||||
|
|
||||||
def test_nested_machines(self):
|
|
||||||
dialer, _number_calling = self._make_phone_dialer()
|
|
||||||
self.assertEqual(1, len(dialer.nested_machines))
|
|
||||||
|
|
||||||
def test_nested_machine_initializers(self):
|
|
||||||
dialer, _number_calling = self._make_phone_dialer()
|
|
||||||
queried_for = []
|
|
||||||
|
|
||||||
def init_with(nested_machine):
|
|
||||||
queried_for.append(nested_machine)
|
|
||||||
return None
|
|
||||||
|
|
||||||
dialer.initialize(nested_start_state_fetcher=init_with)
|
|
||||||
self.assertEqual(1, len(queried_for))
|
|
||||||
|
|
||||||
def test_phone_dialer_iter(self):
|
|
||||||
dialer, number_calling = self._make_phone_dialer()
|
|
||||||
self.assertEqual(0, len(number_calling))
|
|
||||||
r = runners.HierarchicalRunner(dialer)
|
|
||||||
transitions = list(r.run_iter('dial'))
|
|
||||||
self.assertEqual(('talk', 'hangup'), transitions[-1])
|
|
||||||
self.assertEqual(len(number_calling),
|
|
||||||
sum(1 if new_state == 'accumulate' else 0
|
|
||||||
for (old_state, new_state) in transitions))
|
|
||||||
self.assertEqual(10, len(number_calling))
|
|
||||||
|
|
||||||
def test_phone_call(self):
|
|
||||||
handler = self._make_phone_call()
|
|
||||||
r = runners.HierarchicalRunner(handler)
|
|
||||||
r.run('call')
|
|
||||||
self.assertTrue(handler.terminated)
|
|
||||||
|
|
||||||
def test_phone_call_iter(self):
|
|
||||||
handler = self._make_phone_call()
|
|
||||||
r = runners.HierarchicalRunner(handler)
|
|
||||||
transitions = list(r.run_iter('call'))
|
|
||||||
self.assertEqual(('talk', 'hangup'), transitions[-1])
|
|
||||||
self.assertEqual(("begin", 'phone'), transitions[0])
|
|
||||||
talk_talk = 0
|
|
||||||
for transition in transitions:
|
|
||||||
if transition == ("talk", "talk"):
|
|
||||||
talk_talk += 1
|
|
||||||
self.assertGreater(talk_talk, 0)
|
|
|
@ -1,84 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
# -- General configuration ----------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'sphinx.ext.doctest',
|
|
||||||
'sphinx.ext.inheritance_diagram',
|
|
||||||
'sphinx.ext.viewcode',
|
|
||||||
'openstackdocstheme',
|
|
||||||
]
|
|
||||||
|
|
||||||
# openstackdocstheme options
|
|
||||||
repository_name = 'openstack/automaton'
|
|
||||||
bug_project = 'automaton'
|
|
||||||
bug_tag = ''
|
|
||||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
|
||||||
|
|
||||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
|
||||||
# text edit cycles.
|
|
||||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'automaton'
|
|
||||||
copyright = u'2013, OpenStack Foundation'
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
add_module_names = True
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# -- Options for HTML output --------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
# html_theme_path = ["."]
|
|
||||||
# html_theme = '_theme'
|
|
||||||
# html_static_path = ['static']
|
|
||||||
html_theme = 'openstackdocs'
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = '%sdoc' % project
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass
|
|
||||||
# [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index',
|
|
||||||
'%s.tex' % project,
|
|
||||||
u'%s Documentation' % project,
|
|
||||||
u'OpenStack Foundation', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
|
||||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
|
|
@ -1,5 +0,0 @@
|
||||||
============
|
|
||||||
Contributing
|
|
||||||
============
|
|
||||||
|
|
||||||
.. include:: ../../../CONTRIBUTING.rst
|
|
|
@ -1,20 +0,0 @@
|
||||||
=======================================
|
|
||||||
Welcome to automaton's documentation!
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Friendly state machines for python.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
user/index
|
|
||||||
reference/index
|
|
||||||
install/index
|
|
||||||
contributor/index
|
|
||||||
|
|
||||||
.. rubric:: Indices and tables
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
============
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
At the command line::
|
|
||||||
|
|
||||||
$ pip install automaton
|
|
||||||
|
|
||||||
Or, if you have virtualenvwrapper installed::
|
|
||||||
|
|
||||||
$ mkvirtualenv automaton
|
|
||||||
$ pip install automaton
|
|
|
@ -1,51 +0,0 @@
|
||||||
===
|
|
||||||
API
|
|
||||||
===
|
|
||||||
|
|
||||||
--------
|
|
||||||
Machines
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. autoclass:: automaton.machines.State
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: automaton.machines.FiniteMachine
|
|
||||||
:members:
|
|
||||||
:special-members: __iter__, __contains__
|
|
||||||
|
|
||||||
.. autoclass:: automaton.machines.HierarchicalFiniteMachine
|
|
||||||
:members:
|
|
||||||
|
|
||||||
-------
|
|
||||||
Runners
|
|
||||||
-------
|
|
||||||
|
|
||||||
.. autoclass:: automaton.runners.Runner
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: automaton.runners.FiniteRunner
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: automaton.runners.HierarchicalRunner
|
|
||||||
:members:
|
|
||||||
|
|
||||||
----------
|
|
||||||
Converters
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. automodule:: automaton.converters.pydot
|
|
||||||
:members:
|
|
||||||
|
|
||||||
----------
|
|
||||||
Exceptions
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. automodule:: automaton.exceptions
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Hierarchy
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. inheritance-diagram::
|
|
||||||
automaton.exceptions
|
|
||||||
:parts: 1
|
|
|
@ -1,462 +0,0 @@
|
||||||
========
|
|
||||||
Examples
|
|
||||||
========
|
|
||||||
|
|
||||||
-------------------------
|
|
||||||
Creating a simple machine
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. testcode::
|
|
||||||
|
|
||||||
from automaton import machines
|
|
||||||
m = machines.FiniteMachine()
|
|
||||||
m.add_state('up')
|
|
||||||
m.add_state('down')
|
|
||||||
m.add_transition('down', 'up', 'jump')
|
|
||||||
m.add_transition('up', 'down', 'fall')
|
|
||||||
m.default_start_state = 'down'
|
|
||||||
print(m.pformat())
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
.. testoutput::
|
|
||||||
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
| down[^] | jump | up | . | . |
|
|
||||||
| up | fall | down | . | . |
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
Transitioning a simple machine
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
.. testcode::
|
|
||||||
|
|
||||||
m.initialize()
|
|
||||||
m.process_event('jump')
|
|
||||||
print(m.pformat())
|
|
||||||
print(m.current_state)
|
|
||||||
print(m.terminated)
|
|
||||||
m.process_event('fall')
|
|
||||||
print(m.pformat())
|
|
||||||
print(m.current_state)
|
|
||||||
print(m.terminated)
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
.. testoutput::
|
|
||||||
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
| down[^] | jump | up | . | . |
|
|
||||||
| @up | fall | down | . | . |
|
|
||||||
+---------+-------+------+----------+---------+
|
|
||||||
up
|
|
||||||
False
|
|
||||||
+----------+-------+------+----------+---------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+----------+-------+------+----------+---------+
|
|
||||||
| @down[^] | jump | up | . | . |
|
|
||||||
| up | fall | down | . | . |
|
|
||||||
+----------+-------+------+----------+---------+
|
|
||||||
down
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------
|
|
||||||
Running a complex dog-barking machine
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
.. testcode::
|
|
||||||
|
|
||||||
from automaton import machines
|
|
||||||
from automaton import runners
|
|
||||||
|
|
||||||
|
|
||||||
# These reaction functions will get triggered when the registered state
|
|
||||||
# and event occur, it is expected to provide a new event that reacts to the
|
|
||||||
# new stable state (so that the state-machine can transition to a new
|
|
||||||
# stable state, and repeat, until the machine ends up in a terminal
|
|
||||||
# state, whereby it will stop...)
|
|
||||||
|
|
||||||
def react_to_squirrel(old_state, new_state, event_that_triggered):
|
|
||||||
return "gets petted"
|
|
||||||
|
|
||||||
|
|
||||||
def react_to_wagging(old_state, new_state, event_that_triggered):
|
|
||||||
return "gets petted"
|
|
||||||
|
|
||||||
|
|
||||||
m = machines.FiniteMachine()
|
|
||||||
|
|
||||||
m.add_state("sits")
|
|
||||||
m.add_state("lies down", terminal=True)
|
|
||||||
m.add_state("barks")
|
|
||||||
m.add_state("wags tail")
|
|
||||||
|
|
||||||
m.default_start_state = 'sits'
|
|
||||||
|
|
||||||
m.add_transition("sits", "barks", "squirrel!")
|
|
||||||
m.add_transition("barks", "wags tail", "gets petted")
|
|
||||||
m.add_transition("wags tail", "lies down", "gets petted")
|
|
||||||
|
|
||||||
m.add_reaction("barks", "squirrel!", react_to_squirrel)
|
|
||||||
m.add_reaction('wags tail', "gets petted", react_to_wagging)
|
|
||||||
|
|
||||||
print(m.pformat())
|
|
||||||
r = runners.FiniteRunner(m)
|
|
||||||
for (old_state, new_state) in r.run_iter("squirrel!"):
|
|
||||||
print("Leaving '%s'" % old_state)
|
|
||||||
print("Entered '%s'" % new_state)
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
.. testoutput::
|
|
||||||
|
|
||||||
+--------------+-------------+-----------+----------+---------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+--------------+-------------+-----------+----------+---------+
|
|
||||||
| barks | gets petted | wags tail | . | . |
|
|
||||||
| lies down[$] | . | . | . | . |
|
|
||||||
| sits[^] | squirrel! | barks | . | . |
|
|
||||||
| wags tail | gets petted | lies down | . | . |
|
|
||||||
+--------------+-------------+-----------+----------+---------+
|
|
||||||
Leaving 'sits'
|
|
||||||
Entered 'barks'
|
|
||||||
Leaving 'barks'
|
|
||||||
Entered 'wags tail'
|
|
||||||
Leaving 'wags tail'
|
|
||||||
Entered 'lies down'
|
|
||||||
|
|
||||||
|
|
||||||
------------------------------------
|
|
||||||
Creating a complex CD-player machine
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. testcode::
|
|
||||||
|
|
||||||
from automaton import machines
|
|
||||||
|
|
||||||
|
|
||||||
def print_on_enter(new_state, triggered_event):
|
|
||||||
print("Entered '%s' due to '%s'" % (new_state, triggered_event))
|
|
||||||
|
|
||||||
|
|
||||||
def print_on_exit(old_state, triggered_event):
|
|
||||||
print("Exiting '%s' due to '%s'" % (old_state, triggered_event))
|
|
||||||
|
|
||||||
|
|
||||||
m = machines.FiniteMachine()
|
|
||||||
|
|
||||||
m.add_state('stopped', on_enter=print_on_enter, on_exit=print_on_exit)
|
|
||||||
m.add_state('opened', on_enter=print_on_enter, on_exit=print_on_exit)
|
|
||||||
m.add_state('closed', on_enter=print_on_enter, on_exit=print_on_exit)
|
|
||||||
m.add_state('playing', on_enter=print_on_enter, on_exit=print_on_exit)
|
|
||||||
m.add_state('paused', on_enter=print_on_enter, on_exit=print_on_exit)
|
|
||||||
|
|
||||||
m.add_transition('stopped', 'playing', 'play')
|
|
||||||
m.add_transition('stopped', 'opened', 'open_close')
|
|
||||||
m.add_transition('stopped', 'stopped', 'stop')
|
|
||||||
|
|
||||||
m.add_transition('opened', 'closed', 'open_close')
|
|
||||||
|
|
||||||
m.add_transition('closed', 'opened', 'open_close')
|
|
||||||
m.add_transition('closed', 'stopped', 'cd_detected')
|
|
||||||
|
|
||||||
m.add_transition('playing', 'stopped', 'stop')
|
|
||||||
m.add_transition('playing', 'paused', 'pause')
|
|
||||||
m.add_transition('playing', 'opened', 'open_close')
|
|
||||||
|
|
||||||
m.add_transition('paused', 'playing', 'play')
|
|
||||||
m.add_transition('paused', 'stopped', 'stop')
|
|
||||||
m.add_transition('paused', 'opened', 'open_close')
|
|
||||||
|
|
||||||
m.default_start_state = 'closed'
|
|
||||||
|
|
||||||
m.initialize()
|
|
||||||
print(m.pformat())
|
|
||||||
|
|
||||||
for event in ['cd_detected', 'play', 'pause', 'play', 'stop',
|
|
||||||
'open_close', 'open_close']:
|
|
||||||
m.process_event(event)
|
|
||||||
print(m.pformat())
|
|
||||||
print("=============")
|
|
||||||
print("Current state => %s" % m.current_state)
|
|
||||||
print("=============")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
.. testoutput::
|
|
||||||
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
| @closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
Exiting 'closed' due to 'cd_detected'
|
|
||||||
Entered 'stopped' due to 'cd_detected'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => stopped
|
|
||||||
=============
|
|
||||||
Exiting 'stopped' due to 'play'
|
|
||||||
Entered 'playing' due to 'play'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| @playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => playing
|
|
||||||
=============
|
|
||||||
Exiting 'playing' due to 'pause'
|
|
||||||
Entered 'paused' due to 'pause'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| @paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| @paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => paused
|
|
||||||
=============
|
|
||||||
Exiting 'paused' due to 'play'
|
|
||||||
Entered 'playing' due to 'play'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| @playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => playing
|
|
||||||
=============
|
|
||||||
Exiting 'playing' due to 'stop'
|
|
||||||
Entered 'stopped' due to 'stop'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| @stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => stopped
|
|
||||||
=============
|
|
||||||
Exiting 'stopped' due to 'open_close'
|
|
||||||
Entered 'opened' due to 'open_close'
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| @opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => opened
|
|
||||||
=============
|
|
||||||
Exiting 'opened' due to 'open_close'
|
|
||||||
Entered 'closed' due to 'open_close'
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
| @closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| @closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+------------+-------------+---------+----------------+---------------+
|
|
||||||
=============
|
|
||||||
Current state => closed
|
|
||||||
=============
|
|
||||||
|
|
||||||
----------------------------------------------------------
|
|
||||||
Creating a complex CD-player machine (using a state-space)
|
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
This example is equivalent to the prior one but creates a machine in
|
|
||||||
a more declarative manner. Instead of calling ``add_state``
|
|
||||||
and ``add_transition`` a explicit and declarative format can be used. For
|
|
||||||
example to create the same machine:
|
|
||||||
|
|
||||||
.. testcode::
|
|
||||||
|
|
||||||
from automaton import machines
|
|
||||||
|
|
||||||
|
|
||||||
def print_on_enter(new_state, triggered_event):
|
|
||||||
print("Entered '%s' due to '%s'" % (new_state, triggered_event))
|
|
||||||
|
|
||||||
|
|
||||||
def print_on_exit(old_state, triggered_event):
|
|
||||||
print("Exiting '%s' due to '%s'" % (old_state, triggered_event))
|
|
||||||
|
|
||||||
# This will contain all the states and transitions that our machine will
|
|
||||||
# allow, the format is relatively simple and designed to be easy to use.
|
|
||||||
state_space = [
|
|
||||||
{
|
|
||||||
'name': 'stopped',
|
|
||||||
'next_states': {
|
|
||||||
# On event 'play' transition to the 'playing' state.
|
|
||||||
'play': 'playing',
|
|
||||||
'open_close': 'opened',
|
|
||||||
'stop': 'stopped',
|
|
||||||
},
|
|
||||||
'on_enter': print_on_enter,
|
|
||||||
'on_exit': print_on_exit,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'opened',
|
|
||||||
'next_states': {
|
|
||||||
'open_close': 'closed',
|
|
||||||
},
|
|
||||||
'on_enter': print_on_enter,
|
|
||||||
'on_exit': print_on_exit,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'closed',
|
|
||||||
'next_states': {
|
|
||||||
'open_close': 'opened',
|
|
||||||
'cd_detected': 'stopped',
|
|
||||||
},
|
|
||||||
'on_enter': print_on_enter,
|
|
||||||
'on_exit': print_on_exit,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'playing',
|
|
||||||
'next_states': {
|
|
||||||
'stop': 'stopped',
|
|
||||||
'pause': 'paused',
|
|
||||||
'open_close': 'opened',
|
|
||||||
},
|
|
||||||
'on_enter': print_on_enter,
|
|
||||||
'on_exit': print_on_exit,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'next_states': {
|
|
||||||
'play': 'playing',
|
|
||||||
'stop': 'stopped',
|
|
||||||
'open_close': 'opened',
|
|
||||||
},
|
|
||||||
'on_enter': print_on_enter,
|
|
||||||
'on_exit': print_on_exit,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
m = machines.FiniteMachine.build(state_space)
|
|
||||||
m.default_start_state = 'closed'
|
|
||||||
print(m.pformat())
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
.. testoutput::
|
|
||||||
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| Start | Event | End | On Enter | On Exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
| closed[^] | cd_detected | stopped | print_on_enter | print_on_exit |
|
|
||||||
| closed[^] | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| opened | open_close | closed | print_on_enter | print_on_exit |
|
|
||||||
| paused | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| paused | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| paused | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| playing | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| playing | pause | paused | print_on_enter | print_on_exit |
|
|
||||||
| playing | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
| stopped | open_close | opened | print_on_enter | print_on_exit |
|
|
||||||
| stopped | play | playing | print_on_enter | print_on_exit |
|
|
||||||
| stopped | stop | stopped | print_on_enter | print_on_exit |
|
|
||||||
+-----------+-------------+---------+----------------+---------------+
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
As can be seen the two tables from this example and the prior one are
|
|
||||||
exactly the same.
|
|
|
@ -1,17 +0,0 @@
|
||||||
========
|
|
||||||
Features
|
|
||||||
========
|
|
||||||
|
|
||||||
Machines
|
|
||||||
--------
|
|
||||||
|
|
||||||
* A :py:class:`.automaton.machines.FiniteMachine` state machine.
|
|
||||||
* A :py:class:`.automaton.machines.HierarchicalFiniteMachine` hierarchical
|
|
||||||
state machine.
|
|
||||||
|
|
||||||
Runners
|
|
||||||
-------
|
|
||||||
|
|
||||||
* A :py:class:`.automaton.runners.FiniteRunner` state machine runner.
|
|
||||||
* A :py:class:`.automaton.runners.HierarchicalRunner` hierarchical state
|
|
||||||
machine runner.
|
|
|
@ -1,2 +0,0 @@
|
||||||
.. include:: ../../../ChangeLog
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
======================
|
|
||||||
automaton User Guide
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
features
|
|
||||||
examples
|
|
||||||
history
|
|
|
@ -1,279 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# automaton Release Notes documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its
|
|
||||||
# containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
# needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
'openstackdocstheme',
|
|
||||||
'reno.sphinxext',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
# source_encoding = 'utf-8-sig'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'automaton Release Notes'
|
|
||||||
copyright = u'2016, automaton Developers'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
import pkg_resources
|
|
||||||
release = pkg_resources.get_distribution('automaton').version
|
|
||||||
# The short X.Y version.
|
|
||||||
version = release
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
# language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
# today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
# today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = []
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all
|
|
||||||
# documents.
|
|
||||||
# default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
# add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
# add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
# show_authors = False
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
# modindex_common_prefix = []
|
|
||||||
|
|
||||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
|
||||||
# keep_warnings = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
html_theme = 'openstackdocs'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
# html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
# html_theme_path = []
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
# html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
# html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
|
||||||
# of the sidebar.
|
|
||||||
# html_logo = None
|
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
# html_favicon = None
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# Add any extra paths that contain custom files (such as robots.txt or
|
|
||||||
# .htaccess) here, relative to this directory. These files are copied
|
|
||||||
# directly to the root of the documentation.
|
|
||||||
# html_extra_path = []
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
# html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
# html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
# html_sidebars = {}
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
# html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# html_domain_indices = True
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
# html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
# html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
# html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
|
||||||
# html_show_sphinx = True
|
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
|
||||||
# html_show_copyright = True
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
# html_use_opensearch = ''
|
|
||||||
|
|
||||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
# html_file_suffix = None
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'automatonReleaseNotesdoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
# 'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
# 'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
# 'preamble': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title,
|
|
||||||
# author, documentclass [howto, manual, or own class]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'automatonReleaseNotes.tex',
|
|
||||||
u'automaton Release Notes Documentation',
|
|
||||||
u'automaton Developers', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
# latex_logo = None
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
|
||||||
# not chapters.
|
|
||||||
# latex_use_parts = False
|
|
||||||
|
|
||||||
# If true, show page references after internal links.
|
|
||||||
# latex_show_pagerefs = False
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
# latex_show_urls = False
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
# latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# latex_domain_indices = True
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
('index', 'automatonreleasenotes',
|
|
||||||
u'automaton Release Notes Documentation',
|
|
||||||
[u'automaton Developers'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
# man_show_urls = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
('index', 'automatonReleaseNotes',
|
|
||||||
u'automaton Release Notes Documentation',
|
|
||||||
u'automaton Developers', 'automatonReleaseNotes',
|
|
||||||
'An OpenStack library for parsing configuration options from the command'
|
|
||||||
' line and configuration files.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
# texinfo_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# texinfo_domain_indices = True
|
|
||||||
|
|
||||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
|
||||||
# texinfo_show_urls = 'footnote'
|
|
||||||
|
|
||||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
|
||||||
# texinfo_no_detailmenu = False
|
|
||||||
|
|
||||||
# -- Options for Internationalization output ------------------------------
|
|
||||||
locale_dirs = ['locale/']
|
|
|
@ -1,9 +0,0 @@
|
||||||
===========================
|
|
||||||
automaton Release Notes
|
|
||||||
===========================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
unreleased
|
|
||||||
ocata
|
|
|
@ -1,6 +0,0 @@
|
||||||
===================================
|
|
||||||
Ocata Series Release Notes
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. release-notes::
|
|
||||||
:branch: origin/stable/ocata
|
|
|
@ -1,5 +0,0 @@
|
||||||
==========================
|
|
||||||
Unreleased Release Notes
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. release-notes::
|
|
|
@ -1,15 +0,0 @@
|
||||||
# The order of packages is significant, because pip processes them in the order
|
|
||||||
# of appearance. Changing the order has an impact on the overall integration
|
|
||||||
# process, which may cause wedges in the gate later.
|
|
||||||
|
|
||||||
# See: https://bugs.launchpad.net/pbr/+bug/1384919 for why this is here...
|
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
|
||||||
|
|
||||||
# Python 2->3 compatibility library.
|
|
||||||
six>=1.9.0 # MIT
|
|
||||||
|
|
||||||
# For deprecation of things
|
|
||||||
debtcollector>=1.2.0 # Apache-2.0
|
|
||||||
|
|
||||||
# For pretty formatting machines/state tables...
|
|
||||||
PrettyTable<0.8,>=0.7.1 # BSD
|
|
42
setup.cfg
42
setup.cfg
|
@ -1,42 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = automaton
|
|
||||||
summary = Friendly state machines for python.
|
|
||||||
author = OpenStack
|
|
||||||
author-email = openstack-dev@lists.openstack.org
|
|
||||||
home-page = https://docs.openstack.org/automaton/latest/
|
|
||||||
description-file =
|
|
||||||
README.rst
|
|
||||||
classifier =
|
|
||||||
Development Status :: 3 - Alpha
|
|
||||||
Intended Audience :: Developers
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Operating System :: POSIX
|
|
||||||
Programming Language :: Python :: 2
|
|
||||||
Programming Language :: Python :: 2.7
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3.5
|
|
||||||
Topic :: Software Development :: Libraries
|
|
||||||
|
|
||||||
[global]
|
|
||||||
setup-hooks =
|
|
||||||
pbr.hooks.setup_hook
|
|
||||||
|
|
||||||
[files]
|
|
||||||
packages =
|
|
||||||
automaton
|
|
||||||
|
|
||||||
[nosetests]
|
|
||||||
cover-erase = true
|
|
||||||
verbosity = 2
|
|
||||||
|
|
||||||
[wheel]
|
|
||||||
universal = 1
|
|
||||||
|
|
||||||
[build_sphinx]
|
|
||||||
source-dir = doc/source
|
|
||||||
build-dir = doc/build
|
|
||||||
all_files = 1
|
|
||||||
warning-is-error = 1
|
|
||||||
|
|
||||||
[upload_sphinx]
|
|
||||||
upload-dir = doc/build/html
|
|
29
setup.py
29
setup.py
|
@ -1,29 +0,0 @@
|
||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
|
||||||
# setuptools if some other modules registered functions in `atexit`.
|
|
||||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
|
||||||
try:
|
|
||||||
import multiprocessing # noqa
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
setup_requires=['pbr>=2.0.0'],
|
|
||||||
pbr=True)
|
|
|
@ -1,15 +0,0 @@
|
||||||
# The order of packages is significant, because pip processes them in the order
|
|
||||||
# of appearance. Changing the order has an impact on the overall integration
|
|
||||||
# process, which may cause wedges in the gate later.
|
|
||||||
|
|
||||||
hacking<0.11,>=0.10.0
|
|
||||||
|
|
||||||
doc8 # Apache-2.0
|
|
||||||
coverage!=4.4,>=4.0 # Apache-2.0
|
|
||||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
|
||||||
sphinx>=1.6.2 # BSD
|
|
||||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
|
||||||
oslotest>=1.10.0 # Apache-2.0
|
|
||||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
|
||||||
testtools>=1.4.0 # MIT
|
|
||||||
reno!=2.3.1,>=1.8.0 # Apache-2.0
|
|
|
@ -1,30 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Client constraint file contains this client version pin that is in conflict
|
|
||||||
# with installing the client from source. We should remove the version pin in
|
|
||||||
# the constraints file before applying it for from-source installation.
|
|
||||||
|
|
||||||
CONSTRAINTS_FILE="$1"
|
|
||||||
shift 1
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
|
|
||||||
# published to logs.openstack.org for easy debugging.
|
|
||||||
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
|
|
||||||
|
|
||||||
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
|
|
||||||
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
|
|
||||||
fi
|
|
||||||
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
|
|
||||||
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
|
|
||||||
|
|
||||||
pip install -c"$localfile" openstack-requirements
|
|
||||||
|
|
||||||
# This is the main purpose of the script: Allow local installation of
|
|
||||||
# the current repo. It is listed in constraints file and thus any
|
|
||||||
# install will be constrained and we need to unconstrain it.
|
|
||||||
edit-constraints "$localfile" -- "$CLIENT_NAME"
|
|
||||||
|
|
||||||
pip install -c"$localfile" -U "$@"
|
|
||||||
exit $?
|
|
39
tox.ini
39
tox.ini
|
@ -1,39 +0,0 @@
|
||||||
[tox]
|
|
||||||
minversion = 2.0
|
|
||||||
envlist = py35,py27,pypy,docs,pep8,venv
|
|
||||||
|
|
||||||
[testenv:docs]
|
|
||||||
basepython = python2.7
|
|
||||||
commands = python setup.py build_sphinx
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
setenv =
|
|
||||||
VIRTUAL_ENV={envdir}
|
|
||||||
BRANCH_NAME=master
|
|
||||||
CLIENT_NAME=automaton
|
|
||||||
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
|
||||||
deps = -r{toxinidir}/test-requirements.txt
|
|
||||||
commands = python setup.py test --slowest --testr-args='{posargs}'
|
|
||||||
|
|
||||||
[testenv:pep8]
|
|
||||||
commands = flake8 {posargs}
|
|
||||||
|
|
||||||
[testenv:py27]
|
|
||||||
commands =
|
|
||||||
python setup.py testr --slowest --testr-args='{posargs}'
|
|
||||||
sphinx-build -b doctest doc/source doc/build
|
|
||||||
doc8 --ignore-path "doc/source/history.rst" doc/source
|
|
||||||
|
|
||||||
[testenv:venv]
|
|
||||||
basepython = python2.7
|
|
||||||
commands = {posargs}
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
show-source = True
|
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
|
||||||
|
|
||||||
[testenv:cover]
|
|
||||||
commands = python setup.py test --coverage --testr-args="{posargs}"
|
|
||||||
|
|
||||||
[testenv:releasenotes]
|
|
||||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
|
Loading…
Reference in New Issue