glyph / automat Goto Github PK
View Code? Open in Web Editor NEWSelf-service finite-state machines for the programmer on the go.
License: MIT License
Self-service finite-state machines for the programmer on the go.
License: MIT License
Fully recognize this question may reflect bad practice or might just run counter to the opinion of the framework (which is great!), but do you have any plan to implement conditional state transitions?
Borrowing from your example, if the coffee maker needs, say, '5 beans' in order to brew coffee, is there any way (within the framework) to check that condition is satisfied, or would something like the following be the 'automatic' way to implement that? (pythonic -> automat-ic? get it?)
def put_in_some_beans(self, beans):
self._bean_counter += beans.count
if self._bean_counter >= 5:
self._put_in_enough_beans(beans)
else:
self._put_in_not_enough_beans(beans)
@_machine.input()
def _put_in_enough_beans(self, beans):
"There are enough beans in the hopper"
@_machine.input()
def _put_in_not_enough_beans(beans):
"There aren't enough beans"
@_machine.output()
def bean_error(self, beans):
return f'Missing {5-self._bean_counter} beans'
dont_have_beans.upon(_put_in_enough_beans, enter=have_beans, outputs=[])
dont_have_beans.upon(_put_in_not_enough_beans, enter=dont_have_beans, outputs=[bean_error])
I don't have any ideas or thoughts about how an alternative method would be implemented, or whether it even belongs. The reason I'm asking is that I see what could end up being a lot of state-checking and perhaps a lot of branching logic outside of the automat framework -> and maybe thats ok? Just making sure I'm not missing something.
As an aside: I'm working through this document: and trying to learn automat by implementing the examples; which is helpful in terms of thinking about setting up more complicated machines from primitives (cascading, parallel, etc), but their implementation philosophy is so different... if I can grok it, it might make for some helpful documentation additions.
I was trying to link to automat's docs in twisted/twisted#588 but since there are no docs, pydoctor complained at me.
Having this combination is useful because Automat and Twisted have a cicle dependency (Automat needs Twisted and Twisted needs Automat).
So not having Twisted installed leads to:
[ 5s] test_saveOnlyImage (automat._test.test_visualize.VisualizeToolTests) ... skipped 'Twisted is not installed.'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: _visualize (unittest.loader._FailedTest)
[ 5s] ----------------------------------------------------------------------
[ 5s] ImportError: Failed to import test module: _visualize
[ 5s] Traceback (most recent call last):
[ 5s] File "/usr/lib64/python3.6/unittest/loader.py", line 153, in loadTestsFromName
[ 5s] module = __import__(module_name)
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_noAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_quotesAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_sortsAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_validGraphviz (automat._test.test_visualize.IntegrationTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 230, in test_validGraphviz
[ 5s] out, err = p.communicate("".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_containsMachineFeatures (automat._test.test_visualize.SpotChecks)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 247, in test_containsMachineFeatures
[ 5s] gvout = "".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: _discover (unittest.loader._FailedTest)
[ 5s] ----------------------------------------------------------------------
[ 5s] ImportError: Failed to import test module: _discover
[ 5s] Traceback (most recent call last):
[ 5s] File "/usr/lib64/python3.6/unittest/loader.py", line 153, in loadTestsFromName
[ 5s] module = __import__(module_name)
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_noAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_quotesAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_sortsAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_validGraphviz (automat._test.test_visualize.IntegrationTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 230, in test_validGraphviz
[ 5s] out, err = p.communicate("".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_containsMachineFeatures (automat._test.test_visualize.SpotChecks)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 247, in test_containsMachineFeatures
[ 5s] gvout = "".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_noAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_quotesAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_sortsAttrs (automat._test.test_visualize.ElementMakerTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 71, in setUp
[ 5s] from .._visualize import elementMaker
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_validGraphviz (automat._test.test_visualize.IntegrationTests)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 230, in test_validGraphviz
[ 5s] out, err = p.communicate("".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ======================================================================
[ 5s] ERROR: test_containsMachineFeatures (automat._test.test_visualize.SpotChecks)
[ 5s] ----------------------------------------------------------------------
[ 5s] Traceback (most recent call last):
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_test/test_visualize.py", line 247, in test_containsMachineFeatures
[ 5s] gvout = "".join(sampleMachine().asDigraph())
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_methodical.py", line 356, in asDigraph
[ 5s] from ._visualize import makeDigraph
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_visualize.py", line 7, in <module>
[ 5s] from ._discover import findMachines
[ 5s] File "/home/abuild/rpmbuild/BUILD/Automat-0.6.0/automat/_discover.py", line 4, in <module>
[ 5s] from twisted.python.modules import PythonModule, getModule
[ 5s] ModuleNotFoundError: No module named 'twisted'
[ 5s]
[ 5s] ----------------------------------------------------------------------
The docs at https://automat.readthedocs.io/en/latest/about.html don't seem to have syntax highlighting, which might be nice.
is any way to create transition with condition?
https://bitdust.io/visio2python/
Hi. Just found that nice project and I like it.
As part of my works I also developed a tool to develop code using state machine, it is called visio2python. I think it is very related to https://pypi.python.org/pypi/Automat/ . If you think combining both methods together may have some value, lets communicate/coordinate and do it.
Good luck, Glyph. Thanks for your work! I love Twisted.
Unfortunately, our world is filled with side-effects which are out of our control and can result in errors. Based on a short chat with @glyph exceptions occurring when executing outputs will just bubble up resulting the machine to be in an unexpected state. What is the best way to handle this? Would an API to move to another state on error make sense? Maybe an example with current API will be nice.
f strings in automat class or client raise a SyntaxError
in visualize.
Maybe it is a GraphViz
problem?
download and save this gist as tennisgamescore.py
in a tree structure like this:
tennis/
├── __init__.py
└── tennisgamescore.py
uncomment line 23 and run automat-visualize tennis
to reproduce.
Related to #30:
When the state machine is producing outputs, it is in an intermediate, undefined state. One of automat's big benefits is to eliminate the need for manual guards implemented around logic that requires set-up: any output generated by a state that has always had another output run before it can implicitly depend on that previous output having run. (This can be useful for initializing counters, collections, etc)
If we resolve #30 by always forcibly exhausting the output generator, the externally-visible (i.e. non-reentrant) exposure to this state might go away, but an output method might still want to call self.<some input>()
, which would potentially run outputs from the new input before the pending outputs have been invoked.
I ran into this issue while attempting to implement HostnameEndpoint
as an explicit state machine, and my solution was to retrofit input()
to add "feedback": https://github.com/glyph/twisted/blob/b5f7d36f3e69d352248c3d1d94b9d004dcea36cc/src/twisted/internet/_statefulhost.py#L18-L76
Roughly, I think this is the right shape of a solution, but another important piece is to give a sensible error message when outputs generate inputs without using this mechanism.
There's some code now that can do state-machine visualization, but actually getting a PNG out should be as simplified as possible.
And it is not archived :(
https://web.archive.org/web/*/https://clusterhq.com/blog/what-is-a-state-machine/
Maybe just remove the link, or maybe @exarkun or someone else from ClusterHQ have a copy and can republish it
Thanks
Hey Glyph,
I'm a big fan of this library. I'm noticing that there's a certain pattern which makes me wonder about an API, and I'm curious to hear your thoughts.
The docs have an example like this:
connected.upon(send_message, enter=connected, [_actually_send_message])
which uses the _actually_send_message
method. It reminds me a bit of marshmallow's pattern of field.serialize()
calling field._serialize()
and similar inheritance-oriented APIs. The common _actually_*
naming convention makes me wonder if there should be a real namespace for these implementations
outputs=[mm.actually.send_message]
or mm.impl.send_mesage
.
I'm interested if you've had this feeling or how you've dealt with the proliferation of _actually
methods.
Cheers.
With Automat-0.5.0, the following program:
from automat import MethodicalMachine
class Code(object):
m = MethodicalMachine()
@m.state(initial=True)
def S0(self):
print "S0"
@m.state()
def S1(self):
print "S1"
@m.input()
def go(self):
print "go"
S0.upon(go, enter=S1, outputs=[])
print "doing construction"
c = Code()
print "doing go"
c.go()
print "done"
produces the following output:
doing construction
doing go
go
done
I believe the intention of Automat is that States are methods (rather than attributes like attr.ib
would do it) only for:
.upon()
statements easy to write(initial=True)
and Inputs are methods only for:
target.inputXYZ(args)
to effect a state transitionOutputs are allowed to have behavior, so they're fully justified in being real methods.
The bodies of States aren't executed, but apparently the bodies of Inputs are, when the input function is executed.
As Glyph pointed out, really Automat should use dis
to inspect the bodies of both Inputs and States, and insist that they're empty at class-evaluation time. But a good starting point would be to ensure that the bodies aren't ever invoked, since that will help teach users about the intended emptiness.
I was experimenting with automat
to simulate a game. The game has a number of different phases, and different players may only be active in particular phases.
I modelled the phases as different states, and an input, advance_phase
allows transitioning from one phase to the next in a linear fashion. I.e. in state "A", calling advance_phase()
causes the machine to enter state "B". Calling it again causes the machine to enter state "C", etc.
I'd like to be able to query which players are active during a particular phase, but since there doesn't seem to be any way to map the machine state to other meaningful information in the game, I find I need to track the phases independently. So I end up with something like the following:
class Game(object):
_machine = MethodicalMachine()
# States
@_machine.state()
def have_players(self):
"""
The game has players configured.
"""
@_machine.state(initial=True)
def dont_have_players(self):
"""
The game doesn't have any players set.
"""
@_machine.state()
def phase_a(self):
"""
Game phase "A"
"""
@_machine.state()
def phase_b(self):
"""
Game phase "B"
"""
@_machine.state()
def phase_c(self):
"""
Game phase "C"
"""
# ...
@_machine.state()
def phase_z(self):
"""
Game phase "Z"
"""
# Inputs
@_machine.input()
def start_game(self):
"""
Start the game.
"""
@_machine.input()
def advance_phase(self):
"""
Advance to the next phase.
"""
# Outputs
@_machine.output()
def _set_phase_a(self):
self._game_phase = "a"
@_machine.output()
def _set_phase_b(self):
self._game_phase = "b"
# ...
@_machine.output()
def _set_phase_z(self):
self._game_phase = "z"
# Transitions
dont_have_players.upon(add_players, enter=have_players, outputs=[_set_players])
have_players.upon(start_game, enter=phase_a, outputs=[_set_phase_a])
phase_a.upon(advance_phase, enter=phase_b, outputs=[_set_phase_b])
phase_b.upon(advance_phase, enter=phase_c, outputs=[_set_phase_c])
# ...
phase_y.upon(advance_phase, enter=phase_d, outputs=[_set_phase_z])
I end up with lots of repetitive-looking states and outputs, and I was wondering if there is some guidance on the best way for implementing these multiple states or outputs, using some kind of looping perhaps.
Buildbot CI Just got issues with automat and the release of the new attrs:
https://nine.buildbot.net/#/builders/8/builds/235/steps/5/logs/stdio
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/trial/runner.py", line 508, in loadPackage
module = modinfo.load()
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/python/modules.py", line 392, in load
return self.pathEntry.pythonPath.moduleLoader(self.name)
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/python/reflect.py", line 301, in namedAny
topLevelPackage = _importAndCheckStack(trialname)
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/python/reflect.py", line 240, in _importAndCheckStack
return __import__(importName)
File "/buildbot/buildbot-job/build/master/buildbot/test/integration/test_custom_buildstep.py", line 30, in <module>
from buildbot.process import builder
File "/buildbot/buildbot-job/build/master/buildbot/process/builder.py", line 23, in <module>
from twisted.application import internet
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/application/internet.py", line 542, in <module>
class _ClientMachine(object):
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/twisted/application/internet.py", line 842, in _ClientMachine
outputs=[_connect])
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/automat/_methodical.py", line 62, in upon
self.machine._oneTransition(self, input, enter, outputs, collector)
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/automat/_methodical.py", line 255, in _oneTransition
tuple(outputTokens))
File "/buildbot/buildbot-job/build/sandbox/local/lib/python2.7/site-packages/automat/_core.py", line 74, in addTransition
(inState, inputSymbol, outState, tuple(outputSymbols))
I believe I am the first to find out, but we will soon have lots of CI broken.
not sure if this is automat or attrs fault yet...
Add some more description and links to documentation and source on PyPI
If I have multiple outputs, and want to return the result of the first one, an obvious option is to
state.upon(input, enter=next_state, outputs=[first_output, second_output], collect=next)
But since collect is passed a generator of outputs, the collector being next
causes the second output to not be evaluated.
This is related to #18.
A true RESTful interface (i.e. following HATEOAS) can often (always?) be efficiently modeled as a state machine. To do so, the links/actions section is populated based on the available state transitions (i.e. affordances).
There's a certain symmetry between real life and an FSM with a hidden state, but it makes the calculation of affordances especially difficult. Is the package specialized for a different use case (e.g. embedded programming)? or does a method listing affordances make sense?
I’m actually working on library evaluation for state machine and I’m very interesting in what Automat do. To resume, I develop a module to manage small process like Bluetooth communication with a remote sensor.
Automat can help me, in the future, to manage the state machine associate with the process (for example, Remote sensor stopped, remote sensor ready to measure, remote sensor measuring, …)
However, I have some difficulties to find some support (forum or mailing list), it’s why I write an issue on GitHub..
My questions about Automat :
I’m ready to contribute to the project if necessary.
Thanks a lot for your help.
Thibaud
this should go in the now-immense README I guess
Automat should have a test helper that asserts all transitions occurred.
It might go a little something like this:
tracer = TraceAllTransitions()
someMachineInstance.traceWith(tracer)
# testing intensifies
assertHasAllTransitions(tracer, for_=SomeMachine)
...where the tracer stuff would come from #36
Finding docs is non-obvious if you just end up in github repo.
this should be an error; cite http://www.erights.org/data/serial/jhu-paper/upgrade.html
When an input is sent to a state that has not defined an "upon" on it, it raises KeyError. This is an implementation detail -- internally, valid transitions are kept in a dictionary and the look-up fails.
The right thing is to raise an exception, but this is not really the right semantics for LookupError, which is meant to function as a "you have tried to look for a missing key". For an example where this might cause harm in practice, imagine a class which is meant to have a "lookup(key)" method, but which looks in different dictionaries depending on the state. It is natural to catch a KeyError from this, for example to substitute a default value. However, if a state for which "lookup()" is invalid [e.g., before the dictionaries are initialized] we do not want to mask that error.
I suggest "InputInvalidForState" inherit from "ValueError". As a precedent, trying to perform ".read()" from a closed file raises ValueError. UnicodeError, which is also a case of "bad current configuration of input", is also a subclass of ValueError.
My understanding is that there has been a previous suggestion of NotImplementedError. However, NIE is supposed to indicate an unfinished class, not a bad (possibly temporary) state. It is normally implemented class-wide. I do not think we should consider different states to be different classes.
The link to the series of blog posts by ClusterHQ doesn't work. It looks like they took the blog down when they shut the company down.
Today you can do machine.state(initial=True)
to mark the initial state of a machine. In tinkering with automat today, I've got a machine that has some explicit end states, and it occurred to me that it'd be nice to be able to mark them as machine.state(terminal=True)
and then if you try to do .upon(some_action, ...)
on them, they'd explode with an error about trying to leave a terminal state.
We should work out an API that allows you to trace the execution of an Automat machine. You should able to register with a machine an object that receives a sequence of (input, state, outputs)
tuples and then unregister it. It should help with other issues.
hey @hawkowl. what's up
Per @moshez's suggestion:
The collector
callable passed to upon
runs on a generator of outputs. That means replacing it with next
, as suggested by the docs, for a transition that includes multiple outputs will result in only the first output running!
Often times you just want the last output's result. By adding a choose
argument to upon
, a user can use a numeric index to extract their desired output. For example, this:
someState.upon(someInput,
enter=anotherState,
outputs=[firstOutput, secondOutput],
choose=-1)
...would mean that someMachine.someInput()
would run both firstOutput
and secondOutput
, but only return secondOutput
's result.
What about choosing a specific output instead? Like so:
someState.upon(someInput,
enter=anotherState,
outputs=[firstOutput, secondOutput],
choose=secondOutput)
Is that too verbose?
@glyph Hey man! Could you please push to PyPI? The last release was May 16, 2017.
I've noticed problems while trying to develop machines using automat that inherit from base machine classes.
File ".../automat/_methodical.py", line 274, in __get__method=self.method.__name__ AttributeError: GiveFeedback._initialize is a state-machine output method; to produce this output, call an input method instead.
Automat should have a test helper that asserts a given transition occurred after running a block of code.
It might go a little something like this:
with assertTransitioned(someMachineInstance,
through=[SomeMachine.state1, SomeMachine.state2]):
someMachineInstance.blah()
someMachineInstance.otherblah()
While working on adding tracing, I accidentally had a machine with transitions like this:
begin.upon(go1, middle, [out])
middle.upon(go2, end, [out])
end.upon(back, middle, [])
end.upon(back, begin, [])
and then I was surprised when calling m.back
from the end
state gave me an error:
raise NoTransition(state=inState, symbol=inputSymbol)
automat._core.NoTransition: no transition for MethodicalInput(method=<function go2 at 0x10365d1b8>) in MethodicalState(method=<function begin at 0x103653f50>)
Of course my machine should have used:
begin.upon(go1, middle, [out])
middle.upon(go2, end, [out])
end.upon(back, middle, [])
middle.upon(back, begin, [])
I could have discovered this earlier if MethodicalMachine had complained to me that I was registering two transitions with the same state and the same input, instead of allowing the most recent .upon()
call to silently win.
I think this might require the Automaton._transitions
to record dicts of dicts instead of a flat list of tuples. Or maybe the Automaton.addTransition
method could just search the existing list for partial matches and raise an exception if one is found.
Incidentally, is there much value in having that NoTransition exception's string form include all that "MethodicalInput" and object ID noise? I'd find it easier to read if it said:
raise NoTransition(state=inState, symbol=inputSymbol)
automat._core.NoTransition: no transition from state=begin for input=go2
The actual state and input "symbol" objects are available on the exception object, should any exception handlers need them.
on the visualization page, should the output_on_change_state
method be decorated with @_machine.output()
i/o @_machine.input()
?
If yes, I will propose a PR.
I have not because the learning curve is quite steep, and I am not that familiar with state machines yet.
perhaps the same tool in #5 - often it's useful to see "in what states is input X invalid" so we can provide more cases that will be covered
Hi, my code breaks when I accidentally upgrade Automat from 5.0 to 6.0.
Is there anywhere I can find the doc for difference?
Many thanks
feng
Automat uses setup_requires, which is obsolete, broken and discontinued mechanism of supplying build-time dependencies. The main issue is that package build started as a subprocess of other build cannot derive dependencies location supplied in initial build process, thus completely breaking off-line builds (CI, package builders, isolated enterprise build farms, etc).
Recommended way of dealing with build-time dependencies is PEP 518, which is simply a TOML file listing the dependencies: https://www.python.org/dev/peps/pep-0518/
The patch for #62 in 3766391 that passes the frozen
keyword to attr.s
assumes attrs>=16.1.0
per python-attrs/attrs@cfa6d2e.
It would be nice to be able to have outputs attached to an input that don't take all the arguments to the input. At the very least, it would be nice to be able to mix outputs that take no arguments with ones that take arguments that match the input.
The following example is extracted from a machine that I was trying to write for twisted's ClientService
. Note that not even specifying an argument with a default will work.
class Service(object):
machine = MethodicalMachine()
@machine.state()
def idle(self):
"The service isn't started."
@machine.state()
def connecting(self):
"The service is connecting."
@machine.state()
def connected(self):
"The service is connected."
@machine.input()
def startService(self):
"Start the service."
@machine.input()
def connectionMade(self, protocol):
"A connection has been made."
@machine.output()
def resetFaileAttempts(self):
self.failedAttempts = 0
@machine.output()
def notifyWaiters(self, protocol):
self._waiters.notify(protocol)
@machine.output
def connect(self):
callLater(connect, callback=self.connectionMade)
idle.upon(startService, enter=connecting, outputs=[resetFaileAttempts, connect])
connecting.upon(connectionMade, enter=connected, outputs=[resetFaileAttempts, notifyWaiters])
For libraries that wish to use Automat to export public objects, mostly, the fact that Automat is used is an implementation detail. However, any invalid transitions raise an Automat-specific error (NoTransition
).
automat-visualize
really needs documentation to explain how to use it.
Per @moshez's suggestion and @glyph's API design guidance:
Writing a transition that loops back to a given state should be as easy as omitting enter
.
So this:
someState.upon(someInput, enter=someState, ....)
should instead be:
someState.upon(someInput, ....)
One suggestion: what if this were a new method, instead? Like so:
someState.loopOn(someInput, ...)
I think that reads a little nicer, and keeps enter
a required argument for upon
, which, in general, should require a state to enter.
This is more of a question than an issue, but there's no #automat tag in stack overflow (yet!), so I hope it's ok to ask it here. Also, it kinda relates to #70, I think?
I would like to build a state machine class programmatically, with a decorator, for instance, that takes a class with a MethodicalMachine attribute, and set some methods as inputs, some as outputs, some as states, and add the transitions (which methods are what, and which transitions should be add will come from somewhere else). However, I'm hitting into the fact that I can't access the inputs and outputs from outside the class scope, it seems like the descriptors block me from doing that.
So, my question: Is there a way to manipulate the states, inputs and outpus programmatically like that, that I'm missing somehow?
Automat should have a benchmark suite so we can measure performance overhead.
@tardyp pointed out (in #54) that the function-bodies-are-empty check fails on python3.6, causing tests to fail. Oops.
I've got a PR to add test coverage for py3.6: #57, which should fail until we fix this check. I tried to add py3.6 support a few weeks ago, but at that point Travis didn't provide python3.6 in the normal python: 2.7
environment. I don't know if they do now or not.. I'll wait to see how that PR fails.
I'll file a PR to fix this function-bodies-are-empty check, we should land it first, so the add-py3.6 can pass CI before landing.
With the coffee machine example in the readme, we go from 3 state attributes (beans, water, lid), to just one (beans), to make the following examples simpler. I'm a little confused about the complex case though. It seems like with n independent boolean state attributes, you need 2^n automat states, as well as appropriate upon
s . So for the coffee machine example:
@_machine.state(initial=True)
def no_beans_no_water_open_lid(self):
pass
@_machine.state()
def no_beans_water_open_lid(self):
pass
@_machine.state()
def no_beans_no_water_closed_lid(self):
pass
@_machine.state()
def no_beans_water_closed_lid(self):
pass
@_machine.state()
def beans_no_water_open_lid(self):
pass
@_machine.state()
def beans_water_open_lid(self):
pass
@_machine.state()
def beans_no_water_closed_lid(self):
pass
@_machine.state()
def beans_water_closed_lid(self):
pass
Or am I just thinking about this completely wrong?
This will let pydoctor resolve L{automat}
.
I am working my may through the docs to discover and learn automat. I am finding the docs well done, and the library well documented. It seems, however, to mostly cater to the "already well informed gentleman"'s questions though!
What I think is missing, is a sort of a step by step process for creating a state machine, and using it.
I am willing to contribute and write such a process, but I am afraid I need a little bit of feedback, for calibration, and best practices:
I have written a state machine that models the scoring of a tennis game, linked as a gist
. If someone would provide feedback, and maybe guidance, I would spend the time to polish it, and attempt to produce a useful step by step tutorial to include in the documentation of the library.
A short list of what I think are bad ideas I used to solve the problems encountered:
from automat._core import NoTransition
to use in a try/except block to exit a loop in the client. The problem was: how can I know the process has ended (reach a winning state)._love_love
) that requires a state transition to the real initial state in order to allow for variable initialization - maybe there is a better way to init variables? or maybe there is an approach that does not require these variables?: @_machine.state(initial=True)
def _state__(self):
"""
State __
"""
@_machine.output()
def _output_on_initialize_game(self):
"""
State __ -> State love-love
"""
self._server_points = 0
self._receiver_points = 0
self.winner = None
self._set_increments()
self._show_score()
states = (_love_love, _15_love, _love_15, _love_30, _love_40, _30_love, _40_love, _15_all, _30_15, _15_30
, _40_15, _30_all, _15_40, _30_40, _40_30, _40_all, _deuce, _advantage_serve, _advantage_receive,
_server_wins, _receiver_wins)
for state in states:
state.upon(query_score, enter=state, outputs=[_output_on_query_score], collector=itemgetter(0))
--> maybe there is an automat.method
that does this.
--> or maybe there is a better approach.
--> attaching a these queries as a state transition from/to the same state is also making the rendering of the Tennis Game state machine visualization overly crowded by adding a transition to self to each state..
machine.input / machine.output
pairs to handle the transitions each time a player scored. SCORING = {0: 15, 15: 15, 30: 10}
SCORING.update({k: 1 for k in range(40, 80)})
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.