[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