GithubHelp home page GithubHelp logo

automat's People

Contributors

altendky avatar chellygel avatar cjmayo avatar cjwatson avatar daa avatar daniel-at-github avatar derwolfe avatar dol-sen avatar dowwie avatar dsanders11 avatar eclipseo avatar exarkun avatar fmoor avatar gliptak avatar glyph avatar guy4261 avatar hugovk avatar hynek avatar jameshilliard avatar khorn avatar markrwilliams avatar moshez avatar neirbowj avatar offby1 avatar reblochonmasque avatar rodrigc avatar ronnypfannschmidt avatar shadchin avatar termim avatar warner avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

automat's Issues

Conditional State Transitions?

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.

Tests fail if gaphviz is installed but not Twisted

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

How to handle errors occurring during outputs?

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 raise syntax error in visualize

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.

add "feedback" API - outputs that produce inputs have problems

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.

Well _actually

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.

bodies of Inputs are executed, but shouldn't be

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:

  • their names, which can then appear as nicely-accessible attributes of the hosting class, making all the .upon() statements easy to write
  • a place to add arguments things like (initial=True)

and Inputs are methods only for:

  • their names, as above
  • their names, to be accessible from outside the class, so other "upstream" objects can invoke target.inputXYZ(args) to effect a state transition
  • their method signatures, so they can be compared against Outputs

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

Guidance on creating multiple similar outputs with variations in return value

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.

broken with attrs 17.1.0

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

Not all outputs get run if the collector does not exhaust the generator.

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.

Available Affordances?

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?

Help with output Exception and output with callback on statemachine

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 :

  1. Let’s imagine that :
  • The inputs are done by the user through a GUI (connect to the remote sensor, start remote sensor, start measurements, etc…)
  • We have an output class RemoteSensor to manage the communication with the remote sensor
  1. How is it plan to manage Exception ?
  • For example, the user trigger the input “Connect to the remote sensor”. The associate output method “connect” of RemoteSensor class does the job by opening the connection. If the establishment of the connection fails (ie. Exception throw), I don’t want to go in the next state which is “Remote sensor stopped”. Is there any way to do that ?
  1. Is it possible (and logical) to have an output class which does input too ?
  • For example, my RemoteSensor class manage the connection with the remote sensor. The user triggers the input “Start measurements” through the GUI , so the output method “startMeasurement” of RemoteSensor Class sends the order to the remote sensor to start measuring. The state machine goes into “Remote Sensor Measuring” state. Now, I had to wait until the measuring process finish. This information is gotten from the remote sensor by asking it each second. I want the RemoteSensor class manage this task. However, I don’t know if the design pattern is correct.
    • What I imagine : create a second output in the state machine call “waitForEndingMeasurements” which call a specific method of the RemoteSensor class with a call back to the state machine input
  1. Have you plan something to do automatic GUI (web based if possible) ?
  • I plug Automat with twisted http server and return JSON state machine description with possible states, current state and inputs available function of current state to product HTML5 GUI to :
    • Display information,
    • Allow the user to make inputs
  • In the future, maybe I will associate it with the graphviz svg export of the state machine.

I’m ready to contribute to the project if necessary.

Thanks a lot for your help.

Thibaud

Test helper that asserts all transitions occurred

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

Sending an invalid input causes KeyError to be raised

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.

Mechanism for marking states as explicitly terminal

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.

Tracing API

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.

Shortcuts for choosing specific outputs

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?

New release

@glyph Hey man! Could you please push to PyPI? The last release was May 16, 2017.

inheritance - object oriented machines

I've noticed problems while trying to develop machines using automat that inherit from base machine classes.

  1. @output's defined in base methods misinterpret the fact that the super() is called in the following way:
    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.
  2. It is not obvious how to define transitions using states and outputs defined in base classes. It appears that states are attached to the base class before instantiation as methodicStates but how do you reference them? Outputs seem to be instantiated upon object creation.

Test helper that asserts a given transition occurred

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

assert that transitions are unique

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.

typos/errors in docs

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.

Use of setup_requires breaks offline build process

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/

Not all outputs want the arguments to the input.

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

Make self-loops easier

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.

Programmatically create/manipulate state machines

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?

Benchmark suite.

Automat should have a benchmark suite so we can measure performance overhead.

tests fail on py3.6

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

proliferation of states

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 upons . 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?

Examples, docs, best practices

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:

  • use of 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).
  • Creating a "pre-initial state" (instead of starting at _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()
  • having to build a list of states to assign a query to each:
    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..

  • Having to initiate class constants in order to mimic the passing of parameters that I could not quite figure out. It seemed to be a way to prevent a multiplication of 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)})

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.