Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyscript doesn't run callback in transitions-based state machine #137

Closed
renede opened this issue Jan 4, 2021 · 6 comments
Closed

pyscript doesn't run callback in transitions-based state machine #137

renede opened this issue Jan 4, 2021 · 6 comments

Comments

@renede
Copy link

renede commented Jan 4, 2021

Dear pyscript experts,
I am building a program that would benefit from using a state machine.
I tried to implement one, using the transitions package. The machine works, but callbacks which should be fired on a state transition don't. I tested the same code in 'regular' python 3.8, and there it worked.
The simplest code for reproducing:

from transitions import Machine, State
class C(object):
    def tst(self): log.info('Testing')
c = C()
states=[State('A', on_exit=['tst']),'B']
trans=[['go','A','B'],['back','B','A']]
m = Machine(c, states=states, transitions=trans, initial='A')
before = c.state
c.go()  # should call the callback and print 'Testing'
after = c.state
log.info(f'Before = {before}, After = {after}')
c.tst() # just check the callback

The result in jupyter:

Before = A, After = B
Testing

If I run almost the same code (just replacing log.info( ) by print( )) in jupyter with the python 3 kernel I get:

Testing
Before = A, After = B
Testing

In the pyscript case the function c.tst( ) is not called by the machine (the first 'Testing' missing), but that function does work as shown by the 'Testing' as the last line in the response.
I got the same result when running the test as a service in HA.
Is this a known limitation of pyscript, a bug, or am I doing something wrong?

@craigbarratt
Copy link
Member

craigbarratt commented Jan 4, 2021

All functions in pyscript are (async) coroutines, not regular Python functions. So functions like map or transitions that require a function for callback won't work, since they are expecting a regular function, not a coroutine.

In GitHub master there is an experimental feature that allows you to compile a function (so it's a regular Python function) using the @pyscript_compile decorator. The drawback is that the function then can't contain any pyscript-specific features.

Here's your code slightly modified to use @pyscript_compile. Since the tst function can't use log.info, it increments a class variable instead to confirm it gets called.

Also, see issue #71 and this transitions GitHub issue.

from transitions import Machine, State
class C(object):
    tst_cnt = 0
    
    @pyscript_compile
    def tst(self): 
        self.tst_cnt += 1
c = C()
states=[State('A', on_exit=['tst']),'B']
trans=[['go','A','B'],['back','B','A']]
m = Machine(c, states=states, transitions=trans, initial='A')
before = c.state
c.go()  # should call the callback and print 'Testing'
after = c.state
log.info(f'Before = {before}, After = {after}')
c.tst() # just check the callback
log.info(f'tst_cnt = {c.tst_cnt}')

This prints:

Before = A, After = B
tst_cnt = 2

@craigbarratt
Copy link
Member

craigbarratt commented Jan 4, 2021

It seems that transitions now supports async callbacks, starting with version 0.8.0 (see the "Extensions" section in the documentation). So a better solution than @pyscript_compile would be to use its AsyncMachine support, and then pyscript callbacks should work as intended. Then you should be able to use pyscript features in the callbacks.

@renede
Copy link
Author

renede commented Jan 4, 2021

Dear Craig,

Thanks for your very quick response!

I wasn't aware of this async limitation. The @pyscript_compile option is probably not practical for me, since the callbacks will need to do a lot of interaction with HA- and pyscript state variables. I will look into the transitions.extensions.asyncio stuff,
and I will need to study asyncio in more detail. One of the reasons for me to start working with pyscript was your very useful task.executor( ) function that shielded me from asyncio when making a few web requests calls. I first tried AppDaemon, where I got stuck with a proliferation of async functions.
Knowing the cause and a possible solution to my problem, I will close the issue. Thanks again.
René.

@renede renede closed this as completed Jan 4, 2021
@renede renede reopened this Jan 5, 2021
@renede
Copy link
Author

renede commented Jan 5, 2021

Re-opening issue:
I tried the example with ``transitions.extensions.asyncio``` in jupyter, first with the python 3.8 kernel:

from transitions.extensions.asyncio import AsyncMachine
import asyncio
import time

class AsyncModel:

    def prepare_model(self):
        print("I am synchronous.")
        self.start_time = time.time()

    async def before_change(self):
        print("I am asynchronous and will block now for 100 milliseconds.")
        await asyncio.sleep(0.1)
        print("I am done waiting.")

    def sync_before_change(self):
        print("I am synchronous and will block the event loop (what I probably shouldn't)")
        time.sleep(0.1)
        print("I am done waiting synchronously.")

    def after_change(self):
        print(f"I am synchronous again. Execution took {int((time.time() - self.start_time) * 1000)} ms.")

transition = dict(trigger="start", source="Start", dest="Done", prepare="prepare_model",
                  before=["before_change"] * 5 + ["sync_before_change"],
                  after="after_change")  # execute before function in asynchronously 5 times
model = AsyncModel()
machine = AsyncMachine(model, states=["Start", "Done"], transitions=[transition], initial='Start')

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

Result: RuntimeError: This event loop is already running
This seems to be due to jupyter already running its own eventloop. Found that it works by replacing the last line into
await model.start(), which gives the expected result:

I am synchronous.
I am asynchronous and will block now for 100 milliseconds.
I am asynchronous and will block now for 100 milliseconds.
I am asynchronous and will block now for 100 milliseconds.
I am asynchronous and will block now for 100 milliseconds.
I am asynchronous and will block now for 100 milliseconds.
I am synchronous and will block the event loop (what I probably shouldn't)
I am done waiting synchronously.
I am done waiting.
I am done waiting.
I am done waiting.
I am done waiting.
I am done waiting.
I am synchronous again. Execution took 100 ms. 

Now with the Hass kernel, the original call asyncio.get_event_loop().run_until_complete(model.start())
gives the same runtime error as before, but with await model.start() that worked with the python kernel, I get an unexpected (and useless) result:

from transitions.extensions.asyncio import AsyncMachine
import asyncio
import time

class AsyncModel:

    def prepare_model(self):
        log.info("I am synchronous.")
        self.start_time = time.time()

    async def before_change(self):
        log.info("I am asynchronous and will block now for 100 milliseconds.")
        await asyncio.sleep(0.1)
        log.info("I am done waiting.")

    def sync_before_change(self):
        log.info("I am synchronous and will block the event loop (what I probably shouldn't)")
        time.sleep(0.1)
        log.info("I am done waiting synchronously.")

    def after_change(self):
        log.info(f"I am synchronous again. Execution took {int((time.time() - self.start_time) * 1000)} ms.")

transition = dict(trigger="start", source="Start", dest="Done", prepare="prepare_model",
                  before=["before_change"] * 5 + ["sync_before_change"],
                  after="after_change")  # execute before function in asynchronously 5 times
model = AsyncModel()
machine = AsyncMachine(model, states=["Start", "Done"], transitions=[transition], initial='Start')

await model.start()

Result: <coroutine object AsyncEvent.trigger at 0xa6e426a8>
I have no idea what I can do about this.

@craigbarratt
Copy link
Member

Your test exposed a couple of bugs related to calling pyscript functions from native Python async code. They should be fixed with the latest commit.

Note that in your example, all four class methods are async in pyscript (the async keyword in the function declaration makes no difference). If you really need some class methods to be synchronous, you need to use the @pyscript_compile decorator and then pyscript-specific features (including print) will not be available inside those functions.

@renede
Copy link
Author

renede commented Jan 9, 2021

Sorry for my slow response.
Thanks for addressing this issue.
I'm find the async stuff a bit confusing, so I decided to abandon the transitions package and build my own state machine using a state variable, some state_setting functions and a lot of if...elif... statements. Got it working now.
Thanks again for your kind attention and the great work of creating and maintaining pyscript!
Closing the issue.

@renede renede closed this as completed Jan 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants