From fba848e4502f4e8107fd03be5040f9d825a78e46 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Tue, 11 Jul 2023 19:21:08 +0200 Subject: [PATCH] Add a dedicated namespace for private objs on Parameterized (#766) --- examples/user_guide/How_Param_Works.ipynb | 14 +- examples/user_guide/Parameters.ipynb | 66 +++++- param/__init__.py | 2 +- param/parameterized.py | 271 +++++++++++++++------- tests/testaddparameter.py | 4 +- tests/testdynamicparams.py | 2 +- tests/testparameterizedobject.py | 59 +++++ 7 files changed, 316 insertions(+), 102 deletions(-) diff --git a/examples/user_guide/How_Param_Works.ipynb b/examples/user_guide/How_Param_Works.ipynb index f87e361c9..6688b0fc4 100644 --- a/examples/user_guide/How_Param_Works.ipynb +++ b/examples/user_guide/How_Param_Works.ipynb @@ -23,7 +23,7 @@ "source": [ "class Count:\n", " def __init__(self, start=0):\n", - " self._count=start\n", + " self._count = start\n", " \n", " def __get__(self, obj, objtype=None):\n", " self._count += 1\n", @@ -89,7 +89,7 @@ "\n", "- allowing Parameter default values to be set at the class level (as just described),\n", "- supporting inheriting Parameter objects from superclasses, \n", - "- instantiating parameter default values into the class's dictionary (if needed)\n", + "- instantiating parameter default values (if needed)\n", "- populating the `name` slot of each Parameter by its attribute name in the class,\n", "- reporting whether a class has been declared to be abstract (useful for ignoring it in selectors),\n", "- various bookkeeping about dependencies and watchers, \n", @@ -175,7 +175,7 @@ "id": "36ef8c10", "metadata": {}, "source": [ - "If the value of `p` is set on `a1`, `a1`'s value of `p` is stored in the `a1` instance itself, under a specially mangled attribute name. The mangled name is called the `_internal_name` of the parameter, and is constructed from the \"attrib name\" of the parameter (i.e. `p` in this case) but modified so that it will not be confused with the underlying `Parameter` object `p`:" + "If the value of `p` is set on `a1`, `a1`'s value of `p` is stored in the `a1` instance itself, in a dictionary named `values` under the private namespace `_param__private`:" ] }, { @@ -185,8 +185,8 @@ "metadata": {}, "outputs": [], "source": [ - "a1.p=2\n", - "a1.__dict__['_p_param_value']" + "a1.p = 2\n", + "a1._param__private.values['p']" ] }, { @@ -194,7 +194,7 @@ "id": "9b5c6a79", "metadata": {}, "source": [ - "When `a1.p` is requested, `a1.__dict__['_p_param_value']` is returned. When `a2.p` is requested, `_p_param_value` is not found in `a2.__dict__`, so `A.__dict__['p'].default` (i.e. `A.p`) is returned instead:" + "When `a1.p` is requested, `a1._param__private.values['p']` is returned. When `a2.p` is requested, `p` is not found in `a2._param__private.values`, so `A.__dict__['p'].default` (i.e. `A.p`) is returned instead:" ] }, { @@ -222,7 +222,7 @@ "metadata": {}, "outputs": [], "source": [ - "A.p=3\n", + "A.p = 3\n", "a2.p, a1.p" ] }, diff --git a/examples/user_guide/Parameters.ipynb b/examples/user_guide/Parameters.ipynb index e4bd062ef..fece22781 100644 --- a/examples/user_guide/Parameters.ipynb +++ b/examples/user_guide/Parameters.ipynb @@ -897,6 +897,70 @@ "p.param.pprint()" ] }, + { + "cell_type": "markdown", + "id": "3830ace8", + "metadata": {}, + "source": [ + "## Parameterized namespace\n", + "\n", + "Param allows you to create Parameterized objects by inheriting from the `Parameterized` base class. Param has evolved over time to reduce its footprint and reserve as few as possible attributes on this namespace, to reduce the risk of name clashes and allow you to freely define your attribute names. Param reserves a few names that are described below, make sure not to override, unless it is stated it is allowed:\n", + "\n", + "- Public attributes:\n", + " - `name`: Parameterized classes and instances have a name `String` Parameter, that by default is set to the class name when accessed from the class and to the class name appended with a 5 digit when accessed from the instance. You can override this Parameter by your own `String` Parameter if you need to.\n", + " - `param`: Property that helps keep the Parameterized namespace clean and disambiguate between Parameter objects and parameter values, it gives access to a namespace that offers various methods (see the section below) to update and inspect the Parameterized object at hand.\n", + "- Private attributes:\n", + " - `_param__parameters`: Store the object returned by `.param` on the class\n", + " - `_param__private`: Store various internal data on Parameterized class and instances\n", + " - `_param_watchers` (to be removed soon): Store a dictionary of instance watchers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01759e1b", + "metadata": {}, + "outputs": [], + "source": [ + "class P(param.Parameterized):\n", + " a = param.Number()\n", + " b = param.String()\n", + "\n", + "p = P()\n", + "print(f'{P.name=}, {p.name=}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e802c7c7", + "metadata": {}, + "outputs": [], + "source": [ + "def namespace(obj):\n", + " return [o for o in dir(obj) if not o.startswith('__')]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c702205", + "metadata": {}, + "outputs": [], + "source": [ + "namespace(P)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b74fb157", + "metadata": {}, + "outputs": [], + "source": [ + "namespace(p)" + ] + }, { "cell_type": "markdown", "id": "23979d82", @@ -904,7 +968,7 @@ "source": [ "## Other Parameterized methods\n", "\n", - "Like `.param.pprint`, the remaining \"utility\" or convenience methods available for a `Parameterized` class or object are provided via a subobject called `param` that helps keep the namespace clean and disambiguate between Parameter objects and parameter values:\n", + "Like `.param.pprint`, the remaining \"utility\" or convenience methods available for a `Parameterized` class or object are provided via the `.param` subobject:\n", "\n", "- `.param.update(**kwargs)`: Set parameter values from the given `param=value` keyword arguments (or a dict or iterable), delaying watching and dependency handling until all have been updated. `.param.update` can also be used as a context manager to temporarily set values, that are restored to their original values when the context manager exits." ] diff --git a/param/__init__.py b/param/__init__.py index 7e7d8f322..e0beb4110 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2967,7 +2967,7 @@ def _reset_event(self, obj, val): if obj is None: self.default = val else: - obj.__dict__[self._internal_name] = val + obj._param__private.values[self.name] = val self._post_setter(obj, val) @instance_descriptor diff --git a/param/parameterized.py b/param/parameterized.py index a117fd898..5275ab7d6 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -368,7 +368,7 @@ def no_instance_params(cls): """ Disables instance parameters on the class """ - cls._disable_instance__params = True + cls._param__private.disable_instance_params = True return cls @@ -391,7 +391,11 @@ def iscoroutinefunction(function): def instance_descriptor(f): # If parameter has an instance Parameter, delegate setting def _f(self, obj, val): - instance_param = getattr(obj, '_instance__params', {}).get(self.name) + if obj is None: + # obj is None when the metaclass is setting + return f(self, obj, val) + params = obj._param__private.params + instance_param = None if params is None else params.get(self.name) if instance_param is not None and self is not instance_param: instance_param.__set__(obj, val) return @@ -1062,11 +1066,11 @@ class Foo(Bar): # object (A.__dict__['p'].default). # # * If the value of p is set on a1 (e.g. a1.p=2), a1's value of p - # is stored in a1 itself (a1.__dict__['_p_param_value']) + # is stored in a1 itself (a1._param__private.values['p']) # - # * When a1.p is requested, a1.__dict__['_p_param_value'] is - # returned. When a2.p is requested, '_p_param_value' is not - # found in a2.__dict__, so A.__dict__['p'].default (i.e. A.p) is + # * When a1.p is requested, a1._param__private.values['p'] is + # returned. When a2.p is requested, 'p' is not found in + # a1._param__private.values, so A.__dict__['p'].default (i.e. A.p) is # returned instead. # # @@ -1074,12 +1078,11 @@ class Foo(Bar): # # * A Parameterized class has a name for the attribute which is # being represented by the Parameter ('p' in the example above); - # in the code, this is called the 'attrib_name'. + # in the code, this is called the 'name'. # # * When a Parameterized instance has its own local value for a - # parameter, it is stored as '_X_param_value' (where X is the - # attrib_name for the Parameter); in the code, this is called - # the internal_name. + # parameter, it is stored as 'p._param__private.values[X]' where X is the + # name of the Parameter # So that the extra features of Parameters do not require a lot of @@ -1342,7 +1345,12 @@ def __get__(self, obj, objtype): # pylint: disable-msg=W0613 if obj is None: # e.g. when __get__ called for a Parameterized class result = self.default else: - result = obj.__dict__.get(self._internal_name,self.default) + # Attribute error when .values does not exist (_ClassPrivate) + # and KeyError when there's no cached value for this parameter. + try: + result = obj._param__private.values[self.name] + except (AttributeError, KeyError): + result = self.default return result @instance_descriptor @@ -1355,7 +1363,8 @@ def __set__(self, obj, val): If called for a Parameterized instance, set the value of this Parameter on that instance (i.e. in the instance's - __dict__, under the parameter's internal_name). + `values` dictionary located in the private namespace `_param__private`, + under the parameter's name). If the Parameter's constant attribute is True, only allows the value to be set for a Parameterized class or on @@ -1393,11 +1402,11 @@ def __set__(self, obj, val): elif obj is None: _old = self.default self.default = val - elif not obj.initialized: - _old = obj.__dict__.get(self._internal_name, self.default) - obj.__dict__[self._internal_name] = val + elif not obj._param__private.initialized: + _old = obj._param__private.values.get(self.name, self.default) + obj._param__private.values[self.name] = val else: - _old = obj.__dict__.get(self._internal_name, self.default) + _old = obj._param__private.values.get(self.name, self.default) if val is not _old: raise TypeError("Constant parameter '%s' cannot be modified"%self.name) else: @@ -1405,19 +1414,21 @@ def __set__(self, obj, val): _old = self.default self.default = val else: - _old = obj.__dict__.get(self._internal_name, self.default) - obj.__dict__[self._internal_name] = val + _old = obj._param__private.values.get(self.name, self.default) + obj._param__private.values[self.name] = val self._post_setter(obj, val) if obj is not None: - if not getattr(obj, 'initialized', False): + if not hasattr(obj, '_param__private') or not getattr(obj._param__private, 'initialized', False): return obj.param._update_deps(self.name) if obj is None: watchers = self.watchers.get("value") + # elif hasattr(obj, '_param__private') and hasattr(obj._param__private, 'watchers') and obj._param__private.watchers is not None and self.name in obj._param__private.watchers: elif hasattr(obj, '_param_watchers') and self.name in obj._param_watchers: + # watchers = obj._param__private.watchers[self.name].get('value') watchers = obj._param_watchers[self.name].get('value') if watchers is None: watchers = self.watchers.get("value") @@ -1576,10 +1587,10 @@ def as_uninitialized(fn): @wraps(fn) def override_initialization(self_,*args,**kw): parameterized_instance = self_.self - original_initialized = parameterized_instance.initialized - parameterized_instance.initialized = False + original_initialized = parameterized_instance._param__private.initialized + parameterized_instance._param__private.initialized = False fn(parameterized_instance, *args, **kw) - parameterized_instance.initialized = original_initialized + parameterized_instance._param__private.initialized = original_initialized return override_initialization @@ -1682,35 +1693,35 @@ def __init__(self_, cls, self=None): @property def _BATCH_WATCH(self_): - return self_.self_or_cls._parameters_state['BATCH_WATCH'] + return self_.self_or_cls._param__private.parameters_state['BATCH_WATCH'] @_BATCH_WATCH.setter def _BATCH_WATCH(self_, value): - self_.self_or_cls._parameters_state['BATCH_WATCH'] = value + self_.self_or_cls._param__private.parameters_state['BATCH_WATCH'] = value @property def _TRIGGER(self_): - return self_.self_or_cls._parameters_state['TRIGGER'] + return self_.self_or_cls._param__private.parameters_state['TRIGGER'] @_TRIGGER.setter def _TRIGGER(self_, value): - self_.self_or_cls._parameters_state['TRIGGER'] = value + self_.self_or_cls._param__private.parameters_state['TRIGGER'] = value @property def _events(self_): - return self_.self_or_cls._parameters_state['events'] + return self_.self_or_cls._param__private.parameters_state['events'] @_events.setter def _events(self_, value): - self_.self_or_cls._parameters_state['events'] = value + self_.self_or_cls._param__private.parameters_state['events'] = value @property def _watchers(self_): - return self_.self_or_cls._parameters_state['watchers'] + return self_.self_or_cls._param__private.parameters_state['watchers'] @_watchers.setter def _watchers(self_, value): - self_.self_or_cls._parameters_state['watchers'] = value + self_.self_or_cls._param__private.parameters_state['watchers'] = value @property def watchers(self): @@ -1722,13 +1733,13 @@ def self_or_cls(self_): return self_.cls if self_.self is None else self_.self def __setstate__(self, state): - # Set old parameters state on Parameterized._parameters_state + # Set old parameters state on Parameterized.parameters_state self_, cls = state.get('self'), state.get('cls') self_or_cls = self_ if self_ is not None else cls - for k in self_or_cls._parameters_state: + for k in self_or_cls._param__private.parameters_state: key = '_'+k if key in state: - self_or_cls._parameters_state[k] = state.pop(key) + self_or_cls._param__private.parameters_state[k] = state.pop(key) for k, v in state.items(): setattr(self, k, v) @@ -1739,9 +1750,9 @@ def __getitem__(self_, key): inst = self_.self parameters = self_.objects(False) if inst is None else inst.param.objects(False) p = parameters[key] - if (inst is not None and getattr(inst, 'initialized', False) and p.per_instance and - not getattr(inst, '_disable_instance__params', False)): - if key not in inst._instance__params: + if (inst is not None and getattr(inst._param__private, 'initialized', False) and p.per_instance and + not getattr(self_.cls._param__private, 'disable_instance_params', False)): + if key not in inst._param__private.params: try: # Do not copy watchers on class parameter watchers = p.watchers @@ -1752,9 +1763,9 @@ def __getitem__(self_, key): finally: p.watchers = {k: list(v) for k, v in watchers.items()} p.owner = inst - inst._instance__params[key] = p + inst._param__private.params[key] = p else: - p = inst._instance__params[key] + p = inst._param__private.params[key] return p @@ -1784,9 +1795,8 @@ def __getattr__(self_, attr): if cls is None: # Class not initialized raise AttributeError - try: - params = list(getattr(cls, '_%s__params' % cls.__name__)) - except AttributeError: + params = list(cls._param__private.params) + if not params: params = [n for class_ in classlist(cls) for n, v in class_.__dict__.items() if isinstance(v, Parameter)] @@ -1857,11 +1867,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.__dict__ (or dict_ if supplied) - # under the parameter's _internal_name (or key if supplied) + # deepcopy param_obj.default into self._param__private.values (or dict_ if supplied) + # under the parameter's name (or key if supplied) self = self_.self - dict_ = dict_ or self.__dict__ - key = key or param_obj._internal_name + dict_ = dict_ or self._param__private.values + key = key or param_obj.name if shared_parameters._share: param_key = (str(type(self)), param_obj.name) if param_key in shared_parameters._shared_cache: @@ -1898,7 +1908,7 @@ def _update_deps(self_, attribute=None, init=False): if on_init and m not in init_methods: init_methods.append(m) elif dynamic: - for w in obj._dynamic_watchers.pop(method, []): + for w in obj._param__private.dynamic_watchers.pop(method, []): (w.cls if w.inst is None else w.inst).param.unwatch(w) else: continue @@ -1911,7 +1921,7 @@ def _update_deps(self_, attribute=None, init=False): for group in grouped.values(): watcher = self_._watch_group(obj, method, queued, group, attribute) - obj._dynamic_watchers[method].append(watcher) + obj._param__private.dynamic_watchers[method].append(watcher) for m in init_methods: m() @@ -2034,10 +2044,7 @@ def add_parameter(self_, param_name, param_obj): type.__setattr__(cls,param_name,param_obj) ParameterizedMetaclass._initialize_parameter(cls,param_name,param_obj) # delete cached params() - try: - delattr(cls,'_%s__params'%cls.__name__) - except AttributeError: - pass + cls._param__private.params.clear() # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.add_parameter`") @@ -2166,9 +2173,8 @@ def objects(self_, instance=True): cls = self_.cls # We cache the parameters because this method is called often, # and parameters are rarely added (and cannot be deleted) - try: - pdict = getattr(cls, '_%s__params' % cls.__name__) - except AttributeError: + pdict = cls._param__private.params + if not pdict: paramdict = {} for class_ in classlist(cls): for name, val in class_.__dict__.items(): @@ -2179,13 +2185,14 @@ def objects(self_, instance=True): # params() is called, so we mangle the name ourselves at # runtime (if we were to mangle it now, it would be # _Parameterized.__params for all classes). - setattr(cls, '_%s__params' % cls.__name__, paramdict) + # cls._param__private.params[f'_{cls.__name__}__params'] = paramdict + cls._param__private.params = paramdict pdict = paramdict if instance and self_.self is not None: if instance == 'existing': - if getattr(self_.self, 'initialized', False) and self_.self._instance__params: - return dict(pdict, **self_.self._instance__params) + if getattr(self_.self._param__private, 'initialized', False) and self_.self._param__private.params: + return dict(pdict, **self_.self._param__private.params) return pdict else: return {k: self_.self.param[k] for k in pdict} @@ -2452,10 +2459,10 @@ def get_value_generator(self_,name): # pylint: disable-msg=E0213 # Dynamic Parameter... else: - internal_name = "_%s_param_value"%name - if hasattr(cls_or_slf,internal_name): + # TODO: is this always an instance? + if isinstance(cls_or_slf, Parameterized) and name in cls_or_slf._param__private.values: # dealing with object and it's been set on this object - value = getattr(cls_or_slf,internal_name) + value = cls_or_slf._param__private.values[name] else: # dealing with class or isn't set on the object value = param_obj.default @@ -2638,6 +2645,7 @@ def _register_watcher(self_, action, watcher, what='value'): "parameters of class {}".format(parameter_name, self_.cls.__name__)) if self_.self is not None and what == "value": + # watchers = self_.self._param__private.watchers watchers = self_.self._param_watchers if parameter_name not in watchers: watchers[parameter_name] = {} @@ -3033,22 +3041,17 @@ def __init__(mcs, name, bases, dict_): """ type.__init__(mcs, name, bases, dict_) + _param__private = _ClassPrivate() + mcs._param__private = _param__private mcs.__set_name(name, dict_) - - mcs._parameters_state = { - "BATCH_WATCH": False, # If true, Event and watcher objects are queued. - "TRIGGER": False, - "events": [], # Queue of batched events - "watchers": [] # Queue of batched watchers - } - mcs._param = Parameters(mcs) + mcs._param__parameters = Parameters(mcs) # All objects (with their names) of type Parameter that are # defined in this class parameters = [(n, o) for (n, o) in dict_.items() if isinstance(o, Parameter)] - mcs._param._parameters = dict(parameters) + mcs._param__parameters._parameters = dict(parameters) for param_name,param in parameters: mcs._initialize_parameter(param_name, param) @@ -3071,7 +3074,7 @@ def __init__(mcs, name, bases, dict_): # Resolve dependencies in class hierarchy _inherited = [] for cls in classlist(mcs)[:-1][::-1]: - if not hasattr(cls, '_param'): + if not hasattr(cls, '_param__parameters'): continue for dep in cls.param._depends['watch']: method = getattr(mcs, dep[0], None) @@ -3092,7 +3095,6 @@ def __set_name(mcs, name, dict_): a `name` String Parameter with a defined `default` value, in which case that value is used to set the class name. """ - mcs.__renamed = False name_param = dict_.get("name", None) if name_param is not None: if not type(name_param) is String: @@ -3103,14 +3105,14 @@ def __set_name(mcs, name, dict_): ) if name_param.default: mcs.name = name_param.default - mcs.__renamed = True + mcs._param__private.renamed = True else: mcs.name = name else: classes = classlist(mcs)[::-1] found_renamed = False for c in classes: - if getattr(c, "_ParameterizedMetaclass__renamed", False): + if hasattr(c, '_param__private') and c._param__private.renamed: found_renamed = True break if not found_renamed: @@ -3175,7 +3177,7 @@ def __is_abstract(mcs): abstract = property(__is_abstract) def _get_param(mcs): - return mcs._param + return mcs._param__parameters param = property(_get_param) @@ -3599,6 +3601,99 @@ def _parameterized_repr_html(p, open): ' \n \n\n' ) +# _ClassPrivate and _InstancePrivate are the private namespaces of Parameterized +# classes and instance respectively, stored on the `_param__private` attribute. +# They are implemented with slots for performance reasons. + +class _ClassPrivate: + """ + parameters_state: dict + Dict holding some transient states + disable_instance_params: bool + Whether to disable instance parameters + renamed: bool + Whethe the class has been renamed by a super class + params: dict + Dict of parameter_name:parameter + """ + + __slots__ = [ + 'parameters_state', + 'disable_instance_params', + 'renamed', + 'params', + ] + + def __init__( + self, + parameters_state=None, + disable_instance_params=False, + renamed=False, + params=None, + ): + if parameters_state is None: + parameters_state = { + "BATCH_WATCH": False, # If true, Event and watcher objects are queued. + "TRIGGER": False, + "events": [], # Queue of batched events + "watchers": [] # Queue of batched watchers + } + self.parameters_state = parameters_state + self.disable_instance_params = disable_instance_params + self.renamed = renamed + self.params = {} if params is None else params + + +class _InstancePrivate: + """ + initialized: bool + Flag that can be tested to see if e.g. constant Parameters can still be set + parameters_state: dict + Dict holding some transient states + dynamic_watchers: defaultdict + Dynamic watchers + params: dict + Dict of parameter_name:parameter + # watchers: dict + # Dict of dict: + # parameter_name: + # parameter_attribute (e.g. 'value'): list of `Watcher`s + values: dict + Dict of parameter name: value + """ + + __slots__ = [ + 'initialized', + 'parameters_state', + 'dynamic_watchers', + 'params', + # 'watchers', + 'values', + ] + + def __init__( + self, + initialized=False, + parameters_state=None, + dynamic_watchers=None, + params=None, + # watchers=None, + values=None, + ): + self.initialized = initialized + if parameters_state is None: + parameters_state = { + "BATCH_WATCH": False, # If true, Event and watcher objects are queued. + "TRIGGER": False, + "events": [], # Queue of batched events + "watchers": [] # Queue of batched watchers + } + self.parameters_state = parameters_state + self.dynamic_watchers = defaultdict(list) if dynamic_watchers is None else dynamic_watchers + self.params = {} if params is None else params + # self.watchers = {} if watchers is None else watchers + self.values = {} if values is None else values + class Parameterized(metaclass=ParameterizedMetaclass): """ @@ -3649,18 +3744,8 @@ class Foo(Parameterized): def __init__(self, **params): global object_count - # Flag that can be tested to see if e.g. constant Parameters - # can still be set - self.initialized = False - self._parameters_state = { - "BATCH_WATCH": False, # If true, Event and watcher objects are queued. - "TRIGGER": False, - "events": [], # Queue of batched events - "watchers": [] # Queue of batched watchers - } - self._instance__params = {} + self._param__private = _InstancePrivate() self._param_watchers = {} - self._dynamic_watchers = defaultdict(list) # Skip generating a custom instance name when a class in the hierarchy # has overriden the default of the `name` Parameter. @@ -3671,7 +3756,7 @@ def __init__(self, **params): self.param._update_deps(init=True) - self.initialized = True + self._param__private.initialized = True @property def param(self): @@ -3705,11 +3790,18 @@ def __setstate__(self, state): During this process the object is considered uninitialized. """ - self.initialized=False + self._param__private = _InstancePrivate() + self._param__private.initialized = False + + _param__private = state.get('_param__private', None) + if _param__private is None: + _param__private = _InstancePrivate() # When making a copy the internal watchers have to be # recreated and point to the new instance + # if _param__private.watchers: if '_param_watchers' in state: + # param_watchers = _param__private.watchers param_watchers = state['_param_watchers'] for p, attrs in param_watchers.items(): for attr, watchers in attrs.items(): @@ -3726,15 +3818,14 @@ def __setstate__(self, state): new_watchers.append(Watcher(*watcher_args)) param_watchers[p][attr] = new_watchers - if '_instance__params' not in state: - state['_instance__params'] = {} if '_param_watchers' not in state: state['_param_watchers'] = {} + state.pop('param', None) for name,value in state.items(): setattr(self,name,value) - self.initialized=True + self._param__private.initialized = True @_recursive_repr() def __repr__(self): diff --git a/tests/testaddparameter.py b/tests/testaddparameter.py index 49e3c2740..d9e93ac2a 100644 --- a/tests/testaddparameter.py +++ b/tests/testaddparameter.py @@ -89,12 +89,12 @@ class P(param.Parameterized): # Generate the cache P.param.objects(instance=True) - assert 'x' in P._P__params + assert 'x' in P._param__private.params P.param.add_parameter('y', param.Parameter()) # Check the cache has been removed (not sure why) - assert not hasattr(P, '_P__params') + assert not P._param__private.params def test_add_parameter_subclass(): diff --git a/tests/testdynamicparams.py b/tests/testdynamicparams.py index 8f15adde7..a25e9ce4a 100644 --- a/tests/testdynamicparams.py +++ b/tests/testdynamicparams.py @@ -110,7 +110,7 @@ def test_dynamic_value_setting(self): def test_setting_y_param_numbergen(self): self.TestPO2.y=numbergen.UniformRandom() # now the Parameter instantiate should be true t9 = self.TestPO2() - self.assertEqual('_y_param_value' in t9.__dict__, True) + self.assertEqual('y' in t9._param__private.values, True) def test_shared_numbergen(self): """ diff --git a/tests/testparameterizedobject.py b/tests/testparameterizedobject.py index f44b4711a..5fef7d425 100644 --- a/tests/testparameterizedobject.py +++ b/tests/testparameterizedobject.py @@ -1204,3 +1204,62 @@ def __init__(self, foo=param.Undefined, **params): with pytest.raises(KeyError, match="Slot 'foo' of parameter 'c' has no default value defined in `_slot_defaults`"): class A(param.Parameterized): c = CustomParameter() + + +def _dir(obj): + return [attr for attr in dir(obj) if not attr.startswith('__')] + + +def test_namespace_class(): + + class P(param.Parameterized): + x = param.Parameter() + + @param.depends('x', watch=True) + def foo(self): pass + + P.x = 1 + P.param.x + + assert _dir(P) == [ + '_param__parameters', + '_param__private', + 'foo', + 'name', + 'param', + 'x' + ] + + +def test_namespace_inst(): + + class P(param.Parameterized): + x = param.Parameter() + + @param.depends('x', watch=True) + def foo(self): pass + + p = P(x=2) + p.param.x + + assert _dir(p) == [ + '_param__parameters', + '_param__private', + '_param_watchers', + 'foo', + 'name', + 'param', + 'x' + ] + + +def test_parameterized_access_param_before_super(): + class P(param.Parameterized): + x = param.Parameter(1) + + def __init__(self, **params): + # Reaching out to a Parameter default before calling super + assert self.x == 1 + super().__init__(**params) + + P()