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:
Tony Breeds 2017-09-12 15:55:50 -06:00
parent d34d15737f
commit 8843bea315
41 changed files with 14 additions and 2867 deletions

View File

@ -1,8 +0,0 @@
[run]
branch = True
source = automaton
omit = automaton/tests/*
[report]
ignore_errors = True
precision = 2

60
.gitignore vendored
View File

@ -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/

View File

@ -1,4 +0,0 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/automaton.git

View File

@ -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

View File

@ -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.

View File

@ -1,5 +0,0 @@
Automaton Style Commandments
===============================================
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/

202
LICENSE
View File

@ -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.

14
README Normal file
View File

@ -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.

View File

@ -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

View File

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -1,2 +0,0 @@
[python: **.py]

View File

@ -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}

View File

@ -1,5 +0,0 @@
============
Contributing
============
.. include:: ../../../CONTRIBUTING.rst

View File

@ -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`

View File

@ -1,12 +0,0 @@
============
Installation
============
At the command line::
$ pip install automaton
Or, if you have virtualenvwrapper installed::
$ mkvirtualenv automaton
$ pip install automaton

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -1,2 +0,0 @@
.. include:: ../../../ChangeLog

View File

@ -1,10 +0,0 @@
======================
automaton User Guide
======================
.. toctree::
:maxdepth: 2
features
examples
history

View File

@ -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/']

View File

@ -1,9 +0,0 @@
===========================
automaton Release Notes
===========================
.. toctree::
:maxdepth: 1
unreleased
ocata

View File

@ -1,6 +0,0 @@
===================================
Ocata Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/ocata

View File

@ -1,5 +0,0 @@
==========================
Unreleased Release Notes
==========================
.. release-notes::

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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