pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.68k stars 530 forks source link

Async prepare functions #395

Closed TanjaBayer closed 4 years ago

TanjaBayer commented 4 years ago

Hi,

I looked through old issues but did not find a real solution to that, and even not if async prepare functions are supported at all or not.

Minimal example of what I try to achieve:

class Evaluator:
  async def my_prepare_function():
    await asyncio.sleep(10)

states = ['Start', 'End']

transitions = { 'trigger': 'run', source': '*', dest': 'End', 'prepare': 'my_prepare_function'}

_eval = Evaluator()

 machine = Machine(_eval, states, transitions=transitions, initial='Start')

_eval.run()

assert _eval.state =='End'

The my_prepare_function in reality contains some http requests to services, I need to have the result from before going to the next state.

Before using an async function _eval.run() was waiting for the my_prepare_function to finish before going continuing, but now, as the function is ansync it does no longer wait and _eval.state did not yet change when trying to assert the state.

Is this in general possible with pytransition?
Or is this not at all supported. I can also switch back to using plain threads and waiting for all of them to finish in the my_prepare_function(), but I think using async would be the more elegant way.

Thanks in advance

TanjaBayer commented 4 years ago

After looking a bit closer I found that comment: https://github.com/pytransitions/transitions/issues/259#issuecomment-482139389

But I tried to import transitions.extensions.asyncio and it seems like it isn't there anymore. So did you remove it?

aleneum commented 4 years ago

Hi @TanjaBayer,

haven't had a look at your specific issue yet but just want to tell you that dev-async had been merged into the 0.8 dev branch. If you checkout that one you will find the asyncio extension module. I am waiting for some feedack about a community contributed feature and hope to release 0.8 in the next couple of days on PyPI.

TanjaBayer commented 4 years ago

Hey,

thanks for the feedback, I will try to check out the dev branch. And will give some feedback if it works or not, once I had time to test it.

aleneum commented 4 years ago

Considering your specific issue: Event triggers have to be awaited. You could (a) wrap your machine code into a function which is passed to the asyncio event loop like this:

from transitions.extensions.asyncio import AsyncMachine
import asyncio

class Evaluator:

  async def my_prepare_function(self):
    await asyncio.sleep(1)

states = ['Start', 'End']

transitions = [{ 'trigger': 'run', 'source': '*', 'dest': 'End', 'prepare': 'my_prepare_function'}]

_eval = Evaluator()

machine = AsyncMachine(_eval, states, transitions=transitions, initial='Start')

async def start():
    await _eval.run()

asyncio.get_event_loop().run_until_complete(start())

assert _eval.state =='End'

You could also (b) pass the trigger to the loop directly:

asyncio.get_event_loop().run_until_complete(_eval.run())

Whether (a) or (b) suits you better depends on your particular problem I guess.

TanjaBayer commented 4 years ago

Yes you are right, I forgot that one in my example above.

TanjaBayer commented 4 years ago

So I just checked it with dev-0.8 branch.

In my tests it is working fine:

from transitions.extensions.asyncio import AsyncMachine
import asyncio

async def test_finish_state_is_reached_after_executing_run(_eval):
        await _eval.run()
        assert _eval.state == EvaluationStates.FINISHED

And in my main programm an aiohttp client I use it like that:

In the rest-endpoint callback function:

asyncio.ensure_future(s_h.loop(asyncio.get_event_loop()))

The Instance function which is called in the endpoint:

async def loop(self, loop):
     while self._eval.state != EvaluationStates.FINISHED:
             await self._eval.run()

So no problems so far with that, hence closing that issue, thanks for the help