Unable to move from functions to classes using the reactive API, linking broken

Goal

So I’m trying to both build an app with a couple of goals.

  1. I need it :slight_smile:
  2. I want folks who don’t know panel very well to be able to understand and maintain it
  3. I think it would make a good blog post, improve docs to show how you would go from experimenting to something you would actually deploy

Anyway, I’m really liking the reactive API of Panel as something that is fairly straightforward to understand and explain to folks but I’m having trouble moving from the functional approach to a working Class based approach. I have a small example below that demonstrates the issue.

any pointers are greatly appreciated.

Caveats:

  • avoid using Param directly since it would be one more thing to explain. just use widgets.
  • minimal changes between the functional and class approach

Function Approach (this works):

import panel as pn
pn.extension()

_countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}
    
continent = pn.widgets.AutocompleteInput(
                name='Continent',
                options=list(_countries.keys()),
                case_sensitive=False,
                placeholder='Enter Continent',
            )
country = pn.widgets.Select(name='Country')

@pn.depends(continent, watch=True)
def _update_countries(continent):
    countries = _countries[continent]
    country.options = countries

pn.Row(continent, country)

Class Based Approach (does not work)

  • (minor) The Jupyter cell does not expand vertically to show the options in the Autocompleter
  • (critical) country list does not update on selection of continent. I’ve tried various ways of writing the class. I saw some examples using pn.viewer.Viewer but couldn’t find any docs so the example below is cobbled together from various sources.
import panel as pn
pn.extension()

class Viewer(pn.viewable.Viewer):
    _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}
    
    continent = pn.widgets.AutocompleteInput(
                name='Continent',
                options=list(_countries.keys()),
                case_sensitive=False,
                placeholder='Enter Continent',
            )
    country = pn.widgets.Select(name='Country')
    
    @pn.depends('continent', watch=True)
    def _update_countries(self):
        countries = self._countries[self.continent]
        self.country.options = countries
            
    def __panel__(self):
        return pn.Row(self.continent, self.country)

view = Viewer()
view

I’ve tried a couple of different ways of passing things to pn.depends in the Class, i.e. "continent" vs continent vs continent.value etc but it doesn’t seem like Panel is watching for the changes.

Ok I have a working version now.

essentially I had to make two changes:

  • Change from pn.depends('continent') to pn.depends('continent.param')
  • Autocompleter widget is firing the update on every keypress instead of waiting for an actual selection. This causes a KeyError since A is not in the dict if for example I try and type Africa. I am working around this by handling the error in the update function.

So from the point of view of my second goal, i.e. minimizing pain for new users of panel,

  • is there a way to make pn.depends to not require using continent.param
  • The Autocompleter widget has a different behavior inside the Class, any idea why?
class Viewer(pn.viewable.Viewer):
    _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}
    
    continent = pn.widgets.AutocompleteInput(
                name='Continent',
                options=list(_countries.keys()),
                case_sensitive=False,
                placeholder='Enter Continent',
            )
    country = pn.widgets.Select(name='Country')
    
    @pn.depends('continent.param', watch=True)
    def _update_countries(self):
        countries = self._countries.get(self.continent.value, [])
        self.country.options = countries
            
    def __panel__(self):
        return pn.Row(self.continent, self.country)

The saga continues:

edit: just the presence of __init__() breaks things again:

class Viewer(pn.viewable.Viewer):
    _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}

    continent = pn.widgets.Select(options=list(_countries.keys()), name='Country')
    country = pn.widgets.Select(name='Country')
    
    def __init__(self):
        return
    
    @pn.depends('continent.param', watch=True)
    def _update_countries(self):
        countries = self._countries.get(self.continent.value, [])
        self.country.options = countries
            
    def __panel__(self):
        return pn.Row(self.continent, self.country)

Ok I solved my own problem. Sorry for the noise, hope this is useful for others:

need to use def __init__(self, **params) and call super().__init__(**params) as mentioned here:

https://pyviz-dev.github.io/panel/user_guide/Custom_Components.html

Working Version

class Viewer(pn.viewable.Viewer): 

    continent = pn.widgets.Select(name='Continent')
    country = pn.widgets.Select(name='Country')
    
    def __init__(self, countries, **params):
        self._countries = countries
        self.continent.options = list(self._countries.keys())  
        super().__init__(**params)        
    
    @pn.depends('continent.param', watch=True, on_init=True)
    def _update_countries(self):
        countries = self._countries.get(self.continent.value, [])
        self.country.options = countries
            
    def __panel__(self):
        return pn.Row(self.continent, self.country)

Viewer(
countries={'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}
)

3 Likes

Just a small note if you don’t want to share the state of the widgets between different instances, you can do something like this:

import panel as pn
import param

class FindCountry(pn.viewable.Viewer):
    continent = param.Selector()
    country = param.Selector()

    def __init__(self, countries, **params):
        self._countries = countries
        super().__init__(**params)
        self.param.continent.objects = list(self._countries)
        self.continent = next(iter(self._countries))

    @param.depends("continent", watch=True)
    def _update_countries(self):
        countries = self._countries.get(self.continent, [])
        self.param.country.objects = countries
        self.country = next(iter(countries))

    def __panel__(self):
        return pn.Param(self.param, default_layout=pn.Row, show_name=False)


FindCountry(countries=countries)

Shared widgets between instances:

Independent widgets between instances:

1 Like

@Hoxbro So how do I use the nicer widgets when also using param objects. i.e. it is unclear to me how to use a param object with say Tabulator or the Autocomplete widget or the datepicker.

i.e. continent = param.Selector() but I actually want to use the fancier autocomplete widget.

You can just specify the “widgets” parameter like below. See here for more detail:
https://panel.holoviz.org/user_guide/Param.html#custom-widgets


    def __panel__(self):
        return pn.Param(
            self.param, 
            widgets={
                'continent': pn.widgets.AutocompleteInput,
                'country': {
                    'widget_type': pn.widgets.AutocompleteInput, 
                    'placeholder': 'Find a country...',
                    'case_sensitive': False
                }
            },
            default_layout=pn.Row, 
            show_name=False
        )

2 Likes