param
param copied to clipboard
Dynamically adding to param.ObjectSelector after instantiation
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 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
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.
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?
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.
@jbednar Any thoughts on this since you implemented the dict mappings for ObjectSelectors?
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)
With the current code, the above pattern is the correct approach for adding to an initially empty objects list. All three steps are important:
- Add the object <-> name mapping, which will be used in the GUI to display names and select objects
- Add the object to the allowed list, which will be used for validation that a manually set object is in the list.
- 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:
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.
I am also having issues dynamically changing the objects in ObjectSelector. When trying to recreate the code, I get the following error:
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), andreplace_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.
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.