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

pn.state.onload results in "Uncaught Error: reference pXXXX isn't known" starting in Panel1.0 / Bokeh 3.0 #6019

Closed
tomascsantos opened this issue Dec 8, 2023 · 2 comments · Fixed by #6028
Milestone

Comments

@tomascsantos
Copy link

To begin, thank you very much to the team for all the hard work. I'm working on upgrading to panel1.0 and excited for the new features that have been developed!

ALL software version info

bokeh==3.3.1
panel==1.3.4
param==2.0.1
python==3.9.16

Description of expected behavior and the observed behavior

I apologize in advance for the large "minimum viable example", I tried to squish it down as much as possible.

For the example below, the expected behavior is that onload, the application should set the "default" value, then the boxes should toggle their color from red to green and the webpage should load with one box that is green.

This is shown in the video below using the same code but with the following software versions:
bokeh==2.4.3
param==1.12.3
panel==0.14.4
python==3.7.11

(the toggling happens so fast sometimes it's not visible at all)

py37_success.mov

When I run the same code using the version reported at the top of the file, this is the result:

py39_bug.mov

This is problematic, because in the "real" application it results in the viewer being stuck in the "loading" pane instead of in the "LOADED" pane.

Complete, minimal, self-contained example code that reproduces the issue

"""A minimum reproducible example of a new startup bug"""
from time import sleep
from typing import List
import asyncio
import panel as pn
import param


class BoxMachine(param.Parameterized):
    """A simple state machine. In reality this would be the async data fetching mechanism"""
    box_name = param.String(default='foo')
    state = param.Selector(objects=['red', 'green'])

    async def delayed_color_change(self):
        """Mock logic that modifies the state of the Machine"""
        for i in range(3):
            sleep(.1)
            self.state = 'green' if i % 2 == 0 else 'red'


class ColorChangingBoxViewer(param.Parameterized):
    """A viewer which transitions views based on the state machine.

    In reality we'd transition between a pane displaying progress bar and some LOADED state
    """

    box: BoxMachine = param.ClassSelector(class_=BoxMachine)

    def __init__(self, box, **params) -> None:
        super().__init__(box=box, **params)

        self.container = pn.Column()
        # Using legacy background syntax for backwards compatibility
        self.red = pn.Column(f'{self.box.box_name} in red', background='red')
        self.green = pn.Column(f'{self.box.box_name} in green', background='green')

        self.box.param.watch(self._update_state, 'state')
        self._update_state()

    def _update_state(self, event=None):
        """Update the viewer state based on the state machine"""
        if self.box.state == 'red':
            self.container.objects = [self.red]
        elif self.box.state == 'green':
            self.container.objects = [self.green]


class ManyBoxViewer(param.Parameterized):
    """Holds multiple boxes, and a MultiChoice widget allows users to select visible boxes"""
    box_lst: List[BoxMachine] = param.List()
    active_boxes: List[str] = param.ListSelector(default=[])

    def __init__(self, box_lst, **params) -> None:
        super().__init__(box_lst=box_lst, **params)
        self.param.active_boxes.objects = [b.box_name for b in self.box_lst]
        self.box_viewers = {}

        self.choice = pn.widgets.MultiChoice.from_param(self.param.active_boxes)
        self.container = pn.Column()

        self.param.watch(self._update_boxes, 'active_boxes')
        self._update_boxes()

    def _update_boxes(self, event=None):
        """Update the visible list of boxes based on the user choices"""

        viewers = []
        for b in self.box_lst:
            if b.box_name in self.active_boxes:
                viewers.append(
                    ColorChangingBoxViewer(b).container)  # typically viewers are memoized

        self.container.objects = [
            self.choice,
            *viewers,
        ]


class App():
    """Tie the components together"""

    def __init__(self, **params):
        super().__init__(**params)

        self.many_box = ManyBoxViewer(
            box_lst=[BoxMachine(box_name=n) for n in ['hello', 'world', 'foo', 'bar']])

        self.template = pn.Column('my_color_changers', self.many_box.container)

        self.many_box.param.watch(self.update_colors, 'active_boxes')
        pn.state.onload(self.onload)

    async def onload(self, *args):
        """When the application loads, set the defaults (usually loaded from URL)

        This is the critical line that seems to break things... when it's commented out,
        the application functions exactly as expected, but when we load initial values something
        goes wrong and results in a bokeh error about an unknown reference. 
        """
        self.many_box.active_boxes = ['hello']

    def update_colors(self, event=None):
        """When the list of boxes change, toggle the colors"""
        for box in self.many_box.box_lst:
            asyncio.create_task(box.delayed_color_change())

App().template.servable()

As mentioned in the code snippet above, when I comment out the default value of the active boxes, the application functions (mostly) as expected, shown below. I still see a bit of a strange delay in the text which I don't fully understand but I could live with that.

no_default_py39.mov

I'm continuing to search for solutions on my end, but was hoping for suggestions/support from the community as I work on it. This is a blocker for using pane1.0 so I'm keen to get things working and enjoy all the benefits of bokeh3.0 and panel1.0.

Cheers,
Tomas

@philippjfr
Copy link
Member

Seems like a race condition that happens only if the onload callback is async. Will see if I can track it down.

@tomascsantos
Copy link
Author

Amazing thank you for the super quick fix!

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

Successfully merging a pull request may close this issue.

2 participants