Deprecate Selector's compute_default_fn slot and compute_default() method
As suggested in https://github.com/holoviz/param/issues/508#issuecomment-2593954649.
Codecov Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 89.22%. Comparing base (ef39804) to head (3a42ef5).
:warning: Report is 2 commits behind head on main.
Additional details and impacted files
@@ Coverage Diff @@
## main #1012 +/- ##
==========================================
+ Coverage 89.14% 89.22% +0.07%
==========================================
Files 9 9
Lines 4681 4714 +33
==========================================
+ Hits 4173 4206 +33
Misses 508 508
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
@philippjfr @jbednar do you agree with this deprecation?
I agree with this deprecation (and in fact I think I suggested it a while back :-) ). I see you've removed documentation of the old way to do it, so I assume that the new way to do it has already been documented appropriately in a separate PR?
I'm working separately on default_factory but haven't opened a PR yet (you will know about it!). The main motivation for deprecating compute_default_fn/compute_defaut() is that it is imo a weird API and it appears to be unused (see more details in https://github.com/holoviz/param/issues/508#issuecomment-2593954649, with only 3 occurrences found on Github in topographica). As such I don't see a need to document how to reproduce what this feature was achieving. For the users who might need this feature, they can easily reproduce it as the code is trivial:
def compute_default(self):
"""
If this parameter's compute_default_fn is callable, call it
and store the result in self.default.
Also removes None from the list of objects (if the default is
no longer None).
"""
if self.default is None and callable(self.compute_default_fn):
self.default = self.compute_default_fn()
self._ensure_value_is_in_objects(self.default)
def _ensure_value_is_in_objects(self, val):
"""
Make sure that the provided value is present on the objects list.
Subclasses can override if they support multiple items on a list,
to check each item instead.
"""
if val not in self.objects:
self._objects.append(val)
Inserting below and updating the bit of text from https://github.com/holoviz/param/issues/508#issuecomment-2593954649 that is relevant to this PR and supports the answer to:
I agree with this deprecation (and in fact I think I suggested it a while back :-) ). I see you've removed documentation of the old way to do it, so I assume that the new way to do it has already been documented appropriately in a separate PR?
The documentation removed is:
In cases where the objects in the list cannot be known when writing the Parameterized class but can be calculated at runtime, you can supply a callable (of no arguments) to
compute_default_fn, and then ensure that at runtime you callcompute_defaulton that Parameter to initialize the value
I don't think compute_default_fn really does what was described in this paragraph. If I don't know in advance the list of objects:
- For a Selector, I would like to populate the list of all objects, which is not what this does, as it only sets objects as a list of one item (
[default]). - For a ListSelector, it does populate the list of all objects, but also sets the default to the whole list, which is quite arbitrary (in the apps I work on the best default for a ListSelector, driving a MultiSelect widget, is None).
compute_default_fn also only sets the Parameter default and objects slots, which does not affect the Parameterized instance attribute value, which is awkward.
So, I don't think there's any documentation to replace.
compute_default_fn is a slot of Selector (and all its subclasses). It accepts a callable that is called only when the user calls the compute_default() method on Selector. When the default attribute of a Parameter is not None, compute_default() sets default to the value returned by the callable.
This is unrelated to compute_default_fn, but note that default and the instance parameter value aren't linked once a Parameterized class is instantiated.
class P(param.Parameterized):
x = param.Parameter()
p = P()
p.param['x'].default = 'something'
# Setting `default` doesn't affect the current value of `x` on `p`.
assert p.x is None
So calling compute_default() on a Parameterized instance isn't so useful when it comes to updating default; it however does update objects if required, which is slightly more useful.
class P(param.Parameterized):
x = param.Selector(compute_default_fn=lambda: 3)
p = P()
p.param['x'].compute_default()
assert p.x != 3
assert p.x is None
assert p.param['x'].objects == [3]
So compute_default() has to be called before the instance is initialized.
Either in the init:
class P(param.Parameterized):
x = param.Selector(compute_default_fn=lambda: 3)
def __init__(self, **params):
self.param['x'].compute_default()
super().__init__(**params)
p = P()
assert p.x == 3
assert p.param['x'].objects == [3]
# Bad idea as the parameter modified is the class-parameter!
assert P.param.x.default == 3
But obviously that's a bad idea. Or on the class object:
class P(param.Parameterized):
x = param.Selector(compute_default_fn=lambda: 3)
P.param['x'].compute_default()
p = P()
assert p.x == 3
assert p.param['x'].objects == [3]
Note that if the Selector had objects defined (excluding None as the first item), calling compute_default() would not do anything, as it only works if default is None (Selector auto sets default to the first item of objects).
On a list flavor Selector, compute_default_fn can extend the list of objects.
class P(param.Parameterized):
x = param.ListSelector(objects=[0, 1, 2], compute_default_fn=lambda: [3, 4])
P.param['x'].compute_default()
p = P()
assert p.x == [3, 4]
assert p.param['x'].objects == [0, 1, 2, 3, 4]
So basically, the functionality provided by compute_default_fn and the compute_default method can be implemented with this function that needs to be invoked with a parameter and a callable:
def compute_default(parameter, fn):
if parameter.default is None:
parameter.default = default = fn()
objects = parameter.objects
if isinstance(parameter, param.ListSelector):
for o in default:
if o not in objects:
objects.append(o)
else:
if default not in objects:
objects.append(default)
Searching for usage of compute_default_fn on Github, I only found 3 occurrences in topographica. Clearly, this isn't used much :)
https://github.com/holoviz/param/pull/1092 aims to add the default_factory slot to Param Parameters. This is very different from compute_default, as:
- the factory is always invoked by Param on instantiation, and can additionally be invoked on class creation (when the callable is wrapped in a
DefaultFactoryinstance seton_class=True) - the factory sets the instance attribute value and not the parameter default attribute