[Python-modules-commits] [automat] 01/03: New upstream version 0.6.0
Free Ekanayaka
freee at moszumanska.debian.org
Mon Aug 28 09:06:49 UTC 2017
This is an automated email from the git hooks/post-receive script.
freee pushed a commit to branch master
in repository automat.
commit b74098695ba9cacd97d5a29f78c0d4ee443e84b9
Author: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Mon Aug 28 08:46:30 2017 +0000
New upstream version 0.6.0
---
.travis.yml | 41 ++++++++---
Automat.egg-info/PKG-INFO | 6 +-
Automat.egg-info/SOURCES.txt | 2 +
PKG-INFO | 6 +-
automat/_core.py | 22 +++++-
automat/_methodical.py | 65 ++++++++++++++++--
automat/_test/test_methodical.py | 105 ++++++++++++++++++++++++++++
automat/_test/test_trace.py | 98 ++++++++++++++++++++++++++
docs/debugging.md | 144 +++++++++++++++++++++++++++++++++++++++
setup.cfg | 1 +
setup.py | 4 +-
tox.ini | 2 +-
12 files changed, 468 insertions(+), 28 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index f5ea3c1..d56e505 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,35 @@
language: python
-python: 2.7
-env:
- - TOX_ENV=py27-extras
- - TOX_ENV=py27-noextras
- - TOX_ENV=pypy-extras
- - TOX_ENV=pypy-noextras
- - TOX_ENV=py33-extras
- - TOX_ENV=py33-noextras
- - TOX_ENV=py34-extras
- - TOX_ENV=py34-noextras
+matrix:
+ include:
+ - python: 2.7
+ env: TOX_ENV=py27-extras
+ - python: 2.7
+ env: TOX_ENV=py27-noextras
+
+ - python: pypy
+ env: TOX_ENV=pypy-extras
+ - python: pypy
+ env: TOX_ENV=pypy-noextras
+
+ - python: 3.3
+ env: TOX_ENV=py33-extras
+ - python: 3.3
+ env: TOX_ENV=py33-noextras
+
+ - python: 3.4
+ env: TOX_ENV=py34-extras
+ - python: 3.4
+ env: TOX_ENV=py34-noextras
+
+ - python: 3.5
+ env: TOX_ENV=py35-extras
+ - python: 3.5
+ env: TOX_ENV=py35-noextras
+
+ - python: 3.6
+ env: TOX_ENV=py36-extras
+ - python: 3.6
+ env: TOX_ENV=py36-noextras
install:
- sudo apt-get install graphviz
diff --git a/Automat.egg-info/PKG-INFO b/Automat.egg-info/PKG-INFO
index c173d43..c42fe1f 100644
--- a/Automat.egg-info/PKG-INFO
+++ b/Automat.egg-info/PKG-INFO
@@ -1,10 +1,10 @@
Metadata-Version: 1.0
Name: Automat
-Version: 0.5.0
+Version: 0.6.0
Summary: Self-service finite-state machines for the programmer on the go.
Home-page: https://github.com/glyph/Automat
-Author: UNKNOWN
-Author-email: UNKNOWN
+Author: Glyph
+Author-email: glyph at twistedmatrix.com
License: MIT
Description:
Automat
diff --git a/Automat.egg-info/SOURCES.txt b/Automat.egg-info/SOURCES.txt
index 6511a7b..69d9b6d 100644
--- a/Automat.egg-info/SOURCES.txt
+++ b/Automat.egg-info/SOURCES.txt
@@ -21,7 +21,9 @@ automat/_test/__init__.py
automat/_test/test_core.py
automat/_test/test_discover.py
automat/_test/test_methodical.py
+automat/_test/test_trace.py
automat/_test/test_visualize.py
+docs/debugging.md
docs/examples/automat_example.py
docs/examples/io_coffee_example.py
docs/examples/lightswitch.py
diff --git a/PKG-INFO b/PKG-INFO
index c173d43..c42fe1f 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,10 +1,10 @@
Metadata-Version: 1.0
Name: Automat
-Version: 0.5.0
+Version: 0.6.0
Summary: Self-service finite-state machines for the programmer on the go.
Home-page: https://github.com/glyph/Automat
-Author: UNKNOWN
-Author-email: UNKNOWN
+Author: Glyph
+Author-email: glyph at twistedmatrix.com
License: MIT
Description:
Automat
diff --git a/automat/_core.py b/automat/_core.py
index 920af13..273d36a 100644
--- a/automat/_core.py
+++ b/automat/_core.py
@@ -68,8 +68,16 @@ class Automaton(object):
def addTransition(self, inState, inputSymbol, outState, outputSymbols):
"""
- Add the given transition to the outputSymbol.
- """
+ Add the given transition to the outputSymbol. Raise ValueError if
+ there is already a transition with the same inState and inputSymbol.
+ """
+ # keeping self._transitions in a flat list makes addTransition
+ # O(n^2), but state machines don't tend to have hundreds of
+ # transitions.
+ for (anInState, anInputSymbol, anOutState, _) in self._transitions:
+ if (anInState == inState and anInputSymbol == inputSymbol):
+ raise ValueError(
+ "already have transition from {} via {}".format(inState, inputSymbol))
self._transitions.add(
(inState, inputSymbol, outState, tuple(outputSymbols))
)
@@ -137,7 +145,10 @@ class Transitioner(object):
def __init__(self, automaton, initialState):
self._automaton = automaton
self._state = initialState
+ self._tracer = None
+ def setTrace(self, tracer):
+ self._tracer = tracer
def transition(self, inputSymbol):
"""
@@ -145,5 +156,10 @@ class Transitioner(object):
"""
outState, outputSymbols = self._automaton.outputForInput(self._state,
inputSymbol)
+ outTracer = None
+ if self._tracer:
+ outTracer = self._tracer(self._state._name(),
+ inputSymbol._name(),
+ outState._name())
self._state = outState
- return outputSymbols
+ return (outputSymbols, outTracer)
diff --git a/automat/_methodical.py b/automat/_methodical.py
index 08709b3..4f83ce4 100644
--- a/automat/_methodical.py
+++ b/automat/_methodical.py
@@ -31,7 +31,7 @@ def _keywords_only(f):
return g
- at attr.s
+ at attr.s(frozen=True)
class MethodicalState(object):
"""
A state for a L{MethodicalMachine}.
@@ -61,6 +61,9 @@ class MethodicalState(object):
))
self.machine._oneTransition(self, input, enter, outputs, collector)
+ def _name(self):
+ return self.method.__name__
+
def _transitionerFromInstance(oself, symbol, automaton):
"""
@@ -76,13 +79,36 @@ def _transitionerFromInstance(oself, symbol, automaton):
return transitioner
+def _empty():
+ pass
+
+def _docstring():
+ """docstring"""
+
+def assertNoCode(inst, attribute, f):
+ # The function body must be empty, i.e. "pass" or "return None", which
+ # both yield the same bytecode: LOAD_CONST (None), RETURN_VALUE. We also
+ # accept functions with only a docstring, which yields slightly different
+ # bytecode, because the "None" is put in a different constant slot.
+
+ # Unfortunately, this does not catch function bodies that return a
+ # constant value, e.g. "return 1", because their code is identical to a
+ # "return None". They differ in the contents of their constant table, but
+ # checking that would require us to parse the bytecode, find the index
+ # being returned, then making sure the table has a None at that index.
+
+ if f.__code__.co_code not in (_empty.__code__.co_code,
+ _docstring.__code__.co_code):
+ raise ValueError("function body must be empty")
+
+
@attr.s(cmp=False, hash=False)
class MethodicalInput(object):
"""
An input for a L{MethodicalMachine}.
"""
automaton = attr.ib(repr=False)
- method = attr.ib()
+ method = attr.ib(validator=assertNoCode)
symbol = attr.ib(repr=False)
collectors = attr.ib(default=attr.Factory(dict), repr=False)
@@ -100,14 +126,22 @@ class MethodicalInput(object):
def doInput(*args, **kwargs):
self.method(oself, *args, **kwargs)
previousState = transitioner._state
- outputs = transitioner.transition(self)
+ (outputs, outTracer) = transitioner.transition(self)
collector = self.collectors[previousState]
- return collector(output(oself, *args, **kwargs)
- for output in outputs)
+ values = []
+ for output in outputs:
+ if outTracer:
+ outTracer(output._name())
+ value = output(oself, *args, **kwargs)
+ values.append(value)
+ return collector(values)
return doInput
+ def _name(self):
+ return self.method.__name__
- at attr.s
+
+ at attr.s(frozen=True)
class MethodicalOutput(object):
"""
An output for a L{MethodicalMachine}.
@@ -134,6 +168,22 @@ class MethodicalOutput(object):
"""
return self.method(oself, *args, **kwargs)
+ def _name(self):
+ return self.method.__name__
+
+ at attr.s(cmp=False, hash=False)
+class MethodicalTracer(object):
+ automaton = attr.ib(repr=False)
+ symbol = attr.ib(repr=False)
+
+
+ def __get__(self, oself, type=None):
+ transitioner = _transitionerFromInstance(oself, self.symbol,
+ self.automaton)
+ def setTrace(tracer):
+ transitioner.setTrace(tracer)
+ return setTrace
+
counter = count()
@@ -289,6 +339,9 @@ class MethodicalMachine(object):
return unserialize
return decorator
+ @property
+ def _setTrace(self):
+ return MethodicalTracer(self._automaton, self._symbol)
def asDigraph(self):
"""
diff --git a/automat/_test/test_methodical.py b/automat/_test/test_methodical.py
index c94ee7e..8a5a222 100644
--- a/automat/_test/test_methodical.py
+++ b/automat/_test/test_methodical.py
@@ -7,6 +7,7 @@ from functools import reduce
from unittest import TestCase
from .. import MethodicalMachine, NoTransition
+from .. import _methodical
class MethodicalTests(TestCase):
"""
@@ -209,6 +210,87 @@ class MethodicalTests(TestCase):
self.assertEqual(m._x, 3)
+ def test_inputFunctionsMustBeEmpty(self):
+ """
+ The wrapped input function must have an empty body.
+ """
+ # input functions are executed to assert that the signature matches,
+ # but their body must be empty
+
+ _methodical._empty() # chase coverage
+ _methodical._docstring()
+
+ class Mechanism(object):
+ m = MethodicalMachine()
+ with self.assertRaises(ValueError) as cm:
+ @m.input()
+ def input(self):
+ "an input"
+ list() # pragma: no cover
+ self.assertEqual(str(cm.exception), "function body must be empty")
+
+ # all three of these cases should be valid. Functions/methods with
+ # docstrings produce slightly different bytecode than ones without.
+
+ class MechanismWithDocstring(object):
+ m = MethodicalMachine()
+ @m.input()
+ def input(self):
+ "an input"
+ @m.state(initial=True)
+ def start(self):
+ "starting state"
+ start.upon(input, enter=start, outputs=[])
+ MechanismWithDocstring().input()
+
+ class MechanismWithPass(object):
+ m = MethodicalMachine()
+ @m.input()
+ def input(self):
+ pass
+ @m.state(initial=True)
+ def start(self):
+ "starting state"
+ start.upon(input, enter=start, outputs=[])
+ MechanismWithPass().input()
+
+ class MechanismWithDocstringAndPass(object):
+ m = MethodicalMachine()
+ @m.input()
+ def input(self):
+ "an input"
+ pass
+ @m.state(initial=True)
+ def start(self):
+ "starting state"
+ start.upon(input, enter=start, outputs=[])
+ MechanismWithDocstringAndPass().input()
+
+ class MechanismReturnsNone(object):
+ m = MethodicalMachine()
+ @m.input()
+ def input(self):
+ return None
+ @m.state(initial=True)
+ def start(self):
+ "starting state"
+ start.upon(input, enter=start, outputs=[])
+ MechanismReturnsNone().input()
+
+ class MechanismWithDocstringAndReturnsNone(object):
+ m = MethodicalMachine()
+ @m.input()
+ def input(self):
+ "an input"
+ return None
+ @m.state(initial=True)
+ def start(self):
+ "starting state"
+ start.upon(input, enter=start, outputs=[])
+ MechanismWithDocstringAndReturnsNone().input()
+
+
+
def test_inputOutputMismatch(self):
"""
All the argument lists of the outputs for a given input must match; if
@@ -253,6 +335,29 @@ class MethodicalTests(TestCase):
"The second initial state -- results in a ValueError."
+ def test_multipleTransitionsFailure(self):
+ """
+ A L{MethodicalMachine} can only have one transition per start/event
+ pair.
+ """
+
+ class WillFail(object):
+ m = MethodicalMachine()
+
+ @m.state(initial=True)
+ def start(self):
+ "We start here."
+ @m.state()
+ def end(self):
+ "Rainbows end."
+
+ @m.input()
+ def event(self):
+ "An event."
+ start.upon(event, enter=end, outputs=[])
+ with self.assertRaises(ValueError):
+ start.upon(event, enter=end, outputs=[])
+
def test_badTransitionForCurrentState(self):
"""
Calling any input method that lacks a transition for the machine's
diff --git a/automat/_test/test_trace.py b/automat/_test/test_trace.py
new file mode 100644
index 0000000..6d7433b
--- /dev/null
+++ b/automat/_test/test_trace.py
@@ -0,0 +1,98 @@
+from unittest import TestCase
+from .._methodical import MethodicalMachine
+
+class SampleObject(object):
+ mm = MethodicalMachine()
+
+ @mm.state(initial=True)
+ def begin(self):
+ "initial state"
+ @mm.state()
+ def middle(self):
+ "middle state"
+ @mm.state()
+ def end(self):
+ "end state"
+
+ @mm.input()
+ def go1(self):
+ "sample input"
+ @mm.input()
+ def go2(self):
+ "sample input"
+ @mm.input()
+ def back(self):
+ "sample input"
+
+ @mm.output()
+ def out(self):
+ "sample output"
+
+ setTrace = mm._setTrace
+
+ begin.upon(go1, middle, [out])
+ middle.upon(go2, end, [out])
+ end.upon(back, middle, [])
+ middle.upon(back, begin, [])
+
+class TraceTests(TestCase):
+ def test_only_inputs(self):
+ traces = []
+ def tracer(old_state, input, new_state):
+ traces.append((old_state, input, new_state))
+ return None # "I only care about inputs, not outputs"
+ s = SampleObject()
+ s.setTrace(tracer)
+
+ s.go1()
+ self.assertEqual(traces, [("begin", "go1", "middle"),
+ ])
+
+ s.go2()
+ self.assertEqual(traces, [("begin", "go1", "middle"),
+ ("middle", "go2", "end"),
+ ])
+ s.setTrace(None)
+ s.back()
+ self.assertEqual(traces, [("begin", "go1", "middle"),
+ ("middle", "go2", "end"),
+ ])
+ s.go2()
+ self.assertEqual(traces, [("begin", "go1", "middle"),
+ ("middle", "go2", "end"),
+ ])
+
+ def test_inputs_and_outputs(self):
+ traces = []
+ def tracer(old_state, input, new_state):
+ traces.append((old_state, input, new_state, None))
+ def trace_outputs(output):
+ traces.append((old_state, input, new_state, output))
+ return trace_outputs # "I care about outputs too"
+ s = SampleObject()
+ s.setTrace(tracer)
+
+ s.go1()
+ self.assertEqual(traces, [("begin", "go1", "middle", None),
+ ("begin", "go1", "middle", "out"),
+ ])
+
+ s.go2()
+ self.assertEqual(traces, [("begin", "go1", "middle", None),
+ ("begin", "go1", "middle", "out"),
+ ("middle", "go2", "end", None),
+ ("middle", "go2", "end", "out"),
+ ])
+ s.setTrace(None)
+ s.back()
+ self.assertEqual(traces, [("begin", "go1", "middle", None),
+ ("begin", "go1", "middle", "out"),
+ ("middle", "go2", "end", None),
+ ("middle", "go2", "end", "out"),
+ ])
+ s.go2()
+ self.assertEqual(traces, [("begin", "go1", "middle", None),
+ ("begin", "go1", "middle", "out"),
+ ("middle", "go2", "end", None),
+ ("middle", "go2", "end", "out"),
+ ])
diff --git a/docs/debugging.md b/docs/debugging.md
new file mode 100644
index 0000000..a654422
--- /dev/null
+++ b/docs/debugging.md
@@ -0,0 +1,144 @@
+# Tracing API
+
+(NOTE: the Tracing API is currently private and unstable. Use it for
+local debugging, but if you think you need to commit code that
+references it, you should either pin your dependency on the current
+version of Automat, or at least be prepared for your application to
+break when this API is changed or removed).
+
+The tracing API lets you assign a callback function that will be invoked each
+time an input event causes the state machine to move from one state to
+another. This can help you figure out problems caused by events occurring in
+the wrong order, or not happening at all. Your callback function can print a
+message to stdout, write something to a logfile, or deliver the information
+in any application-specific way you like. The only restriction is that the
+function must not touch the state machine at all.
+
+To prepare the state machine for tracing, you must assign a name to the
+"_setTrace" method in your class. In this example, we use
+`setTheTracingFunction`, but the name can be anything you like:
+
+```python
+class Sample(object):
+ mm = MethodicalMachine()
+
+ @mm.state(initial=True)
+ def begin(self):
+ "initial state"
+ @mm.state()
+ def end(self):
+ "end state"
+ @mm.input()
+ def go(self):
+ "event that moves us from begin to end"
+ @mm.output()
+ def doThing1(self):
+ "first thing to do"
+ @mm.output()
+ def doThing2(self):
+ "second thing to do"
+
+ setTheTracingFunction = mm._setTrace
+
+ begin.upon(go, enter=end, outputs=[doThing1, doThing2])
+```
+
+Later, after you instantiate the `Sample` object, you can set the tracing
+callback for that particular instance by calling the
+`setTheTracingFunction()` method on it:
+
+```python
+s = Sample()
+def tracer(oldState, input, newState):
+ pass
+s.setTheTracingFunction(tracer)
+```
+
+Note that you cannot shortcut the name-assignment step:
+`s.mm._setTrace(tracer)` will not work, because Automat goes to great
+lengths to hide that `mm` object from external access. And you cannot
+set the tracing function at class-definition time (e.g. a class-level
+`mm._setTrace(tracer)`) because the state machine has merely been
+*defined* at that point, not instantiated (you might eventually have
+multiple instances of the Sample class, each with their own independent
+state machine), and each one can be traced separately.
+
+Since this is a private API, consider using a tolerant `getattr` when
+retrieving the `_getTrace` method. This way, if you do commit code which
+references it, but you only *call* that code during debugging, then at
+least your application or tests won't crash when the API is removed
+entirely:
+
+```
+mm = MethodicalMachine()
+setTheTracingFunction = getattr(mm, "_setTrace", lambda self, f: None)
+```
+
+## The Tracer Callback Function
+
+When the input event is received, before any transitions are made, the tracer
+function is called with three positional arguments:
+
+* `oldState`: a string with the name of the current state
+* `input`: a string with the name of the input event
+* `newState`: a string with the name of the new state
+
+If your tracer function returns None, then you will only be notified about
+the input events. But, if your tracer function returns a callable, then just
+before each output function is executed (if any), that callable will be
+executed with a single `output` argument (as a string).
+
+So if you only care about the transitions, your tracing function can just do:
+
+```python
+ s = Sample()
+ def tracer(oldState, input, newState):
+ print("%s.%s -> %s" % (oldState, input, newState))
+ s.setTheTracingFunction(tracer)
+ s.go()
+ # prints:
+ # begin.go -> end
+```
+
+But if you want to know when each output is invoked (perhaps to compare
+against other log messages emitted from inside those output functions), you
+can do:
+
+```python
+ s = Sample()
+ def tracer(oldState, input, newState):
+ def traceOutputs(output):
+ print("%s.%s -> %s: %s()" % (oldState, input, newState, output))
+ print("%s.%s -> %s" % (oldState, input, newState))
+ return traceOutputs
+ s.setTheTracingFunction(tracer)
+ s.go()
+ # prints:
+ # begin.go -> end
+ # begin.go -> end: doThing1()
+ # begin.go -> end: doThing2()
+```
+
+
+## Tracing Multiple State Machines
+
+If you have multiple state machines in your application, you will probably
+want to pass a different tracing function to each, so your logs can
+distinguish between the transitions of MachineFoo vs those of MachineBar.
+This is particularly important if your application involves network
+communication, where an instance of MachineFoo (e.g. in a client) is
+communication with a sibling instance of MachineFoo (in a server). When
+exercising both sides of this connection in a single process, perhaps in an
+automated test, you will need to clearly mark the first as "foo1" and the
+second as "foo2" to avoid confusion.
+
+```python
+ s1 = Sample()
+ s2 = Sample()
+ def tracer1(oldState, input, newState):
+ print("S1: %s.%s -> %s" % (oldState, input, newState))
+ s1.setTheTracingFunction(tracer1)
+ def tracer2(oldState, input, newState):
+ print("S2: %s.%s -> %s" % (oldState, input, newState))
+ s2.setTheTracingFunction(tracer2)
+```
diff --git a/setup.cfg b/setup.cfg
index adf5ed7..6f08d0e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,4 +4,5 @@ universal = 1
[egg_info]
tag_build =
tag_date = 0
+tag_svn_revision = 0
diff --git a/setup.py b/setup.py
index 5971d97..82e55fc 100644
--- a/setup.py
+++ b/setup.py
@@ -39,8 +39,8 @@ setup(
"automat-visualize = automat._visualize:tool"
],
},
- author_name='Glyph',
- author_mail='glyph at twistedmatrix.com',
+ author='Glyph',
+ author_email='glyph at twistedmatrix.com',
include_package_data=True,
license="MIT",
keywords='fsm finite state machine automata',
diff --git a/tox.ini b/tox.ini
index c401092..fa0a7d7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = coverage-clean,{py27,pypy,py33,py34}-{extras,noextras},coverage-report
+envlist = coverage-clean,{py27,pypy,py33,py34,py35,py36}-{extras,noextras},coverage-report
[testenv]
deps =
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/automat.git
More information about the Python-modules-commits
mailing list