Automat-0.8.0/0000755000175000017500000000000013552760067012600 5ustar mrwmrw00000000000000Automat-0.8.0/LICENSE0000644000175000017500000000203512641273151013574 0ustar mrwmrw00000000000000Copyright (c) 2014 Rackspace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Automat-0.8.0/automat/0000755000175000017500000000000013552760067014252 5ustar mrwmrw00000000000000Automat-0.8.0/automat/__init__.py0000644000175000017500000000025112720705662016355 0ustar mrwmrw00000000000000# -*- test-case-name: automat -*- from ._methodical import MethodicalMachine from ._core import NoTransition __all__ = [ 'MethodicalMachine', 'NoTransition', ] Automat-0.8.0/automat/_core.py0000644000175000017500000001132313552700725015706 0ustar mrwmrw00000000000000# -*- test-case-name: automat._test.test_core -*- """ A core state-machine abstraction. Perhaps something that could be replaced with or integrated into machinist. """ from itertools import chain _NO_STATE = "" class NoTransition(Exception): """ A finite state machine in C{state} has no transition for C{symbol}. @param state: the finite state machine's state at the time of the illegal transition. @param symbol: the input symbol for which no transition exists. """ def __init__(self, state, symbol): self.state = state self.symbol = symbol super(Exception, self).__init__( "no transition for {} in {}".format(symbol, state) ) class Automaton(object): """ A declaration of a finite state machine. Note that this is not the machine itself; it is immutable. """ def __init__(self): """ Initialize the set of transitions and the initial state. """ self._initialState = _NO_STATE self._transitions = set() @property def initialState(self): """ Return this automaton's initial state. """ return self._initialState @initialState.setter def initialState(self, state): """ Set this automaton's initial state. Raises a ValueError if this automaton already has an initial state. """ if self._initialState is not _NO_STATE: raise ValueError( "initial state already set to {}".format(self._initialState)) self._initialState = state def addTransition(self, inState, inputSymbol, outState, outputSymbols): """ 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)) ) def allTransitions(self): """ All transitions. """ return frozenset(self._transitions) def inputAlphabet(self): """ The full set of symbols acceptable to this automaton. """ return {inputSymbol for (inState, inputSymbol, outState, outputSymbol) in self._transitions} def outputAlphabet(self): """ The full set of symbols which can be produced by this automaton. """ return set( chain.from_iterable( outputSymbols for (inState, inputSymbol, outState, outputSymbols) in self._transitions ) ) def states(self): """ All valid states; "Q" in the mathematical description of a state machine. """ return frozenset( chain.from_iterable( (inState, outState) for (inState, inputSymbol, outState, outputSymbol) in self._transitions ) ) def outputForInput(self, inState, inputSymbol): """ A 2-tuple of (outState, outputSymbols) for inputSymbol. """ for (anInState, anInputSymbol, outState, outputSymbols) in self._transitions: if (inState, inputSymbol) == (anInState, anInputSymbol): return (outState, list(outputSymbols)) raise NoTransition(state=inState, symbol=inputSymbol) class Transitioner(object): """ The combination of a current state and an L{Automaton}. """ 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): """ Transition between states, returning any outputs. """ 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, outTracer) Automat-0.8.0/automat/_discover.py0000644000175000017500000001041713014133630016563 0ustar mrwmrw00000000000000import collections import inspect from automat import MethodicalMachine from twisted.python.modules import PythonModule, getModule def isOriginalLocation(attr): """ Attempt to discover if this appearance of a PythonAttribute representing a class refers to the module where that class was defined. """ sourceModule = inspect.getmodule(attr.load()) if sourceModule is None: return False currentModule = attr while not isinstance(currentModule, PythonModule): currentModule = currentModule.onObject return currentModule.name == sourceModule.__name__ def findMachinesViaWrapper(within): """ Recursively yield L{MethodicalMachine}s and their FQPNs within a L{PythonModule} or a L{twisted.python.modules.PythonAttribute} wrapper object. Note that L{PythonModule}s may refer to packages, as well. The discovery heuristic considers L{MethodicalMachine} instances that are module-level attributes or class-level attributes accessible from module scope. Machines inside nested classes will be discovered, but those returned from functions or methods will not be. @type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute} @param within: Where to start the search. @return: a generator which yields FQPN, L{MethodicalMachine} pairs. """ queue = collections.deque([within]) visited = set() while queue: attr = queue.pop() value = attr.load() if isinstance(value, MethodicalMachine) and value not in visited: visited.add(value) yield attr.name, value elif (inspect.isclass(value) and isOriginalLocation(attr) and value not in visited): visited.add(value) queue.extendleft(attr.iterAttributes()) elif isinstance(attr, PythonModule) and value not in visited: visited.add(value) queue.extendleft(attr.iterAttributes()) queue.extendleft(attr.iterModules()) class InvalidFQPN(Exception): """ The given FQPN was not a dot-separated list of Python objects. """ class NoModule(InvalidFQPN): """ A prefix of the FQPN was not an importable module or package. """ class NoObject(InvalidFQPN): """ A suffix of the FQPN was not an accessible object """ def wrapFQPN(fqpn): """ Given an FQPN, retrieve the object via the global Python module namespace and wrap it with a L{PythonModule} or a L{twisted.python.modules.PythonAttribute}. """ # largely cribbed from t.p.reflect.namedAny if not fqpn: raise InvalidFQPN("FQPN was empty") components = collections.deque(fqpn.split('.')) if '' in components: raise InvalidFQPN( "name must be a string giving a '.'-separated list of Python " "identifiers, not %r" % (fqpn,)) component = components.popleft() try: module = getModule(component) except KeyError: raise NoModule(component) # find the bottom-most module while components: component = components.popleft() try: module = module[component] except KeyError: components.appendleft(component) break else: module.load() else: return module # find the bottom-most attribute attribute = module for component in components: try: attribute = next(child for child in attribute.iterAttributes() if child.name.rsplit('.', 1)[-1] == component) except StopIteration: raise NoObject('{}.{}'.format(attribute.name, component)) return attribute def findMachines(fqpn): """ Recursively yield L{MethodicalMachine}s and their FQPNs in and under the a Python object specified by an FQPN. The discovery heuristic considers L{MethodicalMachine} instances that are module-level attributes or class-level attributes accessible from module scope. Machines inside nested classes will be discovered, but those returned from functions or methods will not be. @type within: an FQPN @param within: Where to start the search. @return: a generator which yields FQPN, L{MethodicalMachine} pairs. """ return findMachinesViaWrapper(wrapFQPN(fqpn)) Automat-0.8.0/automat/_introspection.py0000644000175000017500000000237213552733405017663 0ustar mrwmrw00000000000000""" Python introspection helpers. """ from types import CodeType as code, FunctionType as function def copycode(template, changes): names = [ "argcount", "nlocals", "stacksize", "flags", "code", "consts", "names", "varnames", "filename", "name", "firstlineno", "lnotab", "freevars", "cellvars" ] if hasattr(code, "co_kwonlyargcount"): names.insert(1, "kwonlyargcount") if hasattr(code, "co_posonlyargcount"): # PEP 570 added "positional only arguments" names.insert(1, "posonlyargcount") values = [ changes.get(name, getattr(template, "co_" + name)) for name in names ] return code(*values) def copyfunction(template, funcchanges, codechanges): names = [ "globals", "name", "defaults", "closure", ] values = [ funcchanges.get(name, getattr(template, "__" + name + "__")) for name in names ] return function(copycode(template.__code__, codechanges), *values) def preserveName(f): """ Preserve the name of the given function on the decorated function. """ def decorator(decorated): return copyfunction(decorated, dict(name=f.__name__), dict(name=f.__name__)) return decorator Automat-0.8.0/automat/_visualize.py0000644000175000017500000001427713106701777017007 0ustar mrwmrw00000000000000from __future__ import print_function import argparse import sys import graphviz from ._discover import findMachines def _gvquote(s): return '"{}"'.format(s.replace('"', r'\"')) def _gvhtml(s): return '<{}>'.format(s) def elementMaker(name, *children, **attrs): """ Construct a string from the HTML element description. """ formattedAttrs = ' '.join('{}={}'.format(key, _gvquote(str(value))) for key, value in sorted(attrs.items())) formattedChildren = ''.join(children) return u'<{name} {attrs}>{children}'.format( name=name, attrs=formattedAttrs, children=formattedChildren) def tableMaker(inputLabel, outputLabels, port, _E=elementMaker): """ Construct an HTML table to label a state transition. """ colspan = {} if outputLabels: colspan['colspan'] = str(len(outputLabels)) inputLabelCell = _E("td", _E("font", inputLabel, face="menlo-italic"), color="purple", port=port, **colspan) pointSize = {"point-size": "9"} outputLabelCells = [_E("td", _E("font", outputLabel, **pointSize), color="pink") for outputLabel in outputLabels] rows = [_E("tr", inputLabelCell)] if outputLabels: rows.append(_E("tr", *outputLabelCells)) return _E("table", *rows) def makeDigraph(automaton, inputAsString=repr, outputAsString=repr, stateAsString=repr): """ Produce a L{graphviz.Digraph} object from an automaton. """ digraph = graphviz.Digraph(graph_attr={'pack': 'true', 'dpi': '100'}, node_attr={'fontname': 'Menlo'}, edge_attr={'fontname': 'Menlo'}) for state in automaton.states(): if state is automaton.initialState: stateShape = "bold" fontName = "Menlo-Bold" else: stateShape = "" fontName = "Menlo" digraph.node(stateAsString(state), fontame=fontName, shape="ellipse", style=stateShape, color="blue") for n, eachTransition in enumerate(automaton.allTransitions()): inState, inputSymbol, outState, outputSymbols = eachTransition thisTransition = "t{}".format(n) inputLabel = inputAsString(inputSymbol) port = "tableport" table = tableMaker(inputLabel, [outputAsString(outputSymbol) for outputSymbol in outputSymbols], port=port) digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none") digraph.edge(stateAsString(inState), '{}:{}:w'.format(thisTransition, port), arrowhead="none") digraph.edge('{}:{}:e'.format(thisTransition, port), stateAsString(outState)) return digraph def tool(_progname=sys.argv[0], _argv=sys.argv[1:], _syspath=sys.path, _findMachines=findMachines, _print=print): """ Entry point for command line utility. """ DESCRIPTION = """ Visualize automat.MethodicalMachines as graphviz graphs. """ EPILOG = """ You must have the graphviz tool suite installed. Please visit http://www.graphviz.org for more information. """ if _syspath[0]: _syspath.insert(0, '') argumentParser = argparse.ArgumentParser( prog=_progname, description=DESCRIPTION, epilog=EPILOG) argumentParser.add_argument('fqpn', help="A Fully Qualified Path name" " representing where to find machines.") argumentParser.add_argument('--quiet', '-q', help="suppress output", default=False, action="store_true") argumentParser.add_argument('--dot-directory', '-d', help="Where to write out .dot files.", default=".automat_visualize") argumentParser.add_argument('--image-directory', '-i', help="Where to write out image files.", default=".automat_visualize") argumentParser.add_argument('--image-type', '-t', help="The image format.", choices=graphviz.FORMATS, default='png') argumentParser.add_argument('--view', '-v', help="View rendered graphs with" " default image viewer", default=False, action="store_true") args = argumentParser.parse_args(_argv) explicitlySaveDot = (args.dot_directory and (not args.image_directory or args.image_directory != args.dot_directory)) if args.quiet: def _print(*args): pass for fqpn, machine in _findMachines(args.fqpn): _print(fqpn, '...discovered') digraph = machine.asDigraph() if explicitlySaveDot: digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory) _print(fqpn, "...wrote dot into", args.dot_directory) if args.image_directory: deleteDot = not args.dot_directory or explicitlySaveDot digraph.format = args.image_type digraph.render(filename="{}.dot".format(fqpn), directory=args.image_directory, view=args.view, cleanup=deleteDot) if deleteDot: msg = "...wrote image into" else: msg = "...wrote image and dot into" _print(fqpn, msg, args.image_directory) Automat-0.8.0/automat/_test/0000755000175000017500000000000013552760067015370 5ustar mrwmrw00000000000000Automat-0.8.0/automat/_test/__init__.py0000644000175000017500000000000012717000352017451 0ustar mrwmrw00000000000000Automat-0.8.0/automat/_test/test_visualize.py0000644000175000017500000003266013072723343021014 0ustar mrwmrw00000000000000from __future__ import print_function import functools import os import subprocess from unittest import TestCase, skipIf import attr from .._methodical import MethodicalMachine from .test_discover import isTwistedInstalled def isGraphvizModuleInstalled(): """ Is the graphviz Python module installed? """ try: __import__("graphviz") except ImportError: return False else: return True def isGraphvizInstalled(): """ Are the graphviz tools installed? """ r, w = os.pipe() os.close(w) try: return not subprocess.call("dot", stdin=r, shell=True) finally: os.close(r) def sampleMachine(): """ Create a sample L{MethodicalMachine} with some sample states. """ mm = MethodicalMachine() class SampleObject(object): @mm.state(initial=True) def begin(self): "initial state" @mm.state() def end(self): "end state" @mm.input() def go(self): "sample input" @mm.output() def out(self): "sample output" begin.upon(go, end, [out]) so = SampleObject() so.go() return mm @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class ElementMakerTests(TestCase): """ L{elementMaker} generates HTML representing the specified element. """ def setUp(self): from .._visualize import elementMaker self.elementMaker = elementMaker def test_sortsAttrs(self): """ L{elementMaker} orders HTML attributes lexicographically. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div", b='2', a='1', c='3')) def test_quotesAttrs(self): """ L{elementMaker} quotes HTML attributes according to DOT's quoting rule. See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div", b='a " quote', a=1, c="a string")) def test_noAttrs(self): """ L{elementMaker} should render an element with no attributes. """ expected = r'
' self.assertEqual(expected, self.elementMaker("div")) @attr.s class HTMLElement(object): """Holds an HTML element, as created by elementMaker.""" name = attr.ib() children = attr.ib() attributes = attr.ib() def findElements(element, predicate): """ Recursively collect all elements in an L{HTMLElement} tree that match the optional predicate. """ if predicate(element): return [element] elif isLeaf(element): return [] return [result for child in element.children for result in findElements(child, predicate)] def isLeaf(element): """ This HTML element is actually leaf node. """ return not isinstance(element, HTMLElement) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class TableMakerTests(TestCase): """ Tests that ensure L{tableMaker} generates HTML tables usable as labels in DOT graphs. For more information, read the "HTML-Like Labels" section of U{http://www.graphviz.org/doc/info/shapes.html}. """ def fakeElementMaker(self, name, *children, **attributes): return HTMLElement(name=name, children=children, attributes=attributes) def setUp(self): from .._visualize import tableMaker self.inputLabel = "input label" self.port = "the port" self.tableMaker = functools.partial(tableMaker, _E=self.fakeElementMaker) def test_inputLabelRow(self): """ The table returned by L{tableMaker} always contains the input symbol label in its first row, and that row contains one cell with a port attribute set to the provided port. """ def hasPort(element): return (not isLeaf(element) and element.attributes.get("port") == self.port) for outputLabels in ([], ["an output label"]): table = self.tableMaker(self.inputLabel, outputLabels, port=self.port) self.assertGreater(len(table.children), 0) inputLabelRow = table.children[0] portCandidates = findElements(table, hasPort) self.assertEqual(len(portCandidates), 1) self.assertEqual(portCandidates[0].name, "td") self.assertEqual(findElements(inputLabelRow, isLeaf), [self.inputLabel]) def test_noOutputLabels(self): """ L{tableMaker} does not add a colspan attribute to the input label's cell or a second row if there no output labels. """ table = self.tableMaker("input label", (), port=self.port) self.assertEqual(len(table.children), 1) (inputLabelRow,) = table.children self.assertNotIn("colspan", inputLabelRow.attributes) def test_withOutputLabels(self): """ L{tableMaker} adds a colspan attribute to the input label's cell equal to the number of output labels and a second row that contains the output labels. """ table = self.tableMaker(self.inputLabel, ("output label 1", "output label 2"), port=self.port) self.assertEqual(len(table.children), 2) inputRow, outputRow = table.children def hasCorrectColspan(element): return (not isLeaf(element) and element.name == "td" and element.attributes.get('colspan') == "2") self.assertEqual(len(findElements(inputRow, hasCorrectColspan)), 1) self.assertEqual(findElements(outputRow, isLeaf), ["output label 1", "output label 2"]) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.") class IntegrationTests(TestCase): """ Tests which make sure Graphviz can understand the output produced by Automat. """ def test_validGraphviz(self): """ L{graphviz} emits valid graphviz data. """ p = subprocess.Popen("dot", stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, err = p.communicate("".join(sampleMachine().asDigraph()) .encode("utf-8")) self.assertEqual(p.returncode, 0) @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") class SpotChecks(TestCase): """ Tests to make sure that the output contains salient features of the machine being generated. """ def test_containsMachineFeatures(self): """ The output of L{graphviz} should contain the names of the states, inputs, outputs in the state machine. """ gvout = "".join(sampleMachine().asDigraph()) self.assertIn("begin", gvout) self.assertIn("end", gvout) self.assertIn("go", gvout) self.assertIn("out", gvout) class RecordsDigraphActions(object): """ Records calls made to L{FakeDigraph}. """ def __init__(self): self.reset() def reset(self): self.renderCalls = [] self.saveCalls = [] class FakeDigraph(object): """ A fake L{graphviz.Digraph}. Instantiate it with a L{RecordsDigraphActions}. """ def __init__(self, recorder): self._recorder = recorder def render(self, **kwargs): self._recorder.renderCalls.append(kwargs) def save(self, **kwargs): self._recorder.saveCalls.append(kwargs) class FakeMethodicalMachine(object): """ A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph} """ def __init__(self, digraph): self._digraph = digraph def asDigraph(self): return self._digraph @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.") @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.") @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class VisualizeToolTests(TestCase): def setUp(self): self.digraphRecorder = RecordsDigraphActions() self.fakeDigraph = FakeDigraph(self.digraphRecorder) self.fakeProgname = 'tool-test' self.fakeSysPath = ['ignored'] self.collectedOutput = [] self.fakeFQPN = 'fake.fqpn' def collectPrints(self, *args): self.collectedOutput.append(' '.join(args)) def fakeFindMachines(self, fqpn): yield fqpn, FakeMethodicalMachine(self.fakeDigraph) def tool(self, progname=None, argv=None, syspath=None, findMachines=None, print=None): from .._visualize import tool return tool( _progname=progname or self.fakeProgname, _argv=argv or [self.fakeFQPN], _syspath=syspath or self.fakeSysPath, _findMachines=findMachines or self.fakeFindMachines, _print=print or self.collectPrints) def test_checksCurrentDirectory(self): """ L{tool} adds '' to sys.path to ensure L{automat._discover.findMachines} searches the current directory. """ self.tool(argv=[self.fakeFQPN]) self.assertEqual(self.fakeSysPath[0], '') def test_quietHidesOutput(self): """ Passing -q/--quiet hides all output. """ self.tool(argv=[self.fakeFQPN, '--quiet']) self.assertFalse(self.collectedOutput) self.tool(argv=[self.fakeFQPN, '-q']) self.assertFalse(self.collectedOutput) def test_onlySaveDot(self): """ Passing an empty string for --image-directory/-i disables rendering images. """ for arg in ('--image-directory', '-i'): self.digraphRecorder.reset() self.collectedOutput = [] self.tool(argv=[self.fakeFQPN, arg, '']) self.assertFalse(any("image" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.saveCalls), 1) (call,) = self.digraphRecorder.saveCalls self.assertEqual("{}.dot".format(self.fakeFQPN), call['filename']) self.assertFalse(self.digraphRecorder.renderCalls) def test_saveOnlyImage(self): """ Passing an empty string for --dot-directory/-d disables saving dot files. """ for arg in ('--dot-directory', '-d'): self.digraphRecorder.reset() self.collectedOutput = [] self.tool(argv=[self.fakeFQPN, arg, '']) self.assertFalse(any("dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (call,) = self.digraphRecorder.renderCalls self.assertEqual("{}.dot".format(self.fakeFQPN), call['filename']) self.assertTrue(call['cleanup']) self.assertFalse(self.digraphRecorder.saveCalls) def test_saveDotAndImagesInDifferentDirectories(self): """ Passing different directories to --image-directory and --dot-directory writes images and dot files to those directories. """ imageDirectory = 'image' dotDirectory = 'dot' self.tool(argv=[self.fakeFQPN, '--image-directory', imageDirectory, '--dot-directory', dotDirectory]) self.assertTrue(any("image" in line for line in self.collectedOutput)) self.assertTrue(any("dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (renderCall,) = self.digraphRecorder.renderCalls self.assertEqual(renderCall["directory"], imageDirectory) self.assertTrue(renderCall['cleanup']) self.assertEqual(len(self.digraphRecorder.saveCalls), 1) (saveCall,) = self.digraphRecorder.saveCalls self.assertEqual(saveCall["directory"], dotDirectory) def test_saveDotAndImagesInSameDirectory(self): """ Passing the same directory to --image-directory and --dot-directory writes images and dot files to that one directory. """ directory = 'imagesAndDot' self.tool(argv=[self.fakeFQPN, '--image-directory', directory, '--dot-directory', directory]) self.assertTrue(any("image and dot" in line for line in self.collectedOutput)) self.assertEqual(len(self.digraphRecorder.renderCalls), 1) (renderCall,) = self.digraphRecorder.renderCalls self.assertEqual(renderCall["directory"], directory) self.assertFalse(renderCall['cleanup']) self.assertFalse(len(self.digraphRecorder.saveCalls)) Automat-0.8.0/automat/_test/test_core.py0000644000175000017500000000542113552700725017726 0ustar mrwmrw00000000000000 from .._core import Automaton, NoTransition from unittest import TestCase class CoreTests(TestCase): """ Tests for Automat's (currently private, implementation detail) core. """ def test_NoTransition(self): """ A L{NoTransition} exception describes the state and input symbol that caused it. """ # NoTransition requires two arguments with self.assertRaises(TypeError): NoTransition() state = "current-state" symbol = "transitionless-symbol" noTransitionException = NoTransition(state=state, symbol=symbol) self.assertIs(noTransitionException.symbol, symbol) self.assertIn(state, str(noTransitionException)) self.assertIn(symbol, str(noTransitionException)) def test_noOutputForInput(self): """ L{Automaton.outputForInput} raises L{NoTransition} if no transition for that input is defined. """ a = Automaton() self.assertRaises(NoTransition, a.outputForInput, "no-state", "no-symbol") def test_oneTransition(self): """ L{Automaton.addTransition} adds its input symbol to L{Automaton.inputAlphabet}, all its outputs to L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to start returning the new state and output symbols. """ a = Automaton() a.addTransition("beginning", "begin", "ending", ["end"]) self.assertEqual(a.inputAlphabet(), {"begin"}) self.assertEqual(a.outputAlphabet(), {"end"}) self.assertEqual(a.outputForInput("beginning", "begin"), ("ending", ["end"])) self.assertEqual(a.states(), {"beginning", "ending"}) def test_oneTransition_nonIterableOutputs(self): """ L{Automaton.addTransition} raises a TypeError when given outputs that aren't iterable and doesn't add any transitions. """ a = Automaton() nonIterableOutputs = 1 self.assertRaises( TypeError, a.addTransition, "fromState", "viaSymbol", "toState", nonIterableOutputs) self.assertFalse(a.inputAlphabet()) self.assertFalse(a.outputAlphabet()) self.assertFalse(a.states()) self.assertFalse(a.allTransitions()) def test_initialState(self): """ L{Automaton.initialState} is a descriptor that sets the initial state if it's not yet set, and raises L{ValueError} if it is. """ a = Automaton() a.initialState = "a state" self.assertEqual(a.initialState, "a state") with self.assertRaises(ValueError): a.initialState = "another state" # FIXME: addTransition for transition that's been added before Automat-0.8.0/automat/_test/test_discover.py0000644000175000017500000005252613014133630020610 0ustar mrwmrw00000000000000import operator import os import shutil import sys import textwrap import tempfile from unittest import skipIf, TestCase import six def isTwistedInstalled(): try: __import__('twisted') except ImportError: return False else: return True class _WritesPythonModules(TestCase): """ A helper that enables generating Python module test fixtures. """ def setUp(self): super(_WritesPythonModules, self).setUp() from twisted.python.modules import getModule, PythonPath from twisted.python.filepath import FilePath self.getModule = getModule self.PythonPath = PythonPath self.FilePath = FilePath self.originalSysModules = set(sys.modules.keys()) self.savedSysPath = sys.path[:] self.pathDir = tempfile.mkdtemp() self.makeImportable(self.pathDir) def tearDown(self): super(_WritesPythonModules, self).tearDown() sys.path[:] = self.savedSysPath modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules for module in modulesToDelete: del sys.modules[module] shutil.rmtree(self.pathDir) def makeImportable(self, path): sys.path.append(path) def writeSourceInto(self, source, path, moduleName): directory = self.FilePath(path) module = directory.child(moduleName) # FilePath always opens a file in binary mode - but that will # break on Python 3 with open(module.path, 'w') as f: f.write(textwrap.dedent(source)) return self.PythonPath([directory.path]) def makeModule(self, source, path, moduleName): pythonModuleName, _ = os.path.splitext(moduleName) return self.writeSourceInto(source, path, moduleName)[pythonModuleName] def attributesAsDict(self, hasIterAttributes): return {attr.name: attr for attr in hasIterAttributes.iterAttributes()} def loadModuleAsDict(self, module): module.load() return self.attributesAsDict(module) def makeModuleAsDict(self, source, path, name): return self.loadModuleAsDict(self.makeModule(source, path, name)) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class OriginalLocationTests(_WritesPythonModules): """ Tests that L{isOriginalLocation} detects when a L{PythonAttribute}'s FQPN refers to an object inside the module where it was defined. For example: A L{twisted.python.modules.PythonAttribute} with a name of 'foo.bar' that refers to a 'bar' object defined in module 'baz' does *not* refer to bar's original location, while a L{PythonAttribute} with a name of 'baz.bar' does. """ def setUp(self): super(OriginalLocationTests, self).setUp() from .._discover import isOriginalLocation self.isOriginalLocation = isOriginalLocation def test_failsWithNoModule(self): """ L{isOriginalLocation} returns False when the attribute refers to an object whose source module cannot be determined. """ source = """\ class Fake(object): pass hasEmptyModule = Fake() hasEmptyModule.__module__ = None """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'empty_module_attr.py') self.assertFalse(self.isOriginalLocation( moduleDict['empty_module_attr.hasEmptyModule'])) def test_failsWithDifferentModule(self): """ L{isOriginalLocation} returns False when the attribute refers to an object outside of the module where that object was defined. """ originalSource = """\ class ImportThisClass(object): pass importThisObject = ImportThisClass() importThisNestingObject = ImportThisClass() importThisNestingObject.nestedObject = ImportThisClass() """ importingSource = """\ from original import (ImportThisClass, importThisObject, importThisNestingObject) """ self.makeModule(originalSource, self.pathDir, 'original.py') importingDict = self.makeModuleAsDict(importingSource, self.pathDir, 'importing.py') self.assertFalse( self.isOriginalLocation( importingDict['importing.ImportThisClass'])) self.assertFalse( self.isOriginalLocation( importingDict['importing.importThisObject'])) nestingObject = importingDict['importing.importThisNestingObject'] nestingObjectDict = self.attributesAsDict(nestingObject) nestedObject = nestingObjectDict[ 'importing.importThisNestingObject.nestedObject'] self.assertFalse(self.isOriginalLocation(nestedObject)) def test_succeedsWithSameModule(self): """ L{isOriginalLocation} returns True when the attribute refers to an object inside the module where that object was defined. """ mSource = textwrap.dedent(""" class ThisClassWasDefinedHere(object): pass anObject = ThisClassWasDefinedHere() aNestingObject = ThisClassWasDefinedHere() aNestingObject.nestedObject = ThisClassWasDefinedHere() """) mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py') self.assertTrue(self.isOriginalLocation( mDict['m.ThisClassWasDefinedHere'])) self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject'])) nestingObject = mDict['m.aNestingObject'] nestingObjectDict = self.attributesAsDict(nestingObject) nestedObject = nestingObjectDict['m.aNestingObject.nestedObject'] self.assertTrue(self.isOriginalLocation(nestedObject)) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class FindMachinesViaWrapperTests(_WritesPythonModules): """ L{findMachinesViaWrapper} recursively yields FQPN, L{MethodicalMachine} pairs in and under a given L{twisted.python.modules.PythonModule} or L{twisted.python.modules.PythonAttribute}. """ TEST_MODULE_SOURCE = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() class NestedClass(object): _nestedClassMachine = MethodicalMachine() ignoredAttribute = "I am ignored." def ignoredMethod(self): "I am also ignored." rootLevelMachine = MethodicalMachine() ignoredPythonObject = PythonClass() anotherIgnoredPythonObject = "I am ignored." """ def setUp(self): super(FindMachinesViaWrapperTests, self).setUp() from .._discover import findMachinesViaWrapper self.findMachinesViaWrapper = findMachinesViaWrapper def test_yieldsMachine(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers directly to a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine rootMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py') rootMachine = moduleDict['root.rootMachine'] self.assertIn(('root.rootMachine', rootMachine.load()), list(self.findMachinesViaWrapper(rootMachine))) def test_yieldsMachineInClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a class that contains a L{MethodicalMachine} as a class variable, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py') PythonClass = moduleDict['clsmod.PythonClass'] self.assertIn(('clsmod.PythonClass._classMachine', PythonClass.load()._classMachine), list(self.findMachinesViaWrapper(PythonClass))) def test_yieldsMachineInNestedClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a nested class that contains a L{MethodicalMachine} as a class variable, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): class NestedClass(object): _classMachine = MethodicalMachine() """ moduleDict = self.makeModuleAsDict(source, self.pathDir, 'nestedcls.py') PythonClass = moduleDict['nestedcls.PythonClass'] self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine', PythonClass.load().NestedClass._classMachine), list(self.findMachinesViaWrapper(PythonClass))) def test_yieldsMachineInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to a module that contains a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine rootMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'root.py') rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load() self.assertIn(('root.rootMachine', rootMachine), list(self.findMachinesViaWrapper(module))) def test_yieldsMachineInClassInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to the original module of a class containing a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'clsmod.py') PythonClass = self.loadModuleAsDict( module)['clsmod.PythonClass'].load() self.assertIn(('clsmod.PythonClass._classMachine', PythonClass._classMachine), list(self.findMachinesViaWrapper(module))) def test_yieldsMachineInNestedClassInModule(self): """ When given a L{twisted.python.modules.PythonModule} that refers to the original module of a nested class containing a L{MethodicalMachine}, L{findMachinesViaWrapper} yields that machine and its FQPN. """ source = """\ from automat import MethodicalMachine class PythonClass(object): class NestedClass(object): _classMachine = MethodicalMachine() """ module = self.makeModule(source, self.pathDir, 'nestedcls.py') PythonClass = self.loadModuleAsDict( module)['nestedcls.PythonClass'].load() self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine', PythonClass.NestedClass._classMachine), list(self.findMachinesViaWrapper(module))) def test_ignoresImportedClass(self): """ When given a L{twisted.python.modules.PythonAttribute} that refers to a class imported from another module, any L{MethodicalMachine}s on that class are ignored. This behavior ensures that a machine is only discovered on a class when visiting the module where that class was defined. """ originalSource = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() """ importingSource = """ from original import PythonClass """ self.makeModule(originalSource, self.pathDir, 'original.py') importingModule = self.makeModule(importingSource, self.pathDir, 'importing.py') self.assertFalse(list(self.findMachinesViaWrapper(importingModule))) def test_descendsIntoPackages(self): """ L{findMachinesViaWrapper} descends into packages to discover machines. """ pythonPath = self.PythonPath([self.pathDir]) package = self.FilePath(self.pathDir).child("test_package") package.makedirs() package.child('__init__.py').touch() source = """ from automat import MethodicalMachine class PythonClass(object): _classMachine = MethodicalMachine() rootMachine = MethodicalMachine() """ self.makeModule(source, package.path, 'module.py') test_package = pythonPath['test_package'] machines = sorted(self.findMachinesViaWrapper(test_package), key=operator.itemgetter(0)) moduleDict = self.loadModuleAsDict(test_package['module']) rootMachine = moduleDict['test_package.module.rootMachine'].load() PythonClass = moduleDict['test_package.module.PythonClass'].load() expectedMachines = sorted( [('test_package.module.rootMachine', rootMachine), ('test_package.module.PythonClass._classMachine', PythonClass._classMachine)], key=operator.itemgetter(0)) self.assertEqual(expectedMachines, machines) def test_infiniteLoop(self): """ L{findMachinesViaWrapper} ignores infinite loops. Note this test can't fail - it can only run forever! """ source = """ class InfiniteLoop(object): pass InfiniteLoop.loop = InfiniteLoop """ module = self.makeModule(source, self.pathDir, 'loop.py') self.assertFalse(list(self.findMachinesViaWrapper(module))) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class WrapFQPNTests(TestCase): """ Tests that ensure L{wrapFQPN} loads the L{twisted.python.modules.PythonModule} or L{twisted.python.modules.PythonAttribute} for a given FQPN. """ def setUp(self): from twisted.python.modules import PythonModule, PythonAttribute from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject self.PythonModule = PythonModule self.PythonAttribute = PythonAttribute self.wrapFQPN = wrapFQPN self.InvalidFQPN = InvalidFQPN self.NoModule = NoModule self.NoObject = NoObject def assertModuleWrapperRefersTo(self, moduleWrapper, module): """ Assert that a L{twisted.python.modules.PythonModule} refers to a particular Python module. """ self.assertIsInstance(moduleWrapper, self.PythonModule) self.assertEqual(moduleWrapper.name, module.__name__) self.assertIs(moduleWrapper.load(), module) def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj): """ Assert that a L{twisted.python.modules.PythonAttribute} refers to a particular Python object. """ self.assertIsInstance(attributeWrapper, self.PythonAttribute) self.assertEqual(attributeWrapper.name, fqpn) self.assertIs(attributeWrapper.load(), obj) def test_failsWithEmptyFQPN(self): """ L{wrapFQPN} raises L{InvalidFQPN} when given an empty string. """ with self.assertRaises(self.InvalidFQPN): self.wrapFQPN('') def test_failsWithBadDotting(self): """" L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted FQPN. (e.g., x..y). """ for bad in ('.fails', 'fails.', 'this..fails'): with self.assertRaises(self.InvalidFQPN): self.wrapFQPN(bad) def test_singleModule(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the single module a dotless FQPN describes. """ import os moduleWrapper = self.wrapFQPN('os') self.assertIsInstance(moduleWrapper, self.PythonModule) self.assertIs(moduleWrapper.load(), os) def test_failsWithMissingSingleModuleOrPackage(self): """ L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does not refer to a module or package. """ with self.assertRaises(self.NoModule): self.wrapFQPN("this is not an acceptable name!") def test_singlePackage(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the single package a dotless FQPN describes. """ import xml self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml) def test_multiplePackages(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the deepest package described by dotted FQPN. """ import xml.etree self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree) def test_multiplePackagesFinalModule(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonModule} referring to the deepest module described by dotted FQPN. """ import xml.etree.ElementTree self.assertModuleWrapperRefersTo( self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree) def test_singleModuleObject(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute} referring to the deepest object an FQPN names, traversing one module. """ import os self.assertAttributeWrapperRefersTo( self.wrapFQPN('os.path'), 'os.path', os.path) def test_multiplePackagesObject(self): """ L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute} referring to the deepest object described by an FQPN, descending through several packages. """ import xml.etree.ElementTree import automat for fqpn, obj in [('xml.etree.ElementTree.fromstring', xml.etree.ElementTree.fromstring), ('automat.MethodicalMachine.__doc__', automat.MethodicalMachine.__doc__)]: self.assertAttributeWrapperRefersTo( self.wrapFQPN(fqpn), fqpn, obj) def test_failsWithMultiplePackagesMissingModuleOrPackage(self): """ L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a missing attribute, module, or package. """ for bad in ('xml.etree.nope!', 'xml.etree.nope!.but.the.rest.is.believable'): with self.assertRaises(self.NoObject): self.wrapFQPN(bad) @skipIf(not isTwistedInstalled(), "Twisted is not installed.") class FindMachinesIntegrationTests(_WritesPythonModules): """ Integration tests to check that L{findMachines} yields all machines discoverable at or below an FQPN. """ SOURCE = """ from automat import MethodicalMachine class PythonClass(object): _machine = MethodicalMachine() ignored = "i am ignored" rootLevel = MethodicalMachine() ignored = "i am ignored" """ def setUp(self): super(FindMachinesIntegrationTests, self).setUp() from .._discover import findMachines self.findMachines = findMachines packageDir = self.FilePath(self.pathDir).child("test_package") packageDir.makedirs() self.pythonPath = self.PythonPath([self.pathDir]) self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py') subPackageDir = packageDir.child('subpackage') subPackageDir.makedirs() subPackageDir.child('__init__.py').touch() self.makeModule(self.SOURCE, subPackageDir.path, 'module.py') self.packageDict = self.loadModuleAsDict( self.pythonPath['test_package']) self.moduleDict = self.loadModuleAsDict( self.pythonPath['test_package']['subpackage']['module']) def test_discoverAll(self): """ Given a top-level package FQPN, L{findMachines} discovers all L{MethodicalMachine} instances in and below it. """ machines = sorted(self.findMachines('test_package'), key=operator.itemgetter(0)) tpRootLevel = self.packageDict['test_package.rootLevel'].load() tpPythonClass = self.packageDict['test_package.PythonClass'].load() mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel'] mRootLevel = mRLAttr.load() mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass'] mPythonClass = mPCAttr.load() expectedMachines = sorted( [('test_package.rootLevel', tpRootLevel), ('test_package.PythonClass._machine', tpPythonClass._machine), ('test_package.subpackage.module.rootLevel', mRootLevel), ('test_package.subpackage.module.PythonClass._machine', mPythonClass._machine)], key=operator.itemgetter(0)) self.assertEqual(expectedMachines, machines) Automat-0.8.0/automat/_test/test_trace.py0000644000175000017500000000631713106675015020077 0ustar mrwmrw00000000000000from 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"), ]) Automat-0.8.0/automat/_test/test_methodical.py0000644000175000017500000004465013552700725021116 0ustar mrwmrw00000000000000 """ Tests for the public interface of Automat. """ from functools import reduce from unittest import TestCase from automat._methodical import ArgSpec, _getArgNames, _getArgSpec, _filterArgs from .. import MethodicalMachine, NoTransition from .. import _methodical class MethodicalTests(TestCase): """ Tests for L{MethodicalMachine}. """ def test_oneTransition(self): """ L{MethodicalMachine} provides a way for you to declare a state machine with inputs, outputs, and states as methods. When you have declared an input, an output, and a state, calling the input method in that state will produce the specified output. """ class Machination(object): machine = MethodicalMachine() @machine.input() def anInput(self): "an input" @machine.output() def anOutput(self): "an output" return "an-output-value" @machine.output() def anotherOutput(self): "another output" return "another-output-value" @machine.state(initial=True) def anState(self): "a state" @machine.state() def anotherState(self): "another state" anState.upon(anInput, enter=anotherState, outputs=[anOutput]) anotherState.upon(anInput, enter=anotherState, outputs=[anotherOutput]) m = Machination() self.assertEqual(m.anInput(), ["an-output-value"]) self.assertEqual(m.anInput(), ["another-output-value"]) def test_machineItselfIsPrivate(self): """ L{MethodicalMachine} is an implementation detail. If you attempt to access it on an instance of your class, you will get an exception. However, since tools may need to access it for the purposes of, for example, visualization, you may access it on the class itself. """ expectedMachine = MethodicalMachine() class Machination(object): machine = expectedMachine machination = Machination() with self.assertRaises(AttributeError) as cm: machination.machine self.assertIn("MethodicalMachine is an implementation detail", str(cm.exception)) self.assertIs(Machination.machine, expectedMachine) def test_outputsArePrivate(self): """ One of the benefits of using a state machine is that your output method implementations don't need to take invalid state transitions into account - the methods simply won't be called. This property would be broken if client code called output methods directly, so output methods are not directly visible under their names. """ class Machination(object): machine = MethodicalMachine() counter = 0 @machine.input() def anInput(self): "an input" @machine.output() def anOutput(self): self.counter += 1 @machine.state(initial=True) def state(self): "a machine state" state.upon(anInput, enter=state, outputs=[anOutput]) mach1 = Machination() mach1.anInput() self.assertEqual(mach1.counter, 1) mach2 = Machination() with self.assertRaises(AttributeError) as cm: mach2.anOutput self.assertEqual(mach2.counter, 0) self.assertIn( "Machination.anOutput is a state-machine output method; to " "produce this output, call an input method instead.", str(cm.exception) ) def test_multipleMachines(self): """ Two machines may co-exist happily on the same instance; they don't interfere with each other. """ class MultiMach(object): a = MethodicalMachine() b = MethodicalMachine() @a.input() def inputA(self): "input A" @b.input() def inputB(self): "input B" @a.state(initial=True) def initialA(self): "initial A" @b.state(initial=True) def initialB(self): "initial B" @a.output() def outputA(self): return "A" @b.output() def outputB(self): return "B" initialA.upon(inputA, initialA, [outputA]) initialB.upon(inputB, initialB, [outputB]) mm = MultiMach() self.assertEqual(mm.inputA(), ["A"]) self.assertEqual(mm.inputB(), ["B"]) def test_collectOutputs(self): """ Outputs can be combined with the "collector" argument to "upon". """ import operator class Machine(object): m = MethodicalMachine() @m.input() def input(self): "an input" @m.output() def outputA(self): return "A" @m.output() def outputB(self): return "B" @m.state(initial=True) def state(self): "a state" state.upon(input, state, [outputA, outputB], collector=lambda x: reduce(operator.add, x)) m = Machine() self.assertEqual(m.input(), "AB") def test_methodName(self): """ Input methods preserve their declared names. """ class Mech(object): m = MethodicalMachine() @m.input() def declaredInputName(self): "an input" @m.state(initial=True) def aState(self): "state" m = Mech() with self.assertRaises(TypeError) as cm: m.declaredInputName("too", "many", "arguments") self.assertIn("declaredInputName", str(cm.exception)) def test_inputWithArguments(self): """ If an input takes an argument, it will pass that along to its output. """ class Mechanism(object): m = MethodicalMachine() @m.input() def input(self, x, y=1): "an input" @m.state(initial=True) def state(self): "a state" @m.output() def output(self, x, y=1): self._x = x return x + y state.upon(input, state, [output]) m = Mechanism() self.assertEqual(m.input(3), [4]) self.assertEqual(m._x, 3) def test_outputWithSubsetOfArguments(self): """ Inputs pass arguments that output will accept. """ class Mechanism(object): m = MethodicalMachine() @m.input() def input(self, x, y=1): "an input" @m.state(initial=True) def state(self): "a state" @m.output() def outputX(self, x): self._x = x return x @m.output() def outputY(self, y): self._y = y return y @m.output() def outputNoArgs(self): return None state.upon(input, state, [outputX, outputY, outputNoArgs]) m = Mechanism() # Pass x as positional argument. self.assertEqual(m.input(3), [3, 1, None]) self.assertEqual(m._x, 3) self.assertEqual(m._y, 1) # Pass x as key word argument. self.assertEqual(m.input(x=4), [4, 1, None]) self.assertEqual(m._x, 4) self.assertEqual(m._y, 1) # Pass y as positional argument. self.assertEqual(m.input(6, 3), [6, 3, None]) self.assertEqual(m._x, 6) self.assertEqual(m._y, 3) # Pass y as key word argument. self.assertEqual(m.input(5, y=2), [5, 2, None]) self.assertEqual(m._x, 5) self.assertEqual(m._y, 2) 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 one does not the call to C{upon} will raise a C{TypeError}. """ class Mechanism(object): m = MethodicalMachine() @m.input() def nameOfInput(self, a): "an input" @m.output() def outputThatMatches(self, a): "an output that matches" @m.output() def outputThatDoesntMatch(self, b): "an output that doesn't match" @m.state() def state(self): "a state" with self.assertRaises(TypeError) as cm: state.upon(nameOfInput, state, [outputThatMatches, outputThatDoesntMatch]) self.assertIn("nameOfInput", str(cm.exception)) self.assertIn("outputThatDoesntMatch", str(cm.exception)) def test_getArgNames(self): """ Type annotations should be included in the set of """ spec = ArgSpec( args=('a', 'b'), varargs=None, varkw=None, defaults=None, kwonlyargs=(), kwonlydefaults=None, annotations=(('a', int), ('b', str)), ) self.assertEqual( _getArgNames(spec), {'a', 'b', ('a', int), ('b', str)}, ) def test_filterArgs(self): """ filterArgs() should not filter the `args` parameter if outputSpec accepts `*args`. """ inputSpec = _getArgSpec(lambda *args, **kwargs: None) outputSpec = _getArgSpec(lambda *args, **kwargs: None) argsIn = () argsOut, _ = _filterArgs(argsIn, {}, inputSpec, outputSpec) self.assertIs(argsIn, argsOut) def test_multipleInitialStatesFailure(self): """ A L{MethodicalMachine} can only have one initial state. """ class WillFail(object): m = MethodicalMachine() @m.state(initial=True) def firstInitialState(self): "The first initial state -- this is OK." with self.assertRaises(ValueError): @m.state(initial=True) def secondInitialState(self): "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 current state raises an informative L{NoTransition}. """ class OnlyOnePath(object): m = MethodicalMachine() @m.state(initial=True) def start(self): "Start state." @m.state() def end(self): "End state." @m.input() def advance(self): "Move from start to end." @m.input() def deadEnd(self): "A transition from nowhere to nowhere." start.upon(advance, end, []) machine = OnlyOnePath() with self.assertRaises(NoTransition) as cm: machine.deadEnd() self.assertIn("deadEnd", str(cm.exception)) self.assertIn("start", str(cm.exception)) machine.advance() with self.assertRaises(NoTransition) as cm: machine.deadEnd() self.assertIn("deadEnd", str(cm.exception)) self.assertIn("end", str(cm.exception)) def test_saveState(self): """ L{MethodicalMachine.serializer} is a decorator that modifies its decoratee's signature to take a "state" object as its first argument, which is the "serialized" argument to the L{MethodicalMachine.state} decorator. """ class Mechanism(object): m = MethodicalMachine() def __init__(self): self.value = 1 @m.state(serialized="first-state", initial=True) def first(self): "First state." @m.state(serialized="second-state") def second(self): "Second state." @m.serializer() def save(self, state): return { 'machine-state': state, 'some-value': self.value, } self.assertEqual( Mechanism().save(), { "machine-state": "first-state", "some-value": 1, } ) def test_restoreState(self): """ L{MethodicalMachine.unserializer} decorates a function that becomes a machine-state unserializer; its return value is mapped to the C{serialized} parameter to C{state}, and the L{MethodicalMachine} associated with that instance's state is updated to that state. """ class Mechanism(object): m = MethodicalMachine() def __init__(self): self.value = 1 self.ranOutput = False @m.state(serialized="first-state", initial=True) def first(self): "First state." @m.state(serialized="second-state") def second(self): "Second state." @m.input() def input(self): "an input" @m.output() def output(self): self.value = 2 self.ranOutput = True return 1 @m.output() def output2(self): return 2 first.upon(input, second, [output], collector=lambda x: list(x)[0]) second.upon(input, second, [output2], collector=lambda x: list(x)[0]) @m.serializer() def save(self, state): return { 'machine-state': state, 'some-value': self.value, } @m.unserializer() def _restore(self, blob): self.value = blob['some-value'] return blob['machine-state'] @classmethod def fromBlob(cls, blob): self = cls() self._restore(blob) return self m1 = Mechanism() m1.input() blob = m1.save() m2 = Mechanism.fromBlob(blob) self.assertEqual(m2.ranOutput, False) self.assertEqual(m2.input(), 2) self.assertEqual( m2.save(), { 'machine-state': 'second-state', 'some-value': 2, } ) # FIXME: error for wrong types on any call to _oneTransition # FIXME: better public API for .upon; maybe a context manager? # FIXME: when transitions are defined, validate that we can always get to # terminal? do we care about this? # FIXME: implementation (and use-case/example) for passing args from in to out # FIXME: possibly these need some kind of support from core # FIXME: wildcard state (in all states, when input X, emit Y and go to Z) # FIXME: wildcard input (in state X, when any input, emit Y and go to Z) # FIXME: combined wildcards (in any state for any input, emit Y go to Z) Automat-0.8.0/automat/_methodical.py0000644000175000017500000003707413552700725017102 0ustar mrwmrw00000000000000# -*- test-case-name: automat._test.test_methodical -*- import collections from functools import wraps from itertools import count try: # Python 3 from inspect import getfullargspec as getArgsSpec except ImportError: # Python 2 from inspect import getargspec as getArgsSpec import attr import six from ._core import Transitioner, Automaton from ._introspection import preserveName ArgSpec = collections.namedtuple('ArgSpec', ['args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', 'kwonlydefaults', 'annotations']) def _getArgSpec(func): """ Normalize inspect.ArgSpec across python versions and convert mutable attributes to immutable types. :param Callable func: A function. :return: The function's ArgSpec. :rtype: ArgSpec """ spec = getArgsSpec(func) return ArgSpec( args=tuple(spec.args), varargs=spec.varargs, varkw=spec.varkw if six.PY3 else spec.keywords, defaults=spec.defaults if spec.defaults else (), kwonlyargs=tuple(spec.kwonlyargs) if six.PY3 else (), kwonlydefaults=( tuple(spec.kwonlydefaults.items()) if spec.kwonlydefaults else () ) if six.PY3 else (), annotations=tuple(spec.annotations.items()) if six.PY3 else (), ) def _getArgNames(spec): """ Get the name of all arguments defined in a function signature. The name of * and ** arguments is normalized to "*args" and "**kwargs". :param ArgSpec spec: A function to interrogate for a signature. :return: The set of all argument names in `func`s signature. :rtype: Set[str] """ return set( spec.args + spec.kwonlyargs + (('*args',) if spec.varargs else ()) + (('**kwargs',) if spec.varkw else ()) + spec.annotations ) def _keywords_only(f): """ Decorate a function so all its arguments must be passed by keyword. A useful utility for decorators that take arguments so that they don't accidentally get passed the thing they're decorating as their first argument. Only works for methods right now. """ @wraps(f) def g(self, **kw): return f(self, **kw) return g @attr.s(frozen=True) class MethodicalState(object): """ A state for a L{MethodicalMachine}. """ machine = attr.ib(repr=False) method = attr.ib() serialized = attr.ib(repr=False) def upon(self, input, enter, outputs, collector=list): """ Declare a state transition within the :class:`automat.MethodicalMachine` associated with this :class:`automat.MethodicalState`: upon the receipt of the `input`, enter the `state`, emitting each output in `outputs`. :param MethodicalInput input: The input triggering a state transition. :param MethodicalState enter: The resulting state. :param Iterable[MethodicalOutput] outputs: The outputs to be triggered as a result of the declared state transition. :param Callable collector: The function to be used when collecting output return values. :raises TypeError: if any of the `outputs` signatures do not match the `inputs` signature. :raises ValueError: if the state transition from `self` via `input` has already been defined. """ inputArgs = _getArgNames(input.argSpec) for output in outputs: outputArgs = _getArgNames(output.argSpec) if not outputArgs.issubset(inputArgs): raise TypeError( "method {input} signature {inputSignature} " "does not match output {output} " "signature {outputSignature}".format( input=input.method.__name__, output=output.method.__name__, inputSignature=getArgsSpec(input.method), outputSignature=getArgsSpec(output.method), )) self.machine._oneTransition(self, input, enter, outputs, collector) def _name(self): return self.method.__name__ def _transitionerFromInstance(oself, symbol, automaton): """ Get a L{Transitioner} """ transitioner = getattr(oself, symbol, None) if transitioner is None: transitioner = Transitioner( automaton, automaton.initialState, ) setattr(oself, symbol, transitioner) 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") def _filterArgs(args, kwargs, inputSpec, outputSpec): """ Filter out arguments that were passed to input that output won't accept. :param tuple args: The *args that input received. :param dict kwargs: The **kwargs that input received. :param ArgSpec inputSpec: The input's arg spec. :param ArgSpec outputSpec: The output's arg spec. :return: The args and kwargs that output will accept. :rtype: Tuple[tuple, dict] """ named_args = tuple(zip(inputSpec.args[1:], args)) if outputSpec.varargs: # Only return all args if the output accepts *args. return_args = args else: # Filter out arguments that don't appear # in the output's method signature. return_args = [v for n, v in named_args if n in outputSpec.args] # Get any of input's default arguments that were not passed. passed_arg_names = tuple(kwargs) for name, value in named_args: passed_arg_names += (name, value) defaults = zip(inputSpec.args[::-1], inputSpec.defaults[::-1]) full_kwargs = {n: v for n, v in defaults if n not in passed_arg_names} full_kwargs.update(kwargs) if outputSpec.varkw: # Only pass all kwargs if the output method accepts **kwargs. return_kwargs = full_kwargs else: # Filter out names that the output method does not accept. all_accepted_names = outputSpec.args[1:] + outputSpec.kwonlyargs return_kwargs = {n: v for n, v in full_kwargs.items() if n in all_accepted_names} return return_args, return_kwargs @attr.s(cmp=False, hash=False) class MethodicalInput(object): """ An input for a L{MethodicalMachine}. """ automaton = attr.ib(repr=False) method = attr.ib(validator=assertNoCode) symbol = attr.ib(repr=False) collectors = attr.ib(default=attr.Factory(dict), repr=False) argSpec = attr.ib(init=False, repr=False) @argSpec.default def _buildArgSpec(self): return _getArgSpec(self.method) def __get__(self, oself, type=None): """ Return a function that takes no arguments and returns values returned by output functions produced by the given L{MethodicalInput} in C{oself}'s current state. """ transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton) @preserveName(self.method) @wraps(self.method) def doInput(*args, **kwargs): self.method(oself, *args, **kwargs) previousState = transitioner._state (outputs, outTracer) = transitioner.transition(self) collector = self.collectors[previousState] values = [] for output in outputs: if outTracer: outTracer(output._name()) a, k = _filterArgs(args, kwargs, self.argSpec, output.argSpec) value = output(oself, *a, **k) values.append(value) return collector(values) return doInput def _name(self): return self.method.__name__ @attr.s(frozen=True) class MethodicalOutput(object): """ An output for a L{MethodicalMachine}. """ machine = attr.ib(repr=False) method = attr.ib() argSpec = attr.ib(init=False, repr=False) @argSpec.default def _buildArgSpec(self): return _getArgSpec(self.method) def __get__(self, oself, type=None): """ Outputs are private, so raise an exception when we attempt to get one. """ raise AttributeError( "{cls}.{method} is a state-machine output method; " "to produce this output, call an input method instead.".format( cls=type.__name__, method=self.method.__name__ ) ) def __call__(self, oself, *args, **kwargs): """ Call the underlying method. """ return self.method(oself, *args, **kwargs) def _name(self): return self.method.__name__ @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() def gensym(): """ Create a unique Python identifier. """ return "_symbol_" + str(next(counter)) class MethodicalMachine(object): """ A :class:`MethodicalMachine` is an interface to an `Automaton` that uses methods on a class. """ def __init__(self): self._automaton = Automaton() self._reducers = {} self._symbol = gensym() def __get__(self, oself, type=None): """ L{MethodicalMachine} is an implementation detail for setting up class-level state; applications should never need to access it on an instance. """ if oself is not None: raise AttributeError( "MethodicalMachine is an implementation detail.") return self @_keywords_only def state(self, initial=False, terminal=False, serialized=None): """ Declare a state, possibly an initial state or a terminal state. This is a decorator for methods, but it will modify the method so as not to be callable any more. :param bool initial: is this state the initial state? Only one state on this :class:`automat.MethodicalMachine` may be an initial state; more than one is an error. :param bool terminal: Is this state a terminal state? i.e. a state that the machine can end up in? (This is purely informational at this point.) :param Hashable serialized: a serializable value to be used to represent this state to external systems. This value should be hashable; :py:func:`unicode` is a good type to use. """ def decorator(stateMethod): state = MethodicalState(machine=self, method=stateMethod, serialized=serialized) if initial: self._automaton.initialState = state return state return decorator @_keywords_only def input(self): """ Declare an input. This is a decorator for methods. """ def decorator(inputMethod): return MethodicalInput(automaton=self._automaton, method=inputMethod, symbol=self._symbol) return decorator @_keywords_only def output(self): """ Declare an output. This is a decorator for methods. This method will be called when the state machine transitions to this state as specified in the decorated `output` method. """ def decorator(outputMethod): return MethodicalOutput(machine=self, method=outputMethod) return decorator def _oneTransition(self, startState, inputToken, endState, outputTokens, collector): """ See L{MethodicalState.upon}. """ # FIXME: tests for all of this (some of it is wrong) # if not isinstance(startState, MethodicalState): # raise NotImplementedError("start state {} isn't a state" # .format(startState)) # if not isinstance(inputToken, MethodicalInput): # raise NotImplementedError("start state {} isn't an input" # .format(inputToken)) # if not isinstance(endState, MethodicalState): # raise NotImplementedError("end state {} isn't a state" # .format(startState)) # for output in outputTokens: # if not isinstance(endState, MethodicalState): # raise NotImplementedError("output state {} isn't a state" # .format(endState)) self._automaton.addTransition(startState, inputToken, endState, tuple(outputTokens)) inputToken.collectors[startState] = collector @_keywords_only def serializer(self): """ """ def decorator(decoratee): @wraps(decoratee) def serialize(oself): transitioner = _transitionerFromInstance(oself, self._symbol, self._automaton) return decoratee(oself, transitioner._state.serialized) return serialize return decorator @_keywords_only def unserializer(self): """ """ def decorator(decoratee): @wraps(decoratee) def unserialize(oself, *args, **kwargs): state = decoratee(oself, *args, **kwargs) mapping = {} for eachState in self._automaton.states(): mapping[eachState.serialized] = eachState transitioner = _transitionerFromInstance( oself, self._symbol, self._automaton) transitioner._state = mapping[state] return None # it's on purpose return unserialize return decorator @property def _setTrace(self): return MethodicalTracer(self._automaton, self._symbol) def asDigraph(self): """ Generate a L{graphviz.Digraph} that represents this machine's states and transitions. @return: L{graphviz.Digraph} object; for more information, please see the documentation for U{graphviz} """ from ._visualize import makeDigraph return makeDigraph( self._automaton, stateAsString=lambda state: state.method.__name__, inputAsString=lambda input: input.method.__name__, outputAsString=lambda output: output.method.__name__, ) Automat-0.8.0/.travis.yml0000644000175000017500000000151613552733405014710 0ustar mrwmrw00000000000000language: python branches: only: - master 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.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 - python: 3.7 env: TOX_ENV=py37-extras - python: 3.7 env: TOX_ENV=py37-noextras - python: 3.8 env: TOX_ENV=py38-extras - python: 3.8 env: TOX_ENV=py38-noextras install: - sudo apt-get install graphviz - pip install tox coveralls script: - tox -e $TOX_ENV after_success: - tox -e coverage-report - coveralls Automat-0.8.0/setup.cfg0000644000175000017500000000010313552760067014413 0ustar mrwmrw00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 Automat-0.8.0/benchmark/0000755000175000017500000000000013552760067014532 5ustar mrwmrw00000000000000Automat-0.8.0/benchmark/test_transitions.py0000644000175000017500000000125113264025362020507 0ustar mrwmrw00000000000000# https://github.com/glyph/automat/issues/60 import automat class Simple(object): """ """ _m = automat.MethodicalMachine() @_m.input() def one(self, data): "some input data" @_m.state(initial=True) def waiting(self): "patiently" @_m.output() def boom(self, data): pass waiting.upon( one, enter=waiting, outputs=[boom], ) def simple_one(machine, data): machine.one(data) def test_simple_machine_transitions(benchmark): benchmark(simple_one, Simple(), 0) Automat-0.8.0/docs/0000755000175000017500000000000013552760067013530 5ustar mrwmrw00000000000000Automat-0.8.0/docs/visualize.rst0000644000175000017500000000704613552727030016275 0ustar mrwmrw00000000000000============== Visualizations ============== Installation ============ To create state machine graphs you must install `automat` with the graphing dependencies. .. code-block:: bash pip install automat[visualize] Example ======= Given the following project structure:: mystate/ ├── __init__.py └── machine.py And the following state machine defined in `machine.py` .. code-block:: python from automat import MethodicalMachine class MyMachine(object): _machine = MethodicalMachine() @_machine.state(initial=True) def state_a(self): """ State A """ @_machine.state() def state_b(self): """ State B """ @_machine.input() def change_state(self): """ Change state """ @_machine.output() def output_on_change_state(self): """ Change state """ return "Changing state" state_a.upon(change_state, enter=state_b, outputs=[output_on_change_state]) You can generate a state machine visualization by running: .. code-block:: bash $ automat-visualize mystate mystate.machine.MyMachine._machine ...discovered mystate.machine.MyMachine._machine ...wrote image and dot into .automat_visualize The `dot` file and `png` will be saved in the default output directory of `.automat_visualize/mystatemachine.MyMachine._machine.dot.png` .. image:: _static/mystate.machine.MyMachine._machine.dot.png :alt: my state machine `automat-visualize` help ======================== .. code-block:: bash $ automat-visualize -h usage: /home/tom/Envs/tmp-72fe664d2dc5cbf/bin/automat-visualize [-h] [--quiet] [--dot-directory DOT_DIRECTORY] [--image-directory IMAGE_DIRECTORY] [--image-type {gv,vml,dot_json,imap_np,pov,tiff,pic,canon,jpg,ismap,sgi,webp,gd,json0,ps2,cmapx_np,plain-ext,wbmp,xdot_json,ps,cgimage,ico,gtk,pct,gif,json,fig,xlib,xdot1.2,tif,tk,xdot1.4,svgz,gd2,jpe,psd,xdot,bmp,jpeg,x11,cmapx,jp2,imap,png,tga,pict,plain,eps,vmlz,cmap,exr,svg,pdf,vrml,dot}] [--view] fqpn Visualize automat.MethodicalMachines as graphviz graphs. positional arguments: fqpn A Fully Qualified Path name representing where to find machines. optional arguments: -h, --help show this help message and exit --quiet, -q suppress output --dot-directory DOT_DIRECTORY, -d DOT_DIRECTORY Where to write out .dot files. --image-directory IMAGE_DIRECTORY, -i IMAGE_DIRECTORY Where to write out image files. --image-type {gv,vml,dot_json,imap_np,pov,tiff,pic,canon,jpg,ismap,sgi,webp,gd,json0,ps2,cmapx_np,plain-ext,wbmp,xdot_json,ps,cgimage,ico,gtk,pct,gif,json,fig,xlib,xdot1.2,tif,tk,xdot1.4,svgz,gd2,jpe,psd,xdot,bmp,jpeg,x11,cmapx,jp2,imap,png,tga,pict,plain,eps,vmlz,cmap,exr,svg,pdf,vrml,dot}, -t {gv,vml,dot_json,imap_np,pov,tiff,pic,canon,jpg,ismap,sgi,webp,gd,json0,ps2,cmapx_np,plain-ext,wbmp,xdot_json,ps,cgimage,ico,gtk,pct,gif,json,fig,xlib,xdot1.2,tif,tk,xdot1.4,svgz,gd2,jpe,psd,xdot,bmp,jpeg,x11,cmapx,jp2,imap,png,tga,pict,plain,eps,vmlz,cmap,exr,svg,pdf,vrml,dot} The image format. --view, -v View rendered graphs with default image viewer You must have the graphviz tool suite installed. Please visit http://www.graphviz.org for more information. Automat-0.8.0/docs/api.rst0000644000175000017500000000027313552700725015030 0ustar mrwmrw00000000000000======== API Docs ======== .. automodule:: automat .. autoclass:: automat.MethodicalMachine :members: input, output, state .. automethod:: automat._methodical.MethodicalState.upon Automat-0.8.0/docs/make.bat0000644000175000017500000000144513264025362015131 0ustar mrwmrw00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=automat if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd Automat-0.8.0/docs/conf.py0000644000175000017500000001272013552756732015035 0ustar mrwmrw00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # automat documentation build configuration file, created by # sphinx-quickstart on Thu Sep 14 19:11:24 2017. # # 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. # import os import sys docs_dir = os.path.dirname(os.path.abspath(__file__)) automat_dir = os.path.dirname(docs_dir) sys.path.insert(0, automat_dir) # -- 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 = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'automat' copyright = '2017, Glyph' author = 'Glyph' # 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. def _get_release(): import pkg_resources try: distribution = pkg_resources.get_distribution(project) except pkg_resources.DistributionNotFound: raise Exception( "You must install Automat to build the documentation." ) else: return distribution.version # The full version, including alpha/beta/rc tags. release = _get_release() # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = 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 = 'sphinx_rtd_theme' # 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 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'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', # needs 'show_related': True theme option to display 'searchbox.html', 'donate.html', ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'automatdoc' # -- 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': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # 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 = [ (master_doc, 'automat.tex', 'automat Documentation', 'Glyph', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'automat', 'automat Documentation', [author], 1) ] # -- 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 = [ (master_doc, 'automat', 'automat Documentation', author, 'automat', 'One line description of project.', 'Miscellaneous'), ] Automat-0.8.0/docs/Makefile0000644000175000017500000000113713264025362015162 0ustar mrwmrw00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = automat SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)Automat-0.8.0/docs/_static/0000755000175000017500000000000013552760067015156 5ustar mrwmrw00000000000000Automat-0.8.0/docs/_static/mystate.machine.MyMachine._machine.dot.png0000644000175000017500000003462413264025362025156 0ustar mrwmrw00000000000000‰PNG  IHDR,9*f]bKGDÿÿÿ ½§“ IDATxœíÝwXTWþ?ð÷PÅAЍHUš jHDEq±€%ÖÝ617úM1šìó[ÝÄd7’ˆÙ5e5&®(’(1ˆbÔÅ‚" E"E™óûãd¨ ÃÌ\îÌçõ<óÃs?wtÞœ{n9Æ!„ô~qFBW@!ª¢À"„ˆ!D4L„.@Ójj€ü| ¨ˆ?*+òr ¢‚_QÁ |ùÚZàÑ#þ}c#`Òâ±´LMù÷R)`cX[·þÚ¿?`o¸¸ð¯Šå !š'ºÀª¯rr€ÌÌæGNp÷.PPTU W›DÂCËÁpv¼¼€aÚ¿:; W!ú@Ò›iiüqñ"žܾÍ{BšfjÊ{TŒñ˜6H¥€ðÇèÑ@ß¾ÚY!z&®×–\\¾ $'ÇgÎ𓪤R¾[6x0ïÉ88ŽŽ­wßßÛÚò×ôë×zPÅ.c}}ó.¥âky9PVÆw=óóÂÂæÝQÅ.gWŒyˆ„…&NNªo7!DØÀÊÎâã_~RRºîÙXZò·bKñðôäaÔ[0ÆÃ6+‹?23ù×7€›7™¬ó×{xððš6 ˜:•‡,!DÇ%“©©ÀÁƒ<¨®]ëxY[[¾ëÔr÷iØ0ÀHäÇ5kkyOR±››–Æî¨GfjÊÃkæLþpw×m½„ô"º ¬«W={€mÛ€Ü\åË „„ð]£ˆPb'UÕÕ.'OGò¯uuÊ—õõ/^x¿g„íVe%¨o¾á½ˆ¶ŒÐPÞk˜> ÷è-EOôæÍö˘™‘‘ÀþDEN¸ƒ¦ùÀÊÉ6n¾þxð õïÌÌø‡kÞ<þa³³Óäšõ×µk<¸vïæ»‘myyË—ÑÑüà!zJs•‘¼÷°??â×ÒãÏ=<ó …TOedÛ·»vµ?Šjk ,] ¼õ Ô½ÔóÀÊË֬ᢖG¿úöåc-Ë–Ç÷´NÒ–\$&±±|Ü«¥€wß^}07¦>B´@ýÀzôX·øä“ÖÄŽŽÀk¯/¿Ì/[!Úwå ðé§¼×¥¸Ì†å»ç3g V!š¤^`¥§ó£TééÍÏõï¼ý6ïQYXh²F¢ª¼<þG䫯Z÷v,þùOÞó"Dĺw?,™ øóŸààæ°27V®ä'®\Ia%$à‹/øÀü´iÍÏïÝ Œüô“pµ¢ *÷°ª«ßÿøï›Ÿóóã§.øûk©:Ò#{÷òq¬ÒRþ³±1ð·¿ñ?,„ˆj=¬¼<`ìØæ°25>ü8žÂª7[°€o͚ŖɀU«€W^éúò Bz£.{X¥¥üìó7øÏýûó¿Ü“&é¢<¢ Œ ¼óÿà§™|ó ¿%!"ÑyëÁ>¢«aÀӧ)¬ÄF"á»»v5Ÿæ°cð—¿[!ÝÕi`-^̯qWW )‰ŸUMÄéÙgyhóŸ?ø€ÿLˆXtXßÏÏZøáðŸ~öŽ™'Nœ€D"Áºuëze{b1oðùçÍ?ÿßÿ÷î W!Ý¡4°**øùT [¶ðûPý°d ?â ð1Ê×_¶BT¥4°ââøÝ3`þ|àÉ'uYÑ…O?m¾=ÍþÃoDH¯ÇÚËsvf `ÌÄ„±Û·Û.¡YlãÆ,00ÙØØ0kkkÄ>ùäVSSÃclíÚµ €ÒGKÉÉÉláÂ…ÌÃÙ™™±°3f°'N´ZNÕöär9ûꫯؘ1c˜¥¥%ëÓ§=z4Û´i“Ëåjo³ªujÛ§Ÿòg€±eËtºjBÔ±±]`;×üŸxÖ,íW°bÅŠÃ#..Ž1¦ZÀv¸Œ‰‰ KNNnZV•öär9[´hQ‡Ë½ôÒKjmowêÔ¶ÊJÆúôáÿÖ®®:[-!êjXqqÍõ¯i¿///&•JÙ¾}ûXEE«©©aéééì­·Þb[·nmZ.%%…`k×®UÚNQQ›S4!bÑå=ÝPäåñŸœødAAº(hÊ¿ÿÍÇ­øÏ3g?ü˜´;^«ëYs\\€Ã‡!CøÏ|WqÛ6íVF4ãÁॗøMûaõä“ÀîÝVD|TšækÄ>¥×o'¢£ºšÏüÇüޱ Ë–ñžMxKÄH噟÷uñÅæç~ú‰Ï(üé§Íã"DxùùüÈîĉÀÍ›ü¹~ý€íÛÏ>Œº5ß7!½G·þëöé|õßEüí&¨¬ä÷÷òâã$4A§pÊËùD©Ã†ñ9£“ãÇóéëŸ{NØúé)µþÖN›\¾Ì{[Š¿Öyy|œÄߟoQKw ?ÿðð>úPÜ¡ÆÖؼ™_/èé)l„hB—G »réÿ«ž˜ØúùÁƒù!óW^ồDóÒÓØXà»ï€–wº±°^{ÿ»èøæ„hS\KáØ1>“pjjëçÍͨ(>)kT`f¦‰µ®’àÛo;ù–ÌÌøû¼f°sH¢%š ,…Ó§ €ÚÞjÊÎxæ`Î~ÄÑÔT“kÖ_ee@B?‘71QùûúÊ+¼Gëà L„è€æK!7—_£¶kp÷nûß[[ó±°™3ÈHà·ÛS‘ß\¿Î/HŽç½Ve3{Œ_BõÜs@ß¾º¯‘Ó^`)Èåü·cŸ°³ºZùrîî@D?«~Ò$ÃÛ¥ÉÉNœNž~þ¸}[ùrÎÎÀܹü€‡ŸŸNK$DhÚ¬–ª«ù)ññ|GÉ>ðë}|øå?üäGÿÖ&ˆYn.–ÆO5HKΞåcS>œ÷DgÍÆŒ¡ó¨ˆÁÒm`µ$“ñžW|<ðË/üˆWWçp¹»ó³î‡ ãç})¾öÆÞX}=pëpã•dfò¯—.u}׋~ýxOsÊRº©™^N¸Àj«ªŠï?Î.´>Tß ~"«ƒÿ:x01{{> mmÍ{gНææêÕøà?Q¶¢‚?*+y/± €Ÿ •ŸÏ¿æåñ› v0¿E;vv@h(0a?@×ù¢Dï ¬¶ø-m.^lÞuºtI3SõéÓ|-©©òé­㡤PU¥™³ø滸ͻ»4[ !*é½¥ c¼Ór+3“Xðrx¡õŒxÏÎÙ™ïÊ Ö¼ëêåE'rÒâ ¬®ÔÖò@+*â»e%%@y¹68cèÐ…ðôü*+ù®Ý£G|y€÷æZ½”HZðÛØð ²±á£ØµTìf:;óž“‹ +Ú#D+ô+°”9vì&Mš„3gÎ $$Dèr!êëú~b·gϸ¹¹á±–3ÄBDI¯K&“á‡~ÀSO=‰D"t9„ÒëÀ:vìJJJðÔSO ] !Dô:°öîÝ  ] !Dô6°d2öïßùóç ] !DCô6°Nž<‰’’Ì;WèR!¢·õßÿþŽŽŽtt=¢·µÿ~Ìž=›Ž¢Gô2°.]º„œœÌž=[èR!¤—µÿ~X[[c„ B—BÑ ½ ¬`úôé0£/Ñ+zXHKKìY³„.…¢azX 022ÂäÉ“….…¢azX‡Ƙ1cП¦á!DïèU`544à—_~Add¤Ð¥B´@¯+%%•••ˆŠŠºBˆèU`>|ð£ ûÑKzX ˆŠŠ¢³Û ÑSzX¸zõ*¦N*t)„-Ñ›Àúå—_ ‘H0qâD¡K!„h‰ÞÖ±cÇ0zôh 8PèR!Z¢W5iÒ$¡Ë „h‘^VNNnß¾ððp¡K!„h‘^Ö/¿ücccŒ?^èR!Z¤uìØ1Á¦åtÍ„½£•œœLG 1¢¬ÜÜ\`ܸqB—BÑ2ÑÖéÓ§!!!WBÑ6ÑÖ©S§àéé {{{¡K!„h™^Ö˜1c„.ƒ¢¢¬GáÒ¥KX„QÖùóçñèÑ# ,B „¨ëÌ™3J¥9r¤Ð¥Bt@Ô•––˜˜˜] !DDX—/_ÆèÑ£….ƒ¢#¢ ¬††\¿~£FºBˆŽˆ6°®]»†úúzêab@DX—/_†D"¡!D´uåÊ :VVVB—BÑÑÖåË—©wEˆm`]¿~¾¾¾B—AÑ!QVcc#òóóáææ&t)„e`åå塱±‘‹#ÊÀºuëP`b`DX·o߆‘‘\\\„.…¢C¢ ¬[·nÁÉÉ æææB—BÑ!ÑÖСC….ƒ¢c¢ ¬‚‚¸ºº ]!DÇDX÷ï߇ÐeBtL”U^^N“¦b€DXX„ ÑVcc#ª««)°1@¢ ¬ŠŠ 0Æ`kk+t)„e` !HtUSSJ¥WBÑ5Ñ–L& \ !D×DXr¹`d$ºÒ !=$ÚO½D"ºBˆŽ‰.°úôéxøð¡À•BtMtÕ·o_@mm­À•BtMt¥8:¨8ZH1¢ ,;;;¡´´TèR!:&ºÀ211AÿþýQTT$t)„]`€½½=!H”åææ†ììl¡Ë „è˜(ËÛÛ7nܺ BˆŽ™]€:¼½½qëÖ-<|ø°é¼¬î’Éd˜3gކ+#D»"##ñÇ?þQè2#ÊVPPqñâEµÛËåˆG~~¾+#D{Ž?ŽK—. ]† DÙÃ=z4,--qêÔ)„††ö¨­åË—ãùçŸ×Pe„hOpp°Ð%N”=,<öØc8~ü¸Ð¥BtH”QQQ8rä]¢Cˆå.!Ìš5 +V¬ÀÑ£G1kÖ,´y4ºÕr´Eˆ&Œ^Úî³M….£×m` 6 þþþضm›Æ«ê¶fV؇ˆöm!zäÚ¶GxTÉ„.£Wõ'ó•W^ÁÒ¥K‘››‹!C†h¤M‡P­RïT B4éÆÎGB—Ðëˆv .\KKKlܸQèR!: êÀêׯÞ|óMlÚ´ 999B—CÑ2Q¼ñÆèß¿?V­Z%t)„-}`I¥R|öÙgØ»w/þóŸÿ]!D‹DXðÔSOaÑ¢EXºt)222„.G%‰‰‰H$øôÓO….…ÑЋÀ€þóŸpssôiÓpçΡË!:tâÄ H$¬[·®W¶G4GoËÊÊ ?ýôúöí‹ÐÐÐ]Méô&°`àÀ8vì0aÂìܹSè’!¤WŽŽŽHNNÆœ9sðÜsÏaÊ”):×bŒaÛ¶m ƒ úõë‡Ç{ _~ù%•¾æôéÓ˜8q"¤R)ìììðüóÏãþýûí–;~ü8-ZOOO˜››càÀ˜9s&Nž<©´Ý–ãdª®£¼¼¯¾ú* ãàÁƒØ¶m$ ¾ÿþ{¥Ûüõ×_#44ýúõƒ……üüüðù矃1õÏÔ–Édˆ‹‹CPPlmmaccƒàà`lذ¡éÒuëÖaüøñ€Õ«WC"‘4=ÔyßTiO[ÛKTÀôXbb"óðð`‰„M›6ÅÇdzºº:Æcõõõ Û¶m[Óòû&T±óÖ©½>¹\Ξ~úi@éãÈ‘#MË>|˜` .dæææí– kÕvaaa‡íš˜˜°ääävõtwuuuÌßß¿Ýr‰¤i»öîÝÛn›-ZÔam/½ô’ÚïçŠ+:l7..Ž1ÆØÚµk;\F÷­«ö´¹½míð®`W·=jú9((ˆ-Y²Dcí‹ÐF½,Æx0íÚµ‹3L*•²Y³f±uëÖ1ì£>b÷ïßg2™¬ÇµeË€ÙÙٱ͛7³;wî°êêjvîÜ9ö‡?üýúë¯MË*Â{å•WXVV«­­e'Nœ`®®® KOOoZ¾¨¨ˆMž<™ÅÇdz¼¼þøc€y{{³¤¤$öàÁvëÖ-öÚk¯5µÓ6°¶oßΰQ£F±„„VVVƪ««Yrr2óóócXjjªZï§——“J¥lß¾}¬¢¢‚ÕÔÔ°ôôtöÖ[o±­[·6-—’’°µk×¶kC÷­³ö´¹½mQ`µ£ÿÕRFFûøãYxx8³±±i÷×ÑÍrtküøñízRQ„É”)SÚýnÓ¦M ûæ›oZ=þüy¶`ÁæèèÈLLLZÕîììÜãu„„„0‰DÂþ÷¿ÿµ[~òäÉJ+<<œ³»wï¶{MFFÀV®\ÙñщððpæååÅ:]®³€a¬ûï[gíis{Ûz×å ëo5¹»»³€€fiiɆÎþþ÷¿³øøxVZZª‘õˆÈFQ_üÜ]¾¾¾ðõõÅŠ+ÐÐÐ333¬Zµ ¨®®Æå¿÷lüáúõë°µµEDD„ʯ™8qb»çÜÝÝ>>øê«¯0jÔ(„……á»ï¾kªQ|`i’ÊËË‘””¤ñ¶srr`oo÷ßèÓ§$ ²³³‘••¥‘uxxx ??W¯^m÷;e½+€osß¾}QQQÑôÁmûPvdQU&&& êU«ðí·ßâÖ­[¨ªªBLLLÓ2FFü¿±²£°ê¼oµ§ííU•‹‹ /^Œ;v °°G޵µ5.\ˆQ£F!11Që5Kƒ“Y<ûì³øòË/‘ŸŸšš\¸p/¿ü2’““ÕnÛÕÕ%%%Ø´i*++QYY‰„„DEEiì/ê¼yóÀÃüùóñ믿6톼þúëøù矕¾&&&µµµˆˆˆÀÁƒqïÞ=Ô××#77‡¼yóÔðÐÐPlÞ¼W¯^E]]*++‘˜˜ˆ²²²Vwçèß¿? %%eee­ÚPç}ë¬=mn¯ºŒŒŒøøxœ>}666ˆŒŒDtt4jjjtZ‹Öéf¬¬÷ÑÆi 2™ŒÍŸ?¿[§5ÄÆÆ¶kGñ;Å¡{ÆÛ¿¿Ò6ØÈ‘#™]‡í¨ºŽÚÚZ6zôh¥§5,X°€`û÷ïoÕŽ\.gÑÑÑn3vøðaµÞOe§b(Ë–-kZ®±±‘999)= A÷­³ö´¹½m©{”P.—³o¾ù†õë×1‚]½zU#õô©‡¥AFFFسgþýïã‰'ž€T*…••BBB°eË¥ƒßªzòÉ'±k×.Œ=pppÀ’%K””sssÔoaacÇŽaÉ’%4húô郠  8p¾¾¾[[ÛV¯‘H$غu+vïÞˆˆØÚÚÂÌÌ îîî˜={6~üñÇn„héÌ™3Xºt)|}}aaa`ìØ±Ø²e bcc›–366Æ÷ßqãÆA*•¶jC÷­³ö´¹½š"‘H°xñbœ?ÆÆÆ ÅÙ³g­Ic„ŽL¡h£‡¥¯d2 `‰Ä¥ FçaUVV²ÐÐPfkkËÎ;§éuzX¤µ7ß|;wîDnn.jkk‘žžŽ§žz iii˜8q"ììì„.‘tƒ••áëë‹éÓ§‹~¦s ,ÒÊ7ðÜsÏaèСJ¥À¾}û`ii‰ 6¨Ýnzzz«ëò:zÌž=[ƒ[C~+ñ@*•bÞ¼yxôH¼“[P`‘VbccÝêBáùóçãÔ©Sð÷÷º<¢¦þýûcïÞ½¸|ù2V¯^-t9j3¨3ÝI×¼¼¼°uëV·ëïïOw2XPPÖ¬YƒÕ«WcÑ¢Eðóóº¤n£!äÍ7ß„··7þô§? ]ŠZ(°1 ¦¦¦øì³ÏpâÄ üôÓOB—ÓmX„˜ßýîw Å| t)ÝFcXmäiÀƒ;ú{ñ(yƒöÚ^µjfÍš…3gÎàñÇ×ÞŠ4Œ«ûÔÉÑPMƒÃDxcM`é¤üî=5cÆ ¸¹¹aÛ¶mXb5öãö·a!DI$,Z´qqqذaƒÒ[õFX-¥_jæ^F„hÄPG`ð­4½xñb¬[·?ýô“hNØ¥Àj©¾ècØX ] !ÀB@¦½ñT///øøø 11‘K´¬,ù_5B„–W¤õUL›6 ûöíÓúz4…Nk Ä€M:yyyÈÌ̺•P`­ñ÷÷§Ù·{¹1cÆ@"‘àܹsB—¢ ,B ˜µµ5<==qá¡KQ –cÛ¶m¢i—(§Îû-†£àà`œ?^è2TBEˆ>|8aé“’’,\¸ƒ ‚½½=žþyÜ¿àì쌣G6-›ŸŸ‰D‚ŠŠ À3Ï<ƒ .à…^€D"ÁO<€ï¼ûî»?~<¬¬¬ˆ“'O6µ£n»êlƒ¿¿?V¯^I“&ÁÊÊ ÞÞÞÎ’ÓVUU–/_777ØÚÚbÉ’%­fj¹qãF‡íΘ1‰FFFpssúuëZÝ‚¦«º +++øúú"..‰¥¥¥€êêj,_¾C† f̘ÜÜÜ.·iãÆpssƒT*ÅøñãqñâÅßïζ¡£×¨[—¶xzz¢¸¸UUU‚Õ 2ïÑ,Òê&Œ=IDATe÷tgg.3–×nÙ &°¨¨(VPPÀîܹÃÂÃÃYTTcŒ1''§V³áäåå1¬¼¼¼é¹   ¶uëÖVmúùù1[[[vìØ1VYYÉÖ¯_Ϭ¬¬XIIIÚíHgÛàççÇ\]]Ù™3gXMM [·n4h“Ëå]¶;kÖ,6nÜ8–‘‘ÁÊËËÙ_|Áâãã»Õncc#»xñ"óôôd»víjõuöú±cDzgžy†•””°ììl°{÷î1Æ›3g‹ŒŒd999¬²²’½ùæ›, €Éd²·çÆÌÔÔ”8q‚ÕÕÕ±S§N±·Þz«Ë÷»£mPö•ë:~±üâVmu÷žîª8sæ À.^¼¨ñ¶5l#V•Ͱ[·n5=wåʀݽ{·GõÎ;ï´znøðáì‹/¾`Œi6°ºÚ???öÉ'Ÿ4ýîþýû ËÏÏï´ÝÜÜ\€eee)ý}wÛýì³ÏØÓO?­Òë³²²Z…cŒ%%%5=§x¿ š~/“ɘ••»|ùr‡Û”““äR©ÒeTy¿ÛnCÛ×t«.Vqq±Ò)Üz!š„¢+yyy011ÁСC›žóôôlú]O¸»»·ûY“¨² ƒ jú⺲ºººNÛ½}û6ŒáááÑá2µ»wï^ÂÒ҉˗/o·ý½¾  –––0 ù²•–ïç­[·NNNM÷‹766FUUnß¾Ýa½nnnøöÛo±bÅ ãÅ_Ä¥K—:\^•mhIݺ´iÀ€066FII‰ ëï ¬.¸¸¸ ±±wîÜizîæÍ›M¿³°°hõÁVö®˜ú¼-Å^…œœ8;;@Úíî6¨kèСÉdÈÎÎîök‹‹‹ñì³Ïâ½÷ÞCaa!är9bcc•N¯Œ““ª««›Æ«´š zÈ!022Biii»©ägΜÙiÛ3gÎDbb"Ξ=‹I“&!,, µµµíÞoU¶¡íkzR—¶ÁÎÎ÷îÝdýÝAÕwww„……aéÒ¥(,,D^^–-[†ÈÈH888Àßß[·nEEErss±råÊvm888àòåËí>Œ›7oFrr2ÝjrÔ®xzzb̘1xíµ×pïÞ=äääàwÞiUÛ¬Y³ÌÌLÔÖÖâܹs˜3gN§³Æ9rï¾û.nÞ¼‰††Èd²¦Z۾ߪlCÛר[—¶ 8°Uø÷VX*ؽ{7,--1zôhÁÉÉ Û·o|ôÑG(--…““f̘¹sç¶{ýÛo¿Ã‡âÕѼ—_~þóŸáèèˆ]»váСC8p`Ûíî6ôÄÎ;1jÔ(L:îîî¸páÂÃû|ÝСCññÇãÙgŸ…••Þxã ¥ÛØ™Ý»w£¢¢˜>}:,X033lß¾>>>ˆŒŒÄ AƒðÚk¯!::ºÓ™²'L˜DFFÂÖÖ±±±Ø³g,--۽ߪlƒ²#uêÒ6KKKTWW ¶~• 7~&¬î%Ô???¶cǬËP:tˆÙÚÚ ]†æèhÐ1ÆÂÃÃÙ¢E‹´Ò¶m¤»5ÑÚ½{7LLL0eÊäää`åÊ•xæ™g„.K”ÚŽ™öV´K¨'nß¾ÝálÊÑÑѽ®]MˆˆˆÀÞ½{1dÈ̘1“&MÂ'Ÿ|Òåëzó6 ÅÜÜ\3BSK ééémoèСZ™¨T[íj‚¾ûî»n¿®7o“Pêëë›Æþz3êaBðèÑ#AýUEE¡À"„ˆ‡X‹Æ°Ú*«öþÁGb些ÐW,cXX-Y÷Õ2¡+!°µâ³8éõ°ÄÈ{¨Ð" ,‘ÈÍÍÅÅ‹….ƒ.ÕÖÖj­íªª*XYõþù8 >°Ö¬Yƒ5kÖ]!* Óx›õõõ(++ÃàÁƒ5Þ¶¦l`™ššŠf¦BÇkRqq1cX½]`` Ð%"¸¢">ô‹ÎÃ"ÄÀQ`BD£¨¨ÆÆÆ­n7Ý[Q`bàŠŠŠ0hÐ  ]J—(°1pÅÅÅ¢Ø(°1x¹¹¹M“ŸôvX„¸ÌÌLx{{ ]†J(°1`¸uë¼¼¼„.E%X„°œœ444`ذaB—¢ ,B Xff&P`Bz¿ÌÌLH¥ÒM¨«KX„°¬¬,x{{C"‘]ŠJ(°1`™™™¢Ù(°1h×®]£À"„ô~………(,,D@@€Ð¥¨Œ‹•––X„ÞïâÅ‹°µµ…«««Ð¥¨Œ‹•––†ÀÀ@Ñ!(°1XŠÀ ,B PEEnß¾-ªñ+€‹ƒ”––Æ!¤÷KKKƒT*Í](°1@©©©Åm‘[¢À"Ä¥¦¦bܸqB—ÑmX„˜ììlbìØ±B—ÒmX„˜'N@"‘àñǺ”n£À"ÄÀœ®_¿Ž+W® ++Kïv@ÂcBAQOrr2&Nœ066†L&ƒ››ÌÌÌ///a Ô¬8êa¢'d2àöíÛ¸yó&† |ôÑG¸{÷®ÀÕi!"¦lŒŠ1Ö^YYYx÷Ýwáââ‚ 6èº<£À"DÄ$I§¿—Ëå022°aÃðòË/ë¨*í¡À"Dĺ ,###˜™™á‡~€¥¥¥ŽªÒ ,BD¬«ÀbŒaçÎ>|¸Ž*Ò. ,BD¬³À’H$xï½÷0gÎV¤]&B@Q_Gebb‚‰'bõêÕ:®H»¨‡Eˆˆ) ,899aïÞ½066 *í¡À"DÄÚ–D"‰‰ <ªÒ ,BD¬m`1ưmÛ6Œ9R Š´‹Æ°±–'Žá7ÞÀÓO?-`EÚE=,BDLÑÃ’H$;v,>üðC+Ò.êa¢e÷ïwïÅÅ@EEó£²’­©PX¶µ5ØØð¯ææüySSÀʪýò @yyóÏŠÀ+-•0GEűhÑ ¦ßK$€½=.ggÀÅðô¼½aÀ¡CÅht{BTôð!på –\¼\ºÄCJ$FF€«+àá89ŽŽüáä88ðð°±lm5WÓ¥K—pîÜ9,\øTVee<$ yhyyü‘™É{s«¡C‘#€ 0utÔ\mZGEHòó_ŽΜ®^À×—À‡¼¼x¯ÅË«¹§¤+ 0íFW© €WVÿzéà²2þ{{{ 8? ãß÷¢ž! ee@b"pô(©œ¾kåë Œ×Ü 5 èÓGèj5+7—WZpö,pò$W“J1c€‰ÈH¾ý]\¾¨MXİ]¿ÄÇò©\øù&ðÇøñÀ€BW©{|·÷øñæGe%›1˜9˜4IçÁME Oa!°g°};ÿPZXcÇòâ¼yüCIZ“É€S§x°8\»Æß·3€çž¢¢\DE CCðÃÀ¶mÀ‘#|\æÉ'E‹€ˆþá#ª»v‡þÎÀÍ›À!Àï¼ôÿ^K(°ˆ~+/¾üØ´‰¢ ,^ <õ?€ô c¼çµc°{7÷š;xýuà‰'4¾: ,¢ŸŠ‹>¾úŠïÎ,Z,_Î̉vÔÔðÝìO?åG ŸxX³˜6Mc« Ysˆ~©®ÞŸŸ$¹c°b?¶e …•¶I¥ÀÿÈwããùnwd$œ?wN3ë À"zã›oøI› ¼ú*ÍÿÂÔõk‰æñÁøãÇù ý½{ÀãÏ> ””ô°mÍ”HˆpòóéÓèh`Ê~öùGiöŒr¢žéÓùÉ©_ üò 0bðÝwê·GEDí»ïøå%—.ñ¿æ;vðëæHïadÄÿ˜ddð#²Ï>ËO©ªR£-WGˆ0¼÷°p!?‰1#ƒÿ5'½×€À·ßûöÇŽ¡¡üBñî À"¢S_<ý4ð·¿ë×ó^¢ sçòS!êëùØÖ©Sª¿–‹ˆŠ\ÎϬ>xÿ¥~ûm¡+"êðöNŸæ×iFE—/«ö: ,"*o¼ÁƒjçN`öl¡«¿ÄÄDH$|úé§:_wÿþ@B¿ãÅäÉü]¡À"¢±m°q#ðÅ|·BH'Nœ€D"Áºuëze{bÑ·/ðßÿòû„=õ¿èº3XDJKùîßâÅ@LŒÐÕM8_—˜‘lØÐù²XDÞ|“߇é“O„®„hƒŸ¿þðý÷ù• ¡À"½Þ­[À®]ü?³v×%“ɇ   ØÚÚÂÆÆÁÁÁذajkkëÖ­Ãøñã«W¯†D"iz´tüøq,Z´žžž077ÇÀ1sæLœÌ° 0ccãvmûøø°¨Ý~w½ý6cƒ1ÖØ¨ô×)°H¯7acóæéf]^^^L*•²}ûö±ŠŠ VSSÃÒÓÓÙ[o½Å¶nÝÚ´\JJ ÀÖ®]«´¢¢"6yòdÏòòòX}}=+..f{öìaR©”EFF¶Z¾³ö¶oßΰQ£F±„„VVVƪ««Yrr2óóócXjjªZÛ«,,::šeee±êêjvüøq6jÔ(€ýå/Q«muœ:ÅÀعsJMEz?[[Æ>üP7ë g^^^¬¡¡¡Óåº ,Æ;þ<[°`sttd&&&­z.ÎÎÎ*·ÎŒÙÝ»wÛý.##ƒ`+W®Tq [SVHH“Ëå­~—ÍLMM™Zm«ãáCÆLLûê+¥¿ÞHó’^­±‘ÏÁ7x°nÖ‹yóæÁÓÓS§N…ŸŸÆŒƒ€€€nµ“ššŠððpÔ××+ý}]]Êmedd@&“Áå·‹$cMãVНwîÜéV}mM™2¥Ýœ»»;† †ÌÌÌµÝææü¢õŽîê@ƒî¤WS|†ärݬÏÏÏׯ_ÇöíÛáææ†””L›6 #FŒÀ•+WTngýúõ¨¯¯Çš5kpóæMÔÕÕA.—ƒ1ooïnÕ$ÿmãe2d2YS;¬Å`{GÁ(F2YÇ÷‡§éÕŒùE³º[§‰‰ ¨­­…··7bbbpöìY€‘ÿ[ߨÁ™Ž999°··Çûï¿ßêùììldeeÁ¶Í½o:kÏÇÇ/^ÄÝ»wa­¥‹&þùgüõ¯mÕËÊÉÉAff&<<<´²NejjxÚÞ^ù漢Ez½€>‘©.„††bóæÍ¸zõ*êêêPYY‰ÄÄD”••!''§i¹þýûRRRP¦˜…´WWW”””`Ó¦M¨¬¬Dee%ÕÔcj©³öbbbP[[‹ˆˆá'”¾öpõ*ïm™ÐÿdQ9q‚ß5ÖÆ†ÏW8dˆj¯£]B":FF|pvË૯øýÀoߺ*¢ ¹ˆ~÷;`Ô(>Űaª¿ž‹ˆVL pô(«Q£ø ¼Hö† Rf&¼õß•OLäQtµñãùQÏ=Ç?'çÏ ]i©ºøÿð÷çƒìÇóÝzSÓî·EEDOqÊCRpÿ>Â'ZUeÚ(¢= À矞ž|ÂÛeË€ôt`ìXõÛ¤À"z#<œ ¾þšÏ&⽮ѣùÀüáÃ4Æ¥MÀ;ï..üRªàÒ%þä·{ö]KHôš\:Äÿº§¦òÎÂ…¼çÕ£SD¹‡#G€;€ýû>}øÙêo¼¡ú© ÝGE Æ©Süzµ½{ùMâÆŒáá5k]PÝÇŽñÉO÷í<BCù… ù˜¢–P`Ãóð!lßüü3P_Ï`͘Ìœ ós½H³’ÞS=x¿gÕÕ|0ý÷¿çÝ””‹¶š~¦õÁƒÀ@Q¿®-$ˆˆàG´\½CðbVRÂïòzò$?×--?Ðì]_û§aX„(Èdüš”$'óìÇ€µ5ß} ä€ÀÝ]èj5çÁ~tõâELgÏ×®ñßyzò“='L"#;¿õ‹P`Ò‘G€sçxx>Í?ÌŠÉ0llxxy{óÁûaÃ//ÀÍ­÷^×XZÊÏMËÌä¬,~ïæM~p¢O~D587އ”££ÐU·BEHwóàJKã½Ň¿¶–ÿÞÔ”stäG$œþ°¶æþ詇ù„ÊJþµ´(,äÁZPÀ¿ÏÏîÜá'Õ|7ÎÕ•ìˆͽÆáÃ{oØþ†‹MÈÏoîµääwïò犊€¼<>V¦Œ"¸ŒŒxX(ްI$üù‡Åœ« |°hžëÑ£ömói²œy`º¸ðï½¼š{‚}úhþ=Ð ,Bt¡ªŠ÷ν¡òrþ½âgÅt„UU|, à¡Ô·oóÀ¶¥eóà¿~<ÐZöÖ¬­;;VMD*rq½»Hˆž°²âÒ3t¶ !D4(°!¢AE  ]!„¨ õÿk¾•Ã~ IEND®B`‚Automat-0.8.0/docs/about.rst0000644000175000017500000003550313264025641015372 0ustar mrwmrw00000000000000=========== Quick Start =========== .. people like things that are quick and easy What makes Automat different? ============================= There are `dozens of libraries on PyPI implementing state machines `_. So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: .. code-block:: python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... With Automat, you'd create a class with a :py:class:`automat.MethodicalMachine` attribute: .. code-block:: python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() and then you would break the above logic into two pieces - the `brew_button` *input*, declared like so: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() @_machine.input() def brew_button(self): "The user pressed the 'brew' button." It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*'s job: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() As well as a couple of *states* - and for simplicity's sake let's say that the only two states are `have_beans` and `dont_have_beans`: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." `dont_have_beans` is the `initial` state because `CoffeeBrewer` starts without beans in it. (And another input to put some beans in:) .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.input() def put_in_beans(self): "The user put in some beans." Finally, you hook everything together with the :py:meth:`.upon` method of the functions decorated with `_machine.state`: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to :py:meth:`automat.MethodicalState.upon` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. How do I get the current state of a state machine? ================================================== Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: .. code-block:: python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") Instead, just make your calling code do this: .. code-block:: python connection_state_machine.send_message() and then change your state machine to look like this: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.state() def connected(self): "connected" @_machine.state() def not_connected(self): "not connected" @_machine.input() def send_message(self): "send a message" @_machine.output() def _actually_send_message(self): self._transport.send(b"message") @_machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) so that the responsibility for knowing which state the state machine is in remains within the state machine itself. Input for Inputs and Output for Outputs ======================================= Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.input() def put_in_beans(self, beans): "The user put in some beans." However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans and then connect it to the `put_in_beans` by changing the transition from `dont_have_beans` to `have_beans` like so: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) Now, when you call: .. code-block:: python coffee_machine.put_in_beans("real good beans") the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our `brew_button` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) Note that we don't need to check first whether `self._beans` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up `_describe_coffee` to the process of brewing, so change the brewing transition to: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) Now, we can call it: >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] Except... wait a second, what's that `None` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a `list`. In this case, we have both `_heat_the_heating_element` and `_describe_coffee` outputs, so we're seeing both of their return values. However, this can be customized, with the `collector` argument to :py:meth:`.upon`; the `collector` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to :py:meth:`.upon` like this: .. code-block:: python class CoffeeBrewer(object): _machine = MethodicalMachine() # ... have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) And now, we'll get just the return value we want: >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) ==================================================================================================================== There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the `serialized=` argument to the `MethodicalMachine.state()` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() @_machine.state(serialized="on") def on_state(self): "the switch is on" @_machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @_machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) In this case, we've chosen a serialized representation for each state via the `serialized` argument. The on state is represented by the string `"on"`, and the off state is represented by the string `"off"`. Now, let's just add an input that lets us tell if the switch is on or not. .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() # ... @_machine.input() def query_power(self): "return True if powered, False otherwise" @_machine.output() def _is_powered(self): return True @_machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) To save the state, we have the `MethodicalMachine.serializer()` method. A method decorated with `@serializer()` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either `"on"` or `"off"`. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() # ... @_machine.serializer() def save(self, state): return {"is-it-on": state} Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with `@_machine.serializer()` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() # ... @_machine.unserializer() def _restore(self, blob): return blob["is-it-on"] Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() # ... @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Saving and loading our `LightSwitch` along with its state-machine state can now be accomplished as follows: >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True More comprehensive (tested, working) examples are present in `docs/examples`. Go forth and machine all the state! Automat-0.8.0/docs/index.rst0000644000175000017500000000432413552700725015367 0ustar mrwmrw00000000000000========================================================================= Automat: Self-service finite-state machines for the programmer on the go. ========================================================================= .. image:: https://upload.wikimedia.org/wikipedia/commons/d/db/Automat.jpg :width: 250 :align: right Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Why use state machines? ======================= Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; has_water, has_beans, is_lid_open and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read more about state machines and their advantages for Python programmers `in an excellent article by J.P. Calderone. `_ .. toctree:: :maxdepth: 2 :caption: Contents: about visualize api debugging Automat-0.8.0/docs/debugging.rst0000644000175000017500000001247213264025362016213 0ustar mrwmrw00000000000000========= Debugging ========= Tracing API =========== .. warning:: 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: .. code-block:: 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: .. code-block:: 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: .. code-block:: python 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: >>> s = Sample() >>> def tracer(oldState, input, newState): ... print("%s.%s -> %s" % (oldState, input, newState)) >>> s.setTheTracingFunction(tracer) >>> s.go() 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: >>> 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() 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 in 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. .. code-block:: 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) Automat-0.8.0/docs/examples/0000755000175000017500000000000013552760067015346 5ustar mrwmrw00000000000000Automat-0.8.0/docs/examples/lightswitch.py0000644000175000017500000000250013134677015020242 0ustar mrwmrw00000000000000from operator import itemgetter from automat import MethodicalMachine class LightSwitch(object): machine = MethodicalMachine() @machine.state(serialized="on") def on_state(self): "the switch is on" @machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) @machine.input() def query_power(self): "return True if powered, False otherwise" @machine.output() def _is_powered(self): return True @machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=itemgetter(0)) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=itemgetter(0)) @machine.serializer() def save(self, state): return {"is-it-on": state} @machine.unserializer() def _restore(self, blob): return blob["is-it-on"] @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self if __name__ == "__main__": l = LightSwitch() print(l.query_power()) Automat-0.8.0/docs/examples/automat_example.py0000644000175000017500000000650013264025362021076 0ustar mrwmrw00000000000000 from automat import MethodicalMachine class Door(object): def unlock(self): print("Opening the door so you can get your food.") def lock(self): print("Locking the door so you can't steal the food.") class Light(object): def on(self): print("Need some food over here.") def off(self): print("We're good on food for now.") class FoodSlot(object): """ Automats were a popular kind of business in the 1950s and 60s; a sort of restaurant-sized vending machine that served cooked food out of a coin-operated dispenser. This class represents the logic associated with a single food slot. """ machine = MethodicalMachine() def __init__(self, door, light): self._door = door self._light = light self.start() @machine.state(initial=True) def initial(self): """ The initial state when we are constructed. Note that applications never see this state, because the constructor provides an input to transition out of it immediately. """ @machine.state() def empty(self): """ The machine is empty (and the light asking for food is on). """ @machine.input() def start(self): """ A private input, for transitioning to the initial blank state to 'empty', making sure the door and light are properly configured. """ @machine.state() def ready(self): """ We've got some food and we're ready to serve it. """ @machine.state() def serving(self): """ The door is open, we're serving food. """ @machine.input() def coin(self): """ A coin (of the appropriate denomination) was inserted. """ @machine.input() def food(self): """ Food was prepared and inserted into the back of the machine. """ @machine.output() def turnOnFoodLight(self): """ Turn on the 'we need food' light. """ self._light.on() @machine.output() def turnOffFoodLight(self): """ Turn off the 'we need food' light. """ self._light.off() @machine.output() def lockDoor(self): """ Lock the door, we don't need food. """ self._door.lock() @machine.output() def unlockDoor(self): """ Unock the door, it's chow time!. """ self._door.unlock() @machine.input() def closeDoor(self): """ The door was closed. """ initial.upon(start, enter=empty, outputs=[lockDoor, turnOnFoodLight]) empty.upon(food, enter=ready, outputs=[turnOffFoodLight]) ready.upon(coin, enter=serving, outputs=[unlockDoor]) serving.upon(closeDoor, enter=empty, outputs=[lockDoor, turnOnFoodLight]) slot = FoodSlot(Door(), Light()) if __name__ == '__main__': import sys sys.stdout.writelines(FoodSlot.machine.asDigraph()) # raw_input("Hit enter to make some food and put it in the slot: ") # slot.food() # raw_input("Hit enter to insert a coin: ") # slot.coin() # raw_input("Hit enter to retrieve the food and close the door: ") # slot.closeDoor() # raw_input("Hit enter to make some more food: ") # slot.food() Automat-0.8.0/docs/examples/turnstile_example.py0000644000175000017500000000231212641273151021451 0ustar mrwmrw00000000000000from automat import MethodicalMachine class Turnstile(object): machine = MethodicalMachine() def __init__(self, lock): self.lock = lock @machine.input() def arm_turned(self): "The arm was turned." @machine.input() def fare_paid(self): "The fare was paid." @machine.output() def _engage_lock(self): self.lock.engage() @machine.output() def _disengage_lock(self): self.lock.disengage() @machine.output() def _nope(self): print("**Clunk!** The turnstile doesn't move.") @machine.state(initial=True) def _locked(self): "The turnstile is locked." @machine.state() def _unlocked(self): "The turnstile is unlocked." _locked.upon(fare_paid, enter=_unlocked, outputs=[_disengage_lock]) _unlocked.upon(arm_turned, enter=_locked, outputs=[_engage_lock]) _locked.upon(arm_turned, enter=_locked, outputs=[_nope]) class Lock(object): "A sample I/O device." def engage(self): print("Locked.") def disengage(self): print("Unlocked.") turner = Turnstile(Lock()) turner.fare_paid() turner.arm_turned() turner.arm_turned() turner.fare_paid() turner.arm_turned() Automat-0.8.0/docs/examples/io_coffee_example.py0000644000175000017500000000252412641273151021343 0ustar mrwmrw00000000000000from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() @_machine.input() def brew_button(self): "The user pressed the 'brew' button." @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." # self._heating_element.turn_on() @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." @_machine.input() def put_in_beans(self, beans): "The user put in some beans." @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) cb = CoffeeBrewer() cb.put_in_beans("real good beans") print(cb.brew_button()) Automat-0.8.0/PKG-INFO0000644000175000017500000005166713552760067013714 0ustar mrwmrw00000000000000Metadata-Version: 2.1 Name: Automat Version: 0.8.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: glyph@twistedmatrix.com License: MIT Description: Automat ======= .. image:: https://readthedocs.org/projects/automat/badge/?version=latest :target: http://automat.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://travis-ci.org/glyph/automat.svg?branch=master :target: https://travis-ci.org/glyph/automat :alt: Build Status .. image:: https://coveralls.io/repos/glyph/automat/badge.png :target: https://coveralls.io/r/glyph/automat :alt: Coverage Status Self-service finite-state machines for the programmer on the go. ---------------------------------------------------------------- Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Read more here, or on `Read the Docs `_\ , or watch the following videos for an overview and presentation Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: .. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg :target: https://www.youtube.com/watch?v=0wOZBpD1VVk :alt: Glyph Lefkowitz - Automat - Pyninsula #0 Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: .. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg :target: https://www.youtube.com/watch?v=TedUKXhu9kE :alt: Clinton Roy - State Machines - Pycon Australia 2017 Why use state machines? ^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , ``is_lid_open`` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail `in this excellent series of articles from ClusterHQ `_. What makes Automat different? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are `dozens of libraries on PyPI implementing state machines `_. So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: .. code-block:: python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... With Automat, you'd create a class with a ``MethodicalMachine`` attribute: .. code-block:: python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() and then you would break the above logic into two pieces - the ``brew_button`` *input*\ , declared like so: .. code-block:: python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*\ 's job: .. code-block:: python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() As well as a couple of *states* - and for simplicity's sake let's say that the only two states are ``have_beans`` and ``dont_have_beans``\ : .. code-block:: python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans in it. (And another input to put some beans in:) .. code-block:: python @_machine.input() def put_in_beans(self): "The user put in some beans." Finally, you hook everything together with the ``upon`` method of the functions decorated with ``_machine.state``\ : .. code-block:: python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): .. code-block:: python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to ``upon`` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. How do I get the current state of a state machine? -------------------------------------------------- Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: .. code-block:: python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") Instead, just make your calling code do this: .. code-block:: python connection_state_machine.send_message() and then change your state machine to look like this: .. code-block:: python @_machine.state() def connected(self): "connected" @_machine.state() def not_connected(self): "not connected" @_machine.input() def send_message(self): "send a message" @_machine.output() def _actually_send_message(self): self._transport.send(b"message") @_machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) so that the responsibility for knowing which state the state machine is in remains within the state machine itself. Input for Inputs and Output for Outputs --------------------------------------- Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. .. code-block:: python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: .. code-block:: python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans and then connect it to the ``put_in_beans`` by changing the transition from ``dont_have_beans`` to ``have_beans`` like so: .. code-block:: python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) Now, when you call: .. code-block:: python coffee_machine.put_in_beans("real good beans") the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our ``brew_button`` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: .. code-block:: python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) Note that we don't need to check first whether ``self._beans`` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change the brewing transition to: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) Now, we can call it: .. code-block:: python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] Except... wait a second, what's that ``None`` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a ``list``. In this case, we have both ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing both of their return values. However, this can be customized, with the ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to ``upon`` like this: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) And now, we'll get just the return value we want: .. code-block:: python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) -------------------------------------------------------------------------------------------------------------------- There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() @_machine.state(serialized="on") def on_state(self): "the switch is on" @_machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @_machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) In this case, we've chosen a serialized representation for each state via the ``serialized`` argument. The on state is represented by the string ``"on"``\ , and the off state is represented by the string ``"off"``. Now, let's just add an input that lets us tell if the switch is on or not. .. code-block:: python @_machine.input() def query_power(self): "return True if powered, False otherwise" @_machine.output() def _is_powered(self): return True @_machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) To save the state, we have the ``MethodicalMachine.serializer()`` method. A method decorated with ``@serializer()`` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either ``"on"`` or ``"off"``. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: .. code-block:: python @_machine.serializer() def save(self, state): return {"is-it-on": state} Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with ``@_machine.serializer()`` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: .. code-block:: python @_machine.unserializer() def _restore(self, blob): return blob["is-it-on"] Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: .. code-block:: python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Saving and loading our ``LightSwitch`` along with its state-machine state can now be accomplished as follows: .. code-block:: python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True More comprehensive (tested, working) examples are present in ``docs/examples``. Go forth and machine all the state! Keywords: fsm finite state machine automata Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides-Extra: visualize Automat-0.8.0/README.md0000644000175000017500000003654013552727030014060 0ustar mrwmrw00000000000000# Automat # [![Documentation Status](https://readthedocs.org/projects/automat/badge/?version=latest)](http://automat.readthedocs.io/en/latest/) [![Build Status](https://travis-ci.org/glyph/automat.svg?branch=master)](https://travis-ci.org/glyph/automat) [![Coverage Status](https://coveralls.io/repos/glyph/automat/badge.png)](https://coveralls.io/r/glyph/automat) ## Self-service finite-state machines for the programmer on the go. ## Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: [![Glyph Lefkowitz - Automat - Pyninsula #0](https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg)](https://www.youtube.com/watch?v=0wOZBpD1VVk) Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: [![Clinton Roy - State Machines - Pycon Australia 2017](https://img.youtube.com/vi/TedUKXhu9kE/0.jpg)](https://www.youtube.com/watch?v=TedUKXhu9kE) ### Why use state machines? ### Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; `has_water`, `has_beans`, `is_lid_open` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail [in this excellent series of articles from ClusterHQ](https://clusterhq.com/blog/what-is-a-state-machine/). ### What makes Automat different? ### There are [dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine). So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: ```python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... ``` With Automat, you'd create a class with a `MethodicalMachine` attribute: ```python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() ``` and then you would break the above logic into two pieces - the `brew_button` *input*, declared like so: ```python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." ``` It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*'s job: ```python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() ``` As well as a couple of *states* - and for simplicity's sake let's say that the only two states are `have_beans` and `dont_have_beans`: ```python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``` `dont_have_beans` is the `initial` state because `CoffeeBrewer` starts without beans in it. (And another input to put some beans in:) ```python @_machine.input() def put_in_beans(self): "The user put in some beans." ``` Finally, you hook everything together with the `upon` method of the functions decorated with `_machine.state`: ```python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) ``` To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): ```python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() ``` All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to `upon` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. ## How do I get the current state of a state machine? Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: ```python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") ``` Instead, just make your calling code do this: ```python connection_state_machine.send_message() ``` and then change your state machine to look like this: ```python @_machine.state() def connected(self): "connected" @_machine.state() def not_connected(self): "not connected" @_machine.input() def send_message(self): "send a message" @_machine.output() def _actually_send_message(self): self._transport.send(b"message") @_machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) ``` so that the responsibility for knowing which state the state machine is in remains within the state machine itself. ## Input for Inputs and Output for Outputs Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. ```python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." ``` However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: ```python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans ``` and then connect it to the `put_in_beans` by changing the transition from `dont_have_beans` to `have_beans` like so: ```python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) ``` Now, when you call: ```python coffee_machine.put_in_beans("real good beans") ``` the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our `brew_button` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: ```python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) ``` Note that we don't need to check first whether `self._beans` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up `_describe_coffee` to the process of brewing, so change the brewing transition to: ```python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) ``` Now, we can call it: ```python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] ``` Except... wait a second, what's that `None` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a `list`. In this case, we have both `_heat_the_heating_element` and `_describe_coffee` outputs, so we're seeing both of their return values. However, this can be customized, with the `collector` argument to `upon`; the `collector` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to `upon` like this: ```python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) ``` And now, we'll get just the return value we want: ```python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' ``` ## If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the `serialized=` argument to the `MethodicalMachine.state()` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: ```python class LightSwitch(object): _machine = MethodicalMachine() @_machine.state(serialized="on") def on_state(self): "the switch is on" @_machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @_machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) ``` In this case, we've chosen a serialized representation for each state via the `serialized` argument. The on state is represented by the string `"on"`, and the off state is represented by the string `"off"`. Now, let's just add an input that lets us tell if the switch is on or not. ```python @_machine.input() def query_power(self): "return True if powered, False otherwise" @_machine.output() def _is_powered(self): return True @_machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) ``` To save the state, we have the `MethodicalMachine.serializer()` method. A method decorated with `@serializer()` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either `"on"` or `"off"`. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: ```python @_machine.serializer() def save(self, state): return {"is-it-on": state} ``` Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with `@_machine.serializer()` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: ```python @_machine.unserializer() def _restore(self, blob): return blob["is-it-on"] ``` Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: ```python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self ``` Saving and loading our `LightSwitch` along with its state-machine state can now be accomplished as follows: ```python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True ``` More comprehensive (tested, working) examples are present in `docs/examples`. Go forth and machine all the state! Automat-0.8.0/.gitignore0000644000175000017500000000010013264025362014547 0ustar mrwmrw00000000000000.tox/ .coverage* *.egg-info/ *.py[co] build/ dist/ docs/_build/ Automat-0.8.0/Automat.egg-info/0000755000175000017500000000000013552760067015704 5ustar mrwmrw00000000000000Automat-0.8.0/Automat.egg-info/PKG-INFO0000644000175000017500000005166713552760066017017 0ustar mrwmrw00000000000000Metadata-Version: 2.1 Name: Automat Version: 0.8.0 Summary: Self-service finite-state machines for the programmer on the go. Home-page: https://github.com/glyph/Automat Author: Glyph Author-email: glyph@twistedmatrix.com License: MIT Description: Automat ======= .. image:: https://readthedocs.org/projects/automat/badge/?version=latest :target: http://automat.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://travis-ci.org/glyph/automat.svg?branch=master :target: https://travis-ci.org/glyph/automat :alt: Build Status .. image:: https://coveralls.io/repos/glyph/automat/badge.png :target: https://coveralls.io/r/glyph/automat :alt: Coverage Status Self-service finite-state machines for the programmer on the go. ---------------------------------------------------------------- Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Read more here, or on `Read the Docs `_\ , or watch the following videos for an overview and presentation Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017: .. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg :target: https://www.youtube.com/watch?v=0wOZBpD1VVk :alt: Glyph Lefkowitz - Automat - Pyninsula #0 Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017: .. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg :target: https://www.youtube.com/watch?v=TedUKXhu9kE :alt: Clinton Roy - State Machines - Pycon Australia 2017 Why use state machines? ^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; ``has_water``\ , ``has_beans``\ , ``is_lid_open`` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious 'if' checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in considerably more detail `in this excellent series of articles from ClusterHQ `_. What makes Automat different? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are `dozens of libraries on PyPI implementing state machines `_. So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: .. code-block:: python class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ... With Automat, you'd create a class with a ``MethodicalMachine`` attribute: .. code-block:: python from automat import MethodicalMachine class CoffeeBrewer(object): _machine = MethodicalMachine() and then you would break the above logic into two pieces - the ``brew_button`` *input*\ , declared like so: .. code-block:: python @_machine.input() def brew_button(self): "The user pressed the 'brew' button." It wouldn't do any good to declare a method *body* on this, however, because input methods don't actually execute their bodies when called; doing actual work is the *output*\ 's job: .. code-block:: python @_machine.output() def _heat_the_heating_element(self): "Heat up the heating element, which should cause coffee to happen." self._heating_element.turn_on() As well as a couple of *states* - and for simplicity's sake let's say that the only two states are ``have_beans`` and ``dont_have_beans``\ : .. code-block:: python @_machine.state() def have_beans(self): "In this state, you have some beans." @_machine.state(initial=True) def dont_have_beans(self): "In this state, you don't have any beans." ``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans in it. (And another input to put some beans in:) .. code-block:: python @_machine.input() def put_in_beans(self): "The user put in some beans." Finally, you hook everything together with the ``upon`` method of the functions decorated with ``_machine.state``\ : .. code-block:: python # When we don't have beans, upon putting in beans, we will then have beans # (and produce no output) dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[]) # When we have beans, upon pressing the brew button, we will then not have # beans any more (as they have been entered into the brewing chamber) and # our output will be heating the heating element. have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element]) To *users* of this coffee machine class though, it still looks like a POPO (Plain Old Python Object): .. code-block:: python >>> coffee_machine = CoffeeMachine() >>> coffee_machine.put_in_beans() >>> coffee_machine.brew_button() All of the *inputs* are provided by calling them like methods, all of the *outputs* are automatically invoked when they are produced according to the outputs specified to ``upon`` and all of the states are simply opaque tokens - although the fact that they're defined as methods like inputs and outputs allows you to put docstrings on them easily to document them. How do I get the current state of a state machine? -------------------------------------------------- Don't do that. One major reason for having a state machine is that you want the callers of the state machine to just provide the appropriate input to the machine at the appropriate time, and *not have to check themselves* what state the machine is in. So if you are tempted to write some code like this: .. code-block:: python if connection_state_machine.state == "CONNECTED": connection_state_machine.send_message() else: print("not connected") Instead, just make your calling code do this: .. code-block:: python connection_state_machine.send_message() and then change your state machine to look like this: .. code-block:: python @_machine.state() def connected(self): "connected" @_machine.state() def not_connected(self): "not connected" @_machine.input() def send_message(self): "send a message" @_machine.output() def _actually_send_message(self): self._transport.send(b"message") @_machine.output() def _report_sending_failure(self): print("not connected") connected.upon(send_message, enter=connected, [_actually_send_message]) not_connected.upon(send_message, enter=not_connected, [_report_sending_failure]) so that the responsibility for knowing which state the state machine is in remains within the state machine itself. Input for Inputs and Output for Outputs --------------------------------------- Quite often you want to be able to pass parameters to your methods, as well as inspecting their results. For example, when you brew the coffee, you might expect a cup of coffee to result, and you would like to see what kind of coffee it is. And if you were to put delicious hand-roasted small-batch artisanal beans into the machine, you would expect a *better* cup of coffee than if you were to use mass-produced beans. You would do this in plain old Python by adding a parameter, so that's how you do it in Automat as well. .. code-block:: python @_machine.input() def put_in_beans(self, beans): "The user put in some beans." However, one important difference here is that *we can't add any implementation code to the input method*. Inputs are purely a declaration of the interface; the behavior must all come from outputs. Therefore, the change in the state of the coffee machine must be represented as an output. We can add an output method like this: .. code-block:: python @_machine.output() def _save_beans(self, beans): "The beans are now in the machine; save them." self._beans = beans and then connect it to the ``put_in_beans`` by changing the transition from ``dont_have_beans`` to ``have_beans`` like so: .. code-block:: python dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans]) Now, when you call: .. code-block:: python coffee_machine.put_in_beans("real good beans") the machine will remember the beans for later. So how do we get the beans back out again? One of our outputs needs to have a return value. It would make sense if our ``brew_button`` method returned the cup of coffee that it made, so we should add an output. So, in addition to heating the heating element, let's add a return value that describes the coffee. First a new output: .. code-block:: python @_machine.output() def _describe_coffee(self): return "A cup of coffee made with {}.".format(self._beans) Note that we don't need to check first whether ``self._beans`` exists or not, because we can only reach this output method if the state machine says we've gone through a set of states that sets this attribute. Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change the brewing transition to: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee]) Now, we can call it: .. code-block:: python >>> coffee_machine.brew_button() [None, 'A cup of coffee made with real good beans.'] Except... wait a second, what's that ``None`` doing there? Since every input can produce multiple outputs, in automat, the default return value from every input invocation is a ``list``. In this case, we have both ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing both of their return values. However, this can be customized, with the ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an iterable of all the outputs' return values and "collects" a single return value to return to the caller of the state machine. In this case, we only care about the last output, so we can adjust the call to ``upon`` like this: .. code-block:: python have_beans.upon(brew_button, enter=dont_have_beans, outputs=[_heat_the_heating_element, _describe_coffee], collector=lambda iterable: list(iterable)[-1] ) And now, we'll get just the return value we want: .. code-block:: python >>> coffee_machine.brew_button() 'A cup of coffee made with real good beans.' If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...) -------------------------------------------------------------------------------------------------------------------- There are APIs for serializing the state machine. First, you have to decide on a persistent representation of each state, via the ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator. Let's take this very simple "light switch" state machine, which can be on or off, and flipped to reverse its state: .. code-block:: python class LightSwitch(object): _machine = MethodicalMachine() @_machine.state(serialized="on") def on_state(self): "the switch is on" @_machine.state(serialized="off", initial=True) def off_state(self): "the switch is off" @_machine.input() def flip(self): "flip the switch" on_state.upon(flip, enter=off_state, outputs=[]) off_state.upon(flip, enter=on_state, outputs=[]) In this case, we've chosen a serialized representation for each state via the ``serialized`` argument. The on state is represented by the string ``"on"``\ , and the off state is represented by the string ``"off"``. Now, let's just add an input that lets us tell if the switch is on or not. .. code-block:: python @_machine.input() def query_power(self): "return True if powered, False otherwise" @_machine.output() def _is_powered(self): return True @_machine.output() def _not_powered(self): return False on_state.upon(query_power, enter=on_state, outputs=[_is_powered], collector=next) off_state.upon(query_power, enter=off_state, outputs=[_not_powered], collector=next) To save the state, we have the ``MethodicalMachine.serializer()`` method. A method decorated with ``@serializer()`` gets an extra argument injected at the beginning of its argument list: the serialized identifier for the state. In this case, either ``"on"`` or ``"off"``. Since state machine output methods can also affect other state on the object, a serializer method is expected to return *all* relevant state for serialization. For our simple light switch, such a method might look like this: .. code-block:: python @_machine.serializer() def save(self, state): return {"is-it-on": state} Serializers can be public methods, and they can return whatever you like. If necessary, you can have different serializers - just multiple methods decorated with ``@_machine.serializer()`` - for different formats; return one data-structure for JSON, one for XML, one for a database row, and so on. When it comes time to unserialize, though, you generally want a private method, because an unserializer has to take a not-fully-initialized instance and populate it with state. It is expected to *return* the serialized machine state token that was passed to the serializer, but it can take whatever arguments you like. Of course, in order to return that, it probably has to take it somewhere in its arguments, so it will generally take whatever a paired serializer has returned as an argument. So our unserializer would look like this: .. code-block:: python @_machine.unserializer() def _restore(self, blob): return blob["is-it-on"] Generally you will want a classmethod deserialization constructor which you write yourself to call this, so that you know how to create an instance of your own object, like so: .. code-block:: python @classmethod def from_blob(cls, blob): self = cls() self._restore(blob) return self Saving and loading our ``LightSwitch`` along with its state-machine state can now be accomplished as follows: .. code-block:: python >>> switch1 = LightSwitch() >>> switch1.query_power() False >>> switch1.flip() [] >>> switch1.query_power() True >>> blob = switch1.save() >>> switch2 = LightSwitch.from_blob(blob) >>> switch2.query_power() True More comprehensive (tested, working) examples are present in ``docs/examples``. Go forth and machine all the state! Keywords: fsm finite state machine automata Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides-Extra: visualize Automat-0.8.0/Automat.egg-info/SOURCES.txt0000644000175000017500000000160513552760066017571 0ustar mrwmrw00000000000000.gitignore .travis.yml LICENSE README.md setup.cfg setup.py tox.ini Automat.egg-info/PKG-INFO Automat.egg-info/SOURCES.txt Automat.egg-info/dependency_links.txt Automat.egg-info/entry_points.txt Automat.egg-info/requires.txt Automat.egg-info/top_level.txt automat/__init__.py automat/_core.py automat/_discover.py automat/_introspection.py automat/_methodical.py automat/_visualize.py 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 benchmark/test_transitions.py docs/Makefile docs/about.rst docs/api.rst docs/conf.py docs/debugging.rst docs/index.rst docs/make.bat docs/visualize.rst docs/_static/mystate.machine.MyMachine._machine.dot.png docs/examples/automat_example.py docs/examples/io_coffee_example.py docs/examples/lightswitch.py docs/examples/turnstile_example.pyAutomat-0.8.0/Automat.egg-info/entry_points.txt0000644000175000017500000000007713552760066021205 0ustar mrwmrw00000000000000[console_scripts] automat-visualize = automat._visualize:tool Automat-0.8.0/Automat.egg-info/dependency_links.txt0000644000175000017500000000000113552760066021751 0ustar mrwmrw00000000000000 Automat-0.8.0/Automat.egg-info/requires.txt0000644000175000017500000000007613552760066020306 0ustar mrwmrw00000000000000attrs>=16.1.0 six [visualize] graphviz>0.5.1 Twisted>=16.1.1 Automat-0.8.0/Automat.egg-info/top_level.txt0000644000175000017500000000001013552760066020424 0ustar mrwmrw00000000000000automat Automat-0.8.0/tox.ini0000644000175000017500000000171113552727030014104 0ustar mrwmrw00000000000000[tox] envlist = coverage-clean,{py27,pypy,py35,py36,py38}-{extras,noextras},coverage-report [testenv] deps = extras: graphviz>=0.4.9 extras: Twisted>=16.2.0 coverage pytest commands = coverage run --parallel --source automat -m py.test automat/_test [testenv:coverage-clean] deps = coverage skip_install = true commands = coverage erase [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine coverage report -m [testenv:benchmark] deps = pytest-benchmark commands = pytest --benchmark-only benchmark/ [testenv:py27-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} [testenv:py35-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} [testenv:py36-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} [testenv:pypy-benchmark] deps = {[testenv:benchmark]deps} commands = {[testenv:benchmark]commands} Automat-0.8.0/setup.py0000644000175000017500000000337113552760047014314 0ustar mrwmrw00000000000000""" Setup file for automat """ from setuptools import setup, find_packages try: from m2r import parse_from_file long_description = parse_from_file('README.md') except(IOError, ImportError): print("\n\n!!! m2r not found, long_description is bad, don't upload this to PyPI !!!\n\n") import io long_description = io.open('README.md', encoding="utf-8").read() setup( name='Automat', use_scm_version=True, url='https://github.com/glyph/Automat', description=""" Self-service finite-state machines for the programmer on the go. """.strip(), long_description=long_description, packages=find_packages(exclude=[]), package_dir={'automat': 'automat'}, setup_requires=[ 'setuptools-scm', 'm2r', ], install_requires=[ "attrs>=16.1.0", "six", ], extras_require={ "visualize": ["graphviz>0.5.1", "Twisted>=16.1.1"], }, entry_points={ "console_scripts": [ "automat-visualize = automat._visualize:tool" ], }, author='Glyph', author_email='glyph@twistedmatrix.com', include_package_data=True, license="MIT", keywords='fsm finite state machine automata', classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], )