Watcher on an object defined by a ClassSelector

Hi everyone.

I am trying to set a watcher of a param of an underlying object of my class. I have provided code for the implementation but the idea is pretty simple:

class Styler
   def __init__:
        self.spin = 2

class Dashboard:
   styler=Styler()
   @param.depends('styler.spin', watch=True)
   def watch_spin

This is the general idea. How can I set up a watcher of this subobject ? And is it even possible ?

Here is the full exemple using the wonderful LoadingSpinner exemple provided in Awesome-Panels.

import random
import time

import holoviews as hv
import panel as pn
import param
from awesome_panel_extensions.site import site
from panel.io.loading import start_loading_spinner, stop_loading_spinner

from application.utils import config_spinner as config
hv.extension("bokeh")


class LoadingStyler(param.Parameterized):
    """A utility that can be used to select and style the loading spinner"""

    spinner_height = param.Integer(50, bounds=(1, 100))
    settings_panel = param.Parameter(doc="A panel containing the settings of the LoadingStyler")

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

        self.settings_panel = pn.Param(
            self,
            parameters=[
                "spinner_height",
            ],
        )

        self._update_style()



    @param.depends(
        "spinner_height", watch=True
    )
    def _update_style(self):
        print(self.spinner_height)





class LoadingApp(param.Parameterized):  # pylint: disable=too-many-instance-attributes
    """An app which show cases the loading spinner and enables the user to style it."""

    start_loading = param.Action(label="START LOADING", doc="Start the loading spinner")

    panels = param.List()

    view = param.Parameter()
    styler = param.ClassSelector(class_=LoadingStyler)

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

        self.start_loading = self._start_loading
        self.stop_loading = self._stop_loading

        self.update_plot = self._update_plot

        hv_plot = self._get_plot()
        self.hv_plot_panel = pn.pane.HoloViews(hv_plot, min_height=300, sizing_mode="stretch_both")
        self.styler = LoadingStyler(name="Styles")

        self.panels = [
            self.hv_plot_panel,
        ]

        self.settings_panel = pn.Column(
            pn.pane.Markdown("## Settings"),
            pn.Param(
                self,
                parameters=[
                    "start_loading",
                ],
                show_name=False,
            ),
            self.styler.settings_panel,
            sizing_mode="stretch_width",
        )
        self.main = pn.Column(*self.panels, sizing_mode="stretch_both")
        self.view = pn.Row(self.settings_panel, self.main)

    def _start_loading(self, *_):
        self.loading = True

    def _stop_loading(self, *_):
        self.loading = False

    @staticmethod
    def _get_plot():
        xxs = ["one", "two", "tree", "four", "five", "six"]
        data = []
        for item in xxs:
            data.append((item, random.randint(0, 10)))
        return hv.Bars(data, hv.Dimension("Car occupants"), "Count").opts(
            height=500,
            responsive=True,
            color="red",
        )

    def _update_plot(self, *_):
        self.loading = True
        time.sleep(self.sleep)
        self.hv_plot_panel.object = self._get_plot()
        self.loading = False



    def _start_loading_spinner(self, *_):
        # Only nescessary in this demo app to be able to toggle show_shared_spinner
        self._stop_loading_spinner()
        if self.show_shared_spinner:
            start_loading_spinner(self.main)
        else:
            for panel in self.panels:
                start_loading_spinner(panel)

    def _stop_loading_spinner(self, *_):
        stop_loading_spinner(self.main)
        for panel in self.panels:
            stop_loading_spinner(panel)

    # @param.depends(
    #     "styler.spinner_height", watch=True
    # )
    # def _update_style(self):
    #     print(self.spinner_height)


def view():
    """Returns the app in a Template"""
    pn.config.sizing_mode = "stretch_width"
    template = pn.template.FastListTemplate(title="Loading Spinners")
    app = LoadingApp(name="Loading Spinner App")
    template.sidebar[:] = [app.settings_panel]
    template.main[:] = [app.main]
    if not issubclass(template.theme, pn.template.base.DefaultTheme):
        app.styler.background_rgb = (0, 0, 0)
    return template

view().servable()

1 Like

Maybe this thread is what your looking for?

https://discourse.holoviz.org/t/can-a-paramterized-class-react-to-parameters-outside-of-itself/2067/5

2 Likes

It definitely should work. But when I try it in my use case I have an error when doing the super init 'NoneType' object has no attribute 'param'
I was wondering how to solve this issue

Okay I found it. I need to call LoadingApp(stryler) and I cannot define it in the init it seems

1 Like

Hi @Axeldnahcram

I believe you can also instantiate in the __init__ but NOT like

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

    self.styler=Styler()

instead you should do something like

def __init__(self, styler=None, **params):
    if not styler:
        styler=Styler()
    super().__init__(styler=styler, **params)
1 Like

I’d give Param 1.12.0 a try which resolved a lot of issues depending on sub-objects. Took me a whole to days but sub-object dependencies are now resolved dynamically when they become available.

2 Likes

Both of your answers are just exactly what I needed, thank you very much.

I will try to dive deep a bit more into how param works. Still have some troubles wrapping my head into some concepts with param. Like for instance if I do the following:

class LoadingStyler(param.Parameterized):
    settings_panel = param.Parameter(doc="A panel containing the settings of the LoadingStyler")

    def __init__(self, **params):
        self.spinner_height = param.Integer(50, bounds=(1, 100))
        super().__init__(**params)

        self.settings_panel = pn.Param(
            self,
            parameters=[
                "spinner_height",
            ],
        )

        self._update_style()

    @param.depends(
        "spinner_height", watch=True
    )
    def _update_style(self):
        print(self.spinner_height)

Spinner_height will not be available and I have to declare spinner height outside of init. I think this has to do with the orig_vars = cls.__dict__.copy() that is performed within param but I might be completely off base here.

Yeah, parameters have to be declared on the class or be added using the .add_parameter API, you can’t just add them inside a constructor like that. You really have to have pretty deep understanding of Python, specifically about descriptors and metaclasses to fully understand Param but for most users that isn’t necessary.

The Parameterized metaclass does a bunch of setup during class creation which ensures that a parameter attribute acts like its value, while also ensuring that when you set a parameter value it goes through all of the parameter’s validation logic. If you simple assign it like you are doing in the constructor then self.spinner_height will simply be an unbound Parameter which is pretty useless.

If you haven’t already seen it do check out the newly updated Param docs. Only took @jbednar about 20 years to write those :slight_smile:

3 Likes

Merely 18 years! And to be fair, I only really started writing in 2020; the rest of the time I was just “fixing to” do it, as they say here in Texas. :slight_smile:

3 Likes

Thank you all for the help. I indeed saw the webinar about why param was the best library and I think it is an understatement.

1 Like