param icon indicating copy to clipboard operation
param copied to clipboard

Dynamically adding to param.ObjectSelector after instantiation

Open kcpevey opened this issue 5 years ago • 9 comments

I have an object selector that I'd like to populate dynamically after instantiating the root class. I'll call it poof below, the other params are helpful for testing.

class Simulation(param.Parameterized):
    bar = param.ObjectSelector(default=2, objects={'a':2, 'b':9})
    foo = param.ObjectSelector(default='f', objects=['f','g','h'])
    poof = param.ObjectSelector(objects=dict())

sim = Simulation()
pn.panel(sim)

In some other bit of code, I add a value to poof :

sim.param.poof.names['c'] = 99
sim.param.poof.objects.append(99)

In order for this to show up in my UI, I have to trigger a redraw with pn.panel(sim). Which then shows that the poof widget now contains the c option on the UI. That's great.

However, when I try to access the value of poof from the python side, it still shows up as empty, ie. type(sim.poof) = NoneType

Is this a bug or is there a better way of going about this?

kcpevey avatar Apr 15 '19 16:04 kcpevey

@kcpevey you don't mention it here, but assuming that you are reassigning the poof attribute on the sim object after you add the option to the poof param object:

sim.poof = 99

Then it looks like you get this error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-ef65c8de2fa7> in <module>
      1 sim.param.poof.names['c'] = 99
      2 sim.param.poof.objects.append(99)# = list(sim.param.poof.names.values())
----> 3 sim.poof = 99

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in _f(self, obj, val)
    253         instance_param = getattr(obj, '_instance__params', {}).get(self.name)
    254         if instance_param is not None and self is not instance_param:
--> 255             instance_param.__set__(obj, val)
    256             return
    257         return f(self, obj, val)

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in _f(self, obj, val)
    255             instance_param.__set__(obj, val)
    256             return
--> 257         return f(self, obj, val)
    258     return _f
    259 

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in __set__(self, obj, val)
    784 
    785         for watcher in watchers:
--> 786             obj.param._call_watcher(watcher, event)
    787         if not obj.param._BATCH_WATCH:
    788             obj.param._batch_call_watchers()

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in _call_watcher(self_, watcher, event)
   1379         elif watcher.mode == 'args':
   1380             with batch_watch(self_.self_or_cls, run=False):
-> 1381                 watcher.fn(self_._update_event_type(watcher, event, self_.self_or_cls.param._TRIGGER))
   1382         else:
   1383             with batch_watch(self_.self_or_cls, run=False):

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/panel/param.py in link(change)
    366                 try:
    367                     self._updating = True
--> 368                     widget.set_param(**updates)
    369                 finally:
    370                     self._updating = False

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in inner(*args, **kwargs)
   1144                 get_logger(name=args[0].__class__.__name__).log(
   1145                     WARNING, 'Use method %r via param namespace ' % fn.__name__)
-> 1146             return fn(*args, **kwargs)
   1147 
   1148         inner.__doc__= "Inspect .param.%s method for the full docstring"  % fn.__name__

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in set_param(self_or_cls, *args, **kwargs)
   2486     @Parameters.deprecate
   2487     def set_param(self_or_cls,*args,**kwargs):
-> 2488         return self_or_cls.param.set_param(*args,**kwargs)
   2489 
   2490     @bothmethod

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in set_param(self_, *args, **kwargs)
   1290         self_.self_or_cls.param._BATCH_WATCH = BATCH_WATCH
   1291         if not BATCH_WATCH:
-> 1292             self_._batch_call_watchers()
   1293 
   1294 

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/param/parameterized.py in _batch_call_watchers(self_)
   1405                 with batch_watch(self_.self_or_cls, run=False):
   1406                     if watcher.mode == 'args':
-> 1407                         watcher.fn(*events)
   1408                     else:
   1409                         watcher.fn(**{c.name:c.new for c in events})

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/panel/viewable.py in param_change(*events)
    597                 viewable, root, doc, comm = state._views[ref]
    598                 if comm or state._unblocked(doc):
--> 599                     self._update_model(events, msg, root, model, doc, comm)
    600                     if comm and 'embedded' not in root.tags:
    601                         push(doc, comm)

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/panel/viewable.py in _update_model(self, events, msg, root, model, doc, comm)
    566                     filtered[k] = v
    567             for attr, new in filtered.items():
--> 568                 setattr(model, attr, new)
    569                 event = doc._held_events[-1] if doc._held_events else None
    570                 if (event and event.model is model and event.attr == attr and

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/has_props.py in __setattr__(self, name, value)
    278 
    279         if name in props or (descriptor is not None and descriptor.fset is not None):
--> 280             super(HasProps, self).__setattr__(name, value)
    281         else:
    282             matches, text = difflib.get_close_matches(name.lower(), props), "similar"

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/property/descriptors.py in __set__(self, obj, value, setter)
    543             raise RuntimeError("%s.%s is a readonly property" % (obj.__class__.__name__, self.name))
    544 
--> 545         self._internal_set(obj, value, setter=setter)
    546 
    547     def __delete__(self, obj):

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/property/descriptors.py in _internal_set(self, obj, value, hint, setter)
    764 
    765         '''
--> 766         value = self.property.prepare_value(obj, self.name, value)
    767 
    768         old = self.__get__(obj, obj.__class__)

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/property/bases.py in prepare_value(self, obj_or_cls, name, value)
    325                     break
    326             else:
--> 327                 raise e
    328         else:
    329             value = self.transform(value)

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/property/bases.py in prepare_value(self, obj_or_cls, name, value)
    318         try:
    319             if validation_on():
--> 320                 self.validate(value)
    321         except ValueError as e:
    322             for tp, converter in self.alternatives:

~/miniconda/envs/es-workflows/lib/python3.6/site-packages/bokeh/core/property/bases.py in validate(self, value, detail)
    450                 nice_join([ cls.__name__ for cls in self._underlying_type ]), value, type(value).__name__
    451             )
--> 452             raise ValueError(msg)
    453 
    454     def from_json(self, json, models=None):

ValueError: expected a value of type str, got 99 of type int

sdc50 avatar Apr 17 '19 14:04 sdc50

I'm not sure why we get the error above, but a workaround (for some cases) is to just completely reassign the objects property of the poof param object:

sim.param.poof.names['c'] = 99
sim.param.poof.objects = list(sim.param.poof.names.values())
sim.poof = 99

This works for me, but I don't understand why it's different than the above case.

sdc50 avatar Apr 17 '19 14:04 sdc50

If I use the syntax from @sdc50 :

sim.param.poof.names['c'] = 99
sim.param.poof.objects = list(sim.param.poof.names.values())

Then sim.poof=99 will update the UI without requiring it to be rerendered.

That solves my issue but is it anticipated behavior? Namely the subtle syntax requirements of setting *.objects?

kcpevey avatar Apr 18 '19 12:04 kcpevey

We should definitely think about improving this, the ambiguity between dict and list like ObjectSelectors makes this quite awkward. Maybe the parameter class should have an explicit API to add and remove objects.

philippjfr avatar Apr 23 '19 10:04 philippjfr

@jbednar Any thoughts on this since you implemented the dict mappings for ObjectSelectors?

philippjfr avatar Apr 23 '19 10:04 philippjfr

This is tricky! First, if everything runs in one cell, I don't see the error reported by @sdc50:

import panel as pn, param
pn.extension()

class Simulation(param.Parameterized):
    bar = param.ObjectSelector(default=2, objects={'a':2, 'b':9})
    foo = param.ObjectSelector(default='f', objects=['f','g','h'])
    poof = param.ObjectSelector(objects=dict())

sim = Simulation()
pn.panel(sim)

sim.param.poof.names['c'] = 99
sim.param.poof.objects.append(99)
sim.poof=99
pn.panel(sim)

image

With the current code, the above pattern is the correct approach for adding to an initially empty objects list. All three steps are important:

  1. Add the object <-> name mapping, which will be used in the GUI to display names and select objects
  2. Add the object to the allowed list, which will be used for validation that a manually set object is in the list.
  3. Set the current value of this parameter to the desired value (99) in this case.

If you omit step 1, the GUI dropdown will show the actual objects rather than the label, which is not what you want. If you omit step 3, the allowable objects list will now be non-empty, but there still won't be any actual value. If you omit step 2, step 3 won't work unless you've declared "check_on_set=True".

So far, so good. But if I put a cell boundary that makes pn.panel(sim) be displayed, I can reproduce @sdc50 's traceback:

image image

It looks to me like instantiating the panel forces poof to have a value and makes the type str somehow? That seems like a bug or unintentional behavior, to me. @philippjfr?

Finally, yes, I agree, this is all subtle enough that we should provide an API for adding an object, to avoid everyone who wishes to do this having to understand all these details.

jbednar avatar Apr 23 '19 19:04 jbednar

I am also having issues dynamically changing the objects in ObjectSelector. When trying to recreate the code, I get the following error: image

evanfollis avatar May 22 '19 15:05 evanfollis

Hello, We found this issue as we were ourselves considering to extend the API of ObjectSelector to add/remove items from the list. Are you interested that we do it?

At first glance, we would add:

  • a objects_editable attribute (default False?)
  • add_object(value, name=None) (name should be defined iff self.names is not None)
  • add_objects(values: Union[List,Dict]) (should be List/Dict depending on whether self.names is None)
  • remove_object(item) (item should be the name if self.names is not None)
  • remove_objects(items: List) (list of values, or of names depending on whether self.names is None)
  • set_objects(item: Union[List,Dict]) (probably should enforce to provide names iff they were already provided before)
  • add_object, add_objects (takes a list as input), remove_object, remove_objects (idem), and replace_objects (list or dictionnary)

Thank you for giving us a feedback so that we can start. I did not look specifically at the bug discussed here, probably will we encounter it on the way.

thomasdeneux avatar Jul 01 '19 09:07 thomasdeneux

I've opened a PR with partial support for the issue above: https://github.com/holoviz/param/pull/440

It should preserve backwards compatibility, but I don't know of any way to support .append while still doing so.

jbednar avatar Sep 29 '20 22:09 jbednar