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