From 69eb0b3a18221e40a2862c7aa09281058aad3ca5 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 19 Jul 2023 14:15:07 +0200 Subject: [PATCH] set the instance namespace earlier if needed --- param/parameterized.py | 25 +++++++++---------- tests/testparameterizedobject.py | 41 +++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index a650ff1e2..7eedb15f2 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1403,6 +1403,9 @@ def __set__(self, obj, val): _old = self.default self.default = val else: + # When setting a Parameter before calling super. + if not isinstance(obj._param__private, _InstancePrivate): + obj._param__private = _InstancePrivate() _old = obj._param__private.values.get(self.name, self.default) obj._param__private.values[self.name] = val @@ -3611,9 +3614,6 @@ class _ClassPrivate: Whethe the class has been renamed by a super class params: dict Dict of parameter_name:parameter - values: dict - Dict of parameter_name:value, populated when a Parameter is set before - super().__init__ is called. """ __slots__ = [ @@ -3621,7 +3621,7 @@ class _ClassPrivate: 'disable_instance_params', 'renamed', 'params', - 'values', + 'initialized', ] def __init__( @@ -3630,7 +3630,6 @@ def __init__( disable_instance_params=False, renamed=False, params=None, - values=None, ): if parameters_state is None: parameters_state = { @@ -3643,7 +3642,7 @@ def __init__( self.disable_instance_params = disable_instance_params self.renamed = renamed self.params = {} if params is None else params - self.values = {} if values is None else params + self.initialized = False def __getstate__(self): return {slot: getattr(self, slot) for slot in self.__slots__} @@ -3760,14 +3759,12 @@ class Foo(Parameterized): def __init__(self, **params): global object_count - # Setting a Parameter in an __init__ block before calling super().__init__ - # fills `values` on the class private namespace, that has to be passed - # to the instance private namespace. `values` on the class is cleared - # as it's only meant to be transient data. - values = type(self)._param__private.values - cvalues = values.copy() - values.clear() - self._param__private = _InstancePrivate(values=cvalues) + # Setting a Parameter value in an __init__ block before calling + # Parameterized.__init__ (via super() generally) already sets the + # _InstancePrivate namespace over the _ClassPrivate namespace + # (see Parameter.__set__) so we shouldn't override it here. + if not isinstance(self._param__private, _InstancePrivate): + self._param__private = _InstancePrivate() self._param_watchers = {} # Skip generating a custom instance name when a class in the hierarchy diff --git a/tests/testparameterizedobject.py b/tests/testparameterizedobject.py index 1aa5662c8..805d7b6bf 100644 --- a/tests/testparameterizedobject.py +++ b/tests/testparameterizedobject.py @@ -397,7 +397,46 @@ def cb(self): assert p.x == 1 assert count == 0 - assert not P._param__private.values + + def test_instantiation_set_before_super_contrived(self): + # https://github.com/holoviz/param/pull/790#discussion_r1263483293 + class P(param.Parameterized): + + value = param.String(default="A") + + def __init__(self, depth=0): + self.value = 'B' + if depth < 2: + self.sub = P(depth+1) + super().__init__() + + p = P() + + assert p.value == 'B' + assert p.sub.value == 'B' + + def test_instantiation_set_before_super_subclass(self): + # Inspired by a HoloViews use case (GenericElementPlot, GenericOverlayPlot) + class A(param.Parameterized): + + def __init__(self, batched=False, **params): + self.batched = batched + super().__init__(**params) + + class B(A): + + batched = param.Boolean() + + def __init__(self, batched=True, **params): + super().__init__(batched=batched, **params) + + a = A() + assert a.batched is False + + # When b is instantiated the `batched` Parameter of B is set before + # Parameterized.__init__ is called. + b = B() + assert b.batched is True @pytest.mark.xfail( raises=AttributeError,