param icon indicating copy to clipboard operation
param copied to clipboard

Cryptic error message when forgetting to call super().__init__ on Parameterized

Open dwr-psandhu opened this issue 4 years ago • 3 comments

ALL software version info

Holoviews 1.13.5 Param 1.10.1 Panel 0.10.3

Description of expected behavior and the observed behavior

Extending a Parameterized class without calling init results in a cryptic message. See below It would be nice to have a better message to remind user to call super().init(**kwargs)

AttributeError: 'TestStreamParam' object has no attribute '_param_watchers'

See discussion on discourse

Minimal working example

import param
import panel as pn

class TestParam(param.Parameterized):
    onoff = param.Boolean()
    def __init__(self, initial=False):
        onoff = initial
        
    def view(self):
        print(self.onoff)

Using it like this gives the error

tp = TestParam()
pn.Row(tp.param)

See stack trace below

Fix

The fix is quite simple, Just add **kwargs to the init method and call the super

class TestParam(param.Parameterized):
    onoff = param.Boolean()
    def __init__(self, initial=False, **kwargs):
        super().__init__(**kwargs)
        onoff = initial
....

Stack traceback and/or browser JavaScript console output

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-9-6053fe58e559> in <module>
      1 tsp=TestStreamParam()
      2 
----> 3 pn.Row(tsp.param)

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\layout\base.py in __init__(self, *objects, **params)
    358                                  "as positional arguments or as a keyword, "
    359                                  "not both." % type(self).__name__)
--> 360             params['objects'] = [panel(pane) for pane in objects]
    361         elif 'objects' in params:
    362             params['objects'] = [panel(pane) for pane in params['objects']]

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\layout\base.py in <listcomp>(.0)
    358                                  "as positional arguments or as a keyword, "
    359                                  "not both." % type(self).__name__)
--> 360             params['objects'] = [panel(pane) for pane in objects]
    361         elif 'objects' in params:
    362             params['objects'] = [panel(pane) for pane in params['objects']]

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\pane\base.py in panel(obj, **kwargs)
     49     if kwargs.get('name', False) is None:
     50         kwargs.pop('name')
---> 51     pane = PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
     52     if len(pane.layout) == 1 and pane._unpack:
     53         return pane.layout[0]

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\param.py in __init__(self, object, **params)
    189             'object', 'parameters', 'name', 'display_threshold', 'expand_button',
    190             'expand', 'expand_layout', 'widgets', 'show_labels', 'show_name'])
--> 191         self._update_widgets()
    192 
    193     def __repr__(self, depth=0):

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\param.py in _update_widgets(self, *events)
    258             self._widgets = {}
    259         else:
--> 260             self._widgets = self._get_widgets()
    261 
    262         alias = {'_title': 'name'}

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\param.py in _get_widgets(self)
    540         else:
    541             widgets = []
--> 542         widgets += [(pname, self.widget(pname)) for pname in self._ordered_params]
    543         return OrderedDict(widgets)
    544 

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\param.py in <listcomp>(.0)
    540         else:
    541             widgets = []
--> 542         widgets += [(pname, self.widget(pname)) for pname in self._ordered_params]
    543         return OrderedDict(widgets)
    544 

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\panel\param.py in widget(self, p_name)
    494         if 'step' in kw:
    495             watchers.append(self.object.param.watch(link, p_name, 'step'))
--> 496         watchers.append(self.object.param.watch(link, p_name))
    497 
    498         options = kwargs.get('options', [])

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\param\parameterized.py in watch(self_, fn, parameter_names, what, onlychanged, queued)
   1892                           onlychanged=onlychanged, parameter_names=parameter_names,
   1893                           what=what, queued=queued)
-> 1894         self_._watch('append', watcher, what)
   1895         return watcher
   1896 

~\AppData\Local\Continuum\anaconda3\envs\dms\lib\site-packages\param\parameterized.py in _watch(self_, action, watcher, what, operation)
   1875 
   1876             if self_.self is not None and what == "value":
-> 1877                 watchers = self_.self._param_watchers
   1878                 if parameter_name not in watchers:
   1879                     watchers[parameter_name] = {}

AttributeError: 'TestStreamParam' object has no attribute '_param_watchers'


dwr-psandhu avatar Mar 06 '21 15:03 dwr-psandhu

Thanks! I agree that this is a cryptic error message, but I'm having trouble thinking of anything we can do in Param to detect it. If you override the constructor in this way, Parameterized's own constructor will never get called, so we can't put anything to detect it in there. Maybe it could go in the metaclass somewhere, detecting that the super constructor never got called, but if so I wouldn't know how to go about that.

@philippjfr, were you maybe imagining that Panel could detect this, not Param? Panel could presumably short-circuit this error message by testing for proper construction earlier, but if so this would be an issue for Panel, not Param.

jbednar avatar May 10 '21 16:05 jbednar

I could imagine that a sentinel flag could be set and then any access to parameterized.param, __getattr__ and __setattr__ would generate a clear error message. I'd say that would catch this error in (almost) all cases.

philippjfr avatar May 10 '21 16:05 philippjfr

Hmm. I agree that would catch the message, but I'm a bit concerned that it would require slowing down every attribute access, just to improve what seems a fairly obscure error condition.

Maybe we could instead put .param/getattr/setattr behind a try/except where the first thing we check is on exception if the object was not properly initialized, and if so explain that it needs to be initialized for all Parameterized features to be available, and otherwise reraise the original exception. Seems a bit tricky!

jbednar avatar May 10 '21 20:05 jbednar