diff --git a/param/__init__.py b/param/__init__.py index e0beb4110..a3c62a8e3 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1691,6 +1691,8 @@ def __init__(self, *, objects=Undefined, default=Undefined, instantiate=Undefine # Required as Parameter sets allow_None=True if default is None if allow_None is Undefined: self.allow_None = self._slot_defaults['allow_None'] + else: + self.allow_None = allow_None if self.default is not None and self.check_on_set is True: self._validate(self.default) diff --git a/param/parameterized.py b/param/parameterized.py index 92d2406f9..a650ff1e2 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1254,8 +1254,6 @@ def _set_instantiate(self,instantiate): # having this code avoids needless instantiation. if self.readonly: self.instantiate = False - elif self.constant is True: - self.instantiate = True elif instantiate is not Undefined: self.instantiate = instantiate else: @@ -1813,30 +1811,35 @@ def _setup_params(self_,**params): """ Initialize default and keyword parameter values. - First, ensures that all Parameters with 'instantiate=True' - (typically used for mutable Parameters) are copied directly - into each object, to ensure that there is an independent copy - (to avoid surprising aliasing errors). Then sets each of the - keyword arguments, warning when any of them are not defined as - parameters. - - Constant Parameters can be set during calls to this method. + First, ensures that all Parameters with 'instantiate=True' (typically + used for mutable Parameters) are copied directly into each object, to + ensure that there is an independent copy (to avoid surprising aliasing + errors). Second, ensures that Parameters with 'constant=True' are + referenced on the instance, to make sure that setting a constant + Parameter on the class doesn't affect already created instances. Then + sets each of the keyword arguments, raising when any of them are not + defined as parameters. """ self = self_.param.self ## Deepcopy all 'instantiate=True' parameters # (building a set of names first to avoid redundantly # instantiating a later-overridden parent class's parameter) - params_to_instantiate = {} + params_to_deepcopy = {} + params_to_ref = {} for class_ in classlist(type(self)): if not issubclass(class_, Parameterized): continue for (k, v) in class_.param._parameters.items(): # (avoid replacing name with the default of None) if v.instantiate and k != "name": - params_to_instantiate[k] = v + params_to_deepcopy[k] = v + elif v.constant and k != 'name': + params_to_ref[k] = v - for p in params_to_instantiate.values(): + for p in params_to_deepcopy.values(): self.param._instantiate_param(p) + for p in params_to_ref.values(): + self.param._instantiate_param(p, deepcopy=False) ## keyword arg setting for name, val in params.items(): @@ -1857,9 +1860,11 @@ def _changed(cls, event): """ return not Comparator.is_equal(event.old, event.new) - def _instantiate_param(self_, param_obj, dict_=None, key=None): - # deepcopy param_obj.default into self._param__private.values (or dict_ if supplied) - # under the parameter's name (or key if supplied) + def _instantiate_param(self_, param_obj, dict_=None, key=None, deepcopy=True): + # deepcopy or store a reference to reference param_obj.default into + # self._param__private.values (or dict_ if supplied) under the + # parameter's name (or key if supplied) + instantiator = copy.deepcopy if deepcopy else lambda o: o self = self_.self dict_ = dict_ or self._param__private.values key = key or param_obj.name @@ -1868,10 +1873,10 @@ def _instantiate_param(self_, param_obj, dict_=None, key=None): if param_key in shared_parameters._shared_cache: new_object = shared_parameters._shared_cache[param_key] else: - new_object = copy.deepcopy(param_obj.default) + new_object = instantiator(param_obj.default) shared_parameters._shared_cache[param_key] = new_object else: - new_object = copy.deepcopy(param_obj.default) + new_object = instantiator(param_obj.default) dict_[key] = new_object diff --git a/tests/testobjectselector.py b/tests/testobjectselector.py index 5bbaccc11..d3f7e27c4 100644 --- a/tests/testobjectselector.py +++ b/tests/testobjectselector.py @@ -10,6 +10,7 @@ from collections import OrderedDict import param +import pytest from .utils import check_defaults @@ -104,6 +105,51 @@ def test_unbound_allow_None_not_dynamic(self): assert s.allow_None is None + def test_allow_None_set_and_behavior_class(self): + class P(param.Parameterized): + a = param.ObjectSelector(objects=dict(a=1), allow_None=True) + b = param.ObjectSelector(objects=dict(a=1), allow_None=False) + c = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=True) + d = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=False) + + assert P.param.a.allow_None is True + assert P.param.b.allow_None is False + assert P.param.c.allow_None is True + assert P.param.d.allow_None is False + + P.a = None + assert P.a is None + with pytest.raises(ValueError): + P.b = None + P.c = None + assert P.c is None + with pytest.raises(ValueError): + P.d = None + + def test_allow_None_set_and_behavior_instance(self): + class P(param.Parameterized): + a = param.ObjectSelector(objects=dict(a=1), allow_None=True) + b = param.ObjectSelector(objects=dict(a=1), allow_None=False) + c = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=True) + d = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=False) + + p = P() + + assert p.param.a.allow_None is True + assert p.param.b.allow_None is False + assert p.param.c.allow_None is True + assert p.param.d.allow_None is False + + p.a = None + assert p.a is None + with pytest.raises(ValueError): + p.b = None + p.c = None + assert p.c is None + with pytest.raises(ValueError): + p.d = None + + def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) diff --git a/tests/testparameterizedobject.py b/tests/testparameterizedobject.py index 0666cdf7b..1aa5662c8 100644 --- a/tests/testparameterizedobject.py +++ b/tests/testparameterizedobject.py @@ -263,6 +263,37 @@ class C(A, B): pass assert C.name == 'AA' + def test_constant_parameter_modify_class_before(self): + """Test you can set on class and the new default is picked up + by new instances""" + TestPO.const=9 + testpo = TestPO() + self.assertEqual(testpo.const,9) + + def test_constant_parameter_modify_class_after_init(self): + """Test that setting the value on the class doesn't update the instance value + even when the instance value hasn't yet been set""" + oobj = [] + class P(param.Parameterized): + x = param.Parameter(default=oobj, constant=True) + + p1 = P() + + P.x = nobj = [0] + assert P.x is nobj + assert p1.x == oobj + assert p1.x is oobj + + p2 = P() + assert p2.x == nobj + assert p2.x is nobj + + def test_constant_parameter_after_init(self): + """Test that you can't set a constant parameter after construction.""" + testpo = TestPO(const=17) + self.assertEqual(testpo.const,17) + self.assertRaises(TypeError,setattr,testpo,'const',10) + def test_constant_parameter(self): """Test that you can't set a constant parameter after construction.""" testpo = TestPO(const=17) @@ -274,52 +305,6 @@ def test_constant_parameter(self): testpo = TestPO() self.assertEqual(testpo.const,9) - def test_parameter_constant_instantiate(self): - # instantiate is automatically set to True when constant=True - assert TestPO.param.const.instantiate is True - - class C(param.Parameterized): - # instantiate takes precedence when True - a = param.Parameter(instantiate=True, constant=False) - b = param.Parameter(instantiate=False, constant=False) - c = param.Parameter(instantiate=False, constant=True) - d = param.Parameter(constant=True) - e = param.Parameter(constant=False) - f = param.Parameter() - - assert C.param.a.constant is False - assert C.param.a.instantiate is True - assert C.param.b.constant is False - assert C.param.b.instantiate is False - assert C.param.c.constant is True - assert C.param.c.instantiate is True - assert C.param.d.constant is True - assert C.param.d.instantiate is True - assert C.param.e.constant is False - assert C.param.e.instantiate is False - assert C.param.f.constant is False - assert C.param.f.instantiate is False - - def test_parameter_constant_instantiate_subclass(self): - - obj = object() - - class A(param.Parameterized): - x = param.Parameter(obj) - - class B(param.Parameterized): - x = param.Parameter(constant=True) - - assert A.param.x.constant is False - assert A.param.x.instantiate is False - assert B.param.x.constant is True - assert B.param.x.instantiate is True - - a = A() - b = B() - assert a.x is obj - assert b.x is not obj - def test_readonly_parameter(self): """Test that you can't set a read-only parameter on construction or as an attribute.""" testpo = TestPO() diff --git a/tests/testselector.py b/tests/testselector.py index 618e8fee8..321556354 100644 --- a/tests/testselector.py +++ b/tests/testselector.py @@ -9,8 +9,9 @@ from collections import OrderedDict import param +import pytest -from.utils import check_defaults +from .utils import check_defaults opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) @@ -103,6 +104,50 @@ def test_allow_None_is_None(self): assert p.param.s.allow_None is None assert p.param.d.allow_None is None + def test_allow_None_set_and_behavior_class(self): + class P(param.Parameterized): + a = param.Selector(objects=dict(a=1), allow_None=True) + b = param.Selector(objects=dict(a=1), allow_None=False) + c = param.Selector(default=1, objects=dict(a=1), allow_None=True) + d = param.Selector(default=1, objects=dict(a=1), allow_None=False) + + assert P.param.a.allow_None is True + assert P.param.b.allow_None is False + assert P.param.c.allow_None is True + assert P.param.d.allow_None is False + + P.a = None + assert P.a is None + with pytest.raises(ValueError): + P.b = None + P.c = None + assert P.c is None + with pytest.raises(ValueError): + P.d = None + + def test_allow_None_set_and_behavior_instance(self): + class P(param.Parameterized): + a = param.Selector(objects=dict(a=1), allow_None=True) + b = param.Selector(objects=dict(a=1), allow_None=False) + c = param.Selector(default=1, objects=dict(a=1), allow_None=True) + d = param.Selector(default=1, objects=dict(a=1), allow_None=False) + + p = P() + + assert p.param.a.allow_None is True + assert p.param.b.allow_None is False + assert p.param.c.allow_None is True + assert p.param.d.allow_None is False + + p.a = None + assert p.a is None + with pytest.raises(ValueError): + p.b = None + p.c = None + assert p.c is None + with pytest.raises(ValueError): + p.d = None + def test_autodefault(self): class P(param.Parameterized): o1 = param.Selector(objects=[6, 7]) @@ -337,3 +382,13 @@ class B(A): assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is True + + def test_no_instantiate_when_constant(self): + # https://github.com/holoviz/param/issues/287 + objs = [object(), object()] + + class A(param.Parameterized): + p = param.Selector(default=objs[0], objects=objs, constant=True) + + a = A() + assert a.p is objs[0]