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

Implement support for reactive generator functions #5019

Merged
merged 9 commits into from
Jun 4, 2023
Merged

Conversation

philippjfr
Copy link
Member

@philippjfr philippjfr commented Jun 3, 2023

In looking at #5016 I realized that the only way to write reactive expressions with multiple steps in a natural way would be using generators. So I had a go at implementing support for them and now I'm in love, here's a simplified version of the app in #5016:

Asynchronous Generator

import asyncio
import panel as pn

pn.extension(sizing_mode="stretch_width", template='bootstrap')

run = pn.widgets.Button(name="Press to run calculation")

async def runner(running):
    if not running:
        yield "Calculation did not run yet"
        return
    yield pn.Column(
        pn.indicators.LoadingSpinner(value=True, width=25, height=25),
        "Running...Please wait!",
    )
    time_start = time.perf_counter()
    await asyncio.sleep(1.5)
    time_end = time.perf_counter()
    yield f"""Done!
    The function took {time_end - time_start:1.1f} seconds to complete"""

pn.Column(
    "# Calculation Runner", run, pn.bind(runner, run)
).servable()

Classic Generator

import time
import panel as pn

pn.extension(sizing_mode="stretch_width", template='bootstrap')

run = pn.widgets.Button(name="Press to run calculation")

def runner(running):
    if not running:
        yield "Calculation did not run yet"
        return
    yield pn.Column(
        pn.indicators.LoadingSpinner(value=True, width=25, height=25),
        "Running...Please wait!",
    )
    time_start = time.perf_counter()
    time.sleep(1.5)
    time_end = time.perf_counter()
    yield f"""Done!
    The function took {time_end - time_start:1.1f} seconds to complete"""

pn.Column(
    "# Calculation Runner", run, pn.bind(runner, run)
).servable()

generator

Streaming example

import asyncio
import panel as pn
import random

pn.extension()

@pn.depends()
async def stream():
    while True:
        await asyncio.sleep(0.1)
        yield random.randint(0, 100)
        
pn.indicators.Number(value=stream, name='Random')

random_generator

@philippjfr philippjfr added type: enhancement Minor feature or improvement to an existing feature api Related to Panel's API labels Jun 3, 2023
@codecov
Copy link

codecov bot commented Jun 3, 2023

Codecov Report

Merging #5019 (089f664) into main (480e6b0) will increase coverage by 10.17%.
The diff coverage is 91.37%.

@@             Coverage Diff             @@
##             main    #5019       +/-   ##
===========================================
+ Coverage   73.40%   83.57%   +10.17%     
===========================================
  Files         271      271               
  Lines       38361    38586      +225     
===========================================
+ Hits        28157    32247     +4090     
+ Misses      10204     6339     -3865     
Flag Coverage Δ
ui-tests 40.72% <20.00%> (?)
unitexamples-tests 73.54% <90.98%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
panel/depends.py 75.67% <76.19%> (+0.93%) ⬆️
panel/reactive.py 80.98% <81.69%> (+0.87%) ⬆️
panel/param.py 87.57% <81.81%> (+0.81%) ⬆️
panel/io/convert.py 72.18% <100.00%> (+46.99%) ⬆️
panel/layout/base.py 91.84% <100.00%> (+0.01%) ⬆️
panel/tests/test_param.py 99.73% <100.00%> (+0.01%) ⬆️
panel/tests/test_reactive.py 99.32% <100.00%> (+0.10%) ⬆️

... and 59 files with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jun 3, 2023

The streaming example is extremely powerful. The Async and Classic generators too. ❤️

The downside is that we have to teach our users yield. 🤔 I think its pythonic though and where Python is heading.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jun 3, 2023

Questions

  • Does pn.bind(runner, run) run twice when I click the run button? Has it always done that?
  • Does this work with other widgets like an IntSlider?
  • Does the streaming example perform nicely if you have 100 streaming generators in one app and 2-100 simultaneous users? I'm asking because I've found that its not performant to setup 100 period callbacks - its better to update 100 times in one. It would be so great not to have to couple the streaming components.

panel/depends.py Outdated Show resolved Hide resolved
@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jun 3, 2023

Other use cases for generator functions

I added another use case for generator functions in Panel that I believe should be supported as well. Please consider this in the context of this PR. Thanks. #5020

@philippjfr
Copy link
Member Author

Does pn.bind(runner, run) run twice when I click the run button? Has it always done that?

It runs once but if you bind a Button to a function it'll run the first time with a value of False and on every click it'll run with a value of True.

Does this work with other widgets like an IntSlider?

Absolutely, it works just like a regular function with bind.

Does the streaming example perform nicely if you have 100 streaming generators? I'm asking because I've found that its not performant to setup 100 period callbacks - its better to update 100 times in one. It would be so great not to have to couple the streaming components.

Absolutely, this should be significantly more lightweight than periodic callbacks, here's 100 instances of the streaming generator

many_generators

@philippjfr
Copy link
Member Author

Okay, feature-wise this is now done. Last item was implementing synchronization primitives to ensure the async generators were updated when a dependency changes, e.g. we make the timeout dynamic:

import asyncio
import panel as pn
import random

pn.extension()

timeout = pn.widgets.FloatSlider(start=0.1, end=1.0, value=1, name='Timeout')

@pn.depends(timeout)
async def stream(timeout):
    while True:
        await asyncio.sleep(timeout)
        yield random.randint(0, 100)

pn.Column(
    timeout,
    pn.FlexBox(*(pn.indicators.Number(value=stream, name='Random', font_size='12pt') for _ in range(200)))
).servable()

async_gen_deps

@MarcSkovMadsen MarcSkovMadsen mentioned this pull request Jun 3, 2023
17 tasks
@philippjfr philippjfr merged commit 47d02eb into main Jun 4, 2023
13 of 15 checks passed
@philippjfr philippjfr deleted the generator_funcs branch June 4, 2023 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Related to Panel's API type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants