From 7f3bc09045d17cf69c3fe488610330491f4e1d95 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Fri, 11 Feb 2022 17:10:24 -0600 Subject: [PATCH 01/45] Replace None with _Undefined in constructors, and move validation to the ParameterizedMetaclass --- param/__init__.py | 83 ++++++++++++++++++++---------------------- param/parameterized.py | 30 +++++++++++---- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index d51be21c7..797b816b7 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -25,7 +25,7 @@ import datetime as dt import collections -from .parameterized import ( +from .parameterized import ( _Undefined, Parameterized, Parameter, String, ParameterizedFunction, ParamOverrides, descendents, get_logger, instance_descriptor, basestring) @@ -778,8 +778,8 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - def __init__(self, default=0.0, bounds=None, softbounds=None, - inclusive_bounds=(True,True), step=None, **params): + def __init__(self, default=0.0, bounds=_Undefined, softbounds=_Undefined, + inclusive_bounds=(True,True), step=_Undefined, **params): """ Initialize this parameter object and store the bounds. @@ -792,7 +792,6 @@ def __init__(self, default=0.0, bounds=None, softbounds=None, self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step - self._validate(default) def __get__(self, obj, objtype): """ @@ -944,7 +943,7 @@ def _validate_step(self, val, step): class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" - def __init__(self, default=1.0, softbounds=None, **params): + def __init__(self, default=1.0, softbounds=_Undefined, **params): Number.__init__(self, default=default, bounds=(0.0,1.0), softbounds=softbounds, **params) @@ -977,7 +976,7 @@ class Tuple(Parameter): __slots__ = ['length'] - def __init__(self, default=(0,0), length=None, **params): + def __init__(self, default=(0,0), length=_Undefined, **params): """ Initialize a tuple parameter with a fixed length (number of elements). The length is determined by the initial default @@ -985,14 +984,13 @@ def __init__(self, default=(0,0), length=None, **params): length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) - if length is None and default is not None: - self.length = len(default) - elif length is None and default is None: + if length is _Undefined and self.default is not None: + self.length = len(self.default) + elif length is _Undefined and self.default is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: self.length = length - self._validate(default) def _validate_value(self, val, allow_None): if val is None and allow_None: @@ -1114,10 +1112,10 @@ class Composite(Parameter): __slots__ = ['attribs', 'objtype'] - def __init__(self, attribs=None, **kw): - if attribs is None: + def __init__(self, attribs=_Undefined, **kw): + if attribs is _Undefined: attribs = [] - super(Composite, self).__init__(default=None, **kw) + super(Composite, self).__init__(default=_Undefined, **kw) self.attribs = attribs def __get__(self, obj, objtype): @@ -1192,12 +1190,12 @@ class Selector(SelectorBase): # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. - def __init__(self, objects=None, default=None, instantiate=False, - compute_default_fn=None, check_on_set=None, - allow_None=None, empty_default=False, **params): + def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, + compute_default_fn=_Undefined, check_on_set=_Undefined, + allow_None=_Undefined, empty_default=False, **params): autodefault = None - if objects: + if objects is not _Undefined: if is_ordered_dict(objects): autodefault = list(objects.values())[0] elif isinstance(objects, dict): @@ -1209,9 +1207,9 @@ def __init__(self, objects=None, default=None, instantiate=False, elif isinstance(objects, list): autodefault = objects[0] - default = autodefault if (not empty_default and default is None) else default + default = autodefault if (not empty_default and default is _Undefined) else default - if objects is None: + if objects is _Undefined: objects = [] if isinstance(objects, collections_abc.Mapping): self.names = objects @@ -1221,7 +1219,7 @@ def __init__(self, objects=None, default=None, instantiate=False, self.objects = objects self.compute_default_fn = compute_default_fn - if check_on_set is not None: + if check_on_set is not _Undefined: self.check_on_set = check_on_set elif len(objects) == 0: self.check_on_set = False @@ -1232,8 +1230,6 @@ def __init__(self, objects=None, default=None, instantiate=False, default=default, instantiate=instantiate, **params) # Required as Parameter sets allow_None=True if default is None self.allow_None = allow_None - if default is not None and self.check_on_set is True: - self._validate(default) # Note that if the list of objects is changed, the current value for # this parameter in existing POs could be outside of the new range. @@ -1255,6 +1251,10 @@ def _validate(self, val): """ val must be None or one of the objects in self.objects. """ + + if self.default is None or self.check_on_set is True: + return + if not self.check_on_set: self._ensure_value_is_in_objects(val) return @@ -1305,7 +1305,7 @@ class ObjectSelector(Selector): Deprecated. Same as Selector, but with a different constructor for historical reasons. """ - def __init__(self, default=None, objects=None, **kwargs): + def __init__(self, default=_Undefined, objects=_Undefined, **kwargs): super(ObjectSelector,self).__init__(objects=objects, default=default, empty_default=True, **kwargs) @@ -1320,11 +1320,10 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - def __init__(self,class_,default=None,instantiate=True,is_instance=True,**params): + def __init__(self, class_, default=_Undefined, instantiate=True, is_instance=True, **params): self.class_ = class_ self.is_instance = is_instance super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params) - self._validate(default) def _validate(self, val): super(ClassSelector, self)._validate(val) @@ -1384,14 +1383,13 @@ class List(Parameter): __slots__ = ['bounds', 'item_type', 'class_'] - def __init__(self, default=[], class_=None, item_type=None, + def __init__(self, default=[], class_=_Undefined, item_type=_Undefined, instantiate=True, bounds=(0, None), **params): self.item_type = item_type or class_ self.class_ = self.item_type self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, **params) - self._validate(default) def _validate(self, val): """ @@ -1463,7 +1461,7 @@ class Dict(ClassSelector): Parameter whose value is a dictionary. """ - def __init__(self, default=None, **params): + def __init__(self, default=_Undefined, **params): super(Dict, self).__init__(dict, default=default, **params) @@ -1472,7 +1470,7 @@ class Array(ClassSelector): Parameter whose value is a numpy array. """ - def __init__(self, default=None, **params): + def __init__(self, default=_Undefined, **params): from numpy import ndarray super(Array, self).__init__(ndarray, allow_None=True, default=default, **params) @@ -1511,13 +1509,12 @@ class DataFrame(ClassSelector): __slots__ = ['rows', 'columns', 'ordered'] - def __init__(self, default=None, rows=None, columns=None, ordered=None, **params): + def __init__(self, default=_Undefined, rows=_Undefined, columns=_Undefined, ordered=_Undefined, **params): from pandas import DataFrame as pdDFrame self.rows = rows self.columns = columns self.ordered = ordered super(DataFrame,self).__init__(pdDFrame, default=default, **params) - self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' @@ -1588,12 +1585,11 @@ class Series(ClassSelector): __slots__ = ['rows'] - def __init__(self, default=None, rows=None, allow_None=False, **params): + def __init__(self, default=_Undefined, rows=_Undefined, allow_None=False, **params): from pandas import Series as pdSeries self.rows = rows super(Series,self).__init__(pdSeries, default=default, allow_None=allow_None, **params) - self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' @@ -1731,8 +1727,8 @@ class Path(Parameter): __slots__ = ['search_paths'] - def __init__(self, default=None, search_paths=None, **params): - if search_paths is None: + def __init__(self, default=_Undefined, search_paths=_Undefined, **params): + if search_paths is _Undefined: search_paths = [] self.search_paths = search_paths @@ -1832,7 +1828,7 @@ class FileSelector(Selector): """ __slots__ = ['path'] - def __init__(self, default=None, path="", **kwargs): + def __init__(self, default=_Undefined, path="", **kwargs): self.default = default self.path = path self.update() @@ -1860,7 +1856,7 @@ class ListSelector(Selector): a list of possible objects. """ - def __init__(self, default=None, objects=None, **kwargs): + def __init__(self, default=_Undefined, objects=_Undefined, **kwargs): super(ListSelector,self).__init__( objects=objects, default=default, empty_default=True, **kwargs) @@ -1885,7 +1881,7 @@ class MultiFileSelector(ListSelector): """ __slots__ = ['path'] - def __init__(self, default=None, path="", **kwargs): + def __init__(self, default=_Undefined, path="", **kwargs): self.default = default self.path = path self.update() @@ -1911,7 +1907,7 @@ class Date(Number): Date parameter of datetime or date type. """ - def __init__(self, default=None, **kwargs): + def __init__(self, default=_Undefined, **kwargs): super(Date, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): @@ -1955,7 +1951,7 @@ class CalendarDate(Number): Parameter specifically allowing dates (not datetimes). """ - def __init__(self, default=None, **kwargs): + def __init__(self, default=_Undefined, **kwargs): super(CalendarDate, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): @@ -2031,10 +2027,9 @@ class Color(Parameter): __slots__ = ['allow_named'] - def __init__(self, default=None, allow_named=True, **kwargs): + def __init__(self, default=_Undefined, allow_named=True, **kwargs): super(Color, self).__init__(default=default, **kwargs) self.allow_named = allow_named - self._validate(default) def _validate(self, val): self._validate_value(val, self.allow_None) @@ -2067,8 +2062,8 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] - def __init__(self,default=None, bounds=None, softbounds=None, - inclusive_bounds=(True,True), step=None, **params): + def __init__(self, default=_Undefined, bounds=_Undefined, softbounds=_Undefined, + inclusive_bounds=(True,True), step=_Undefined, **params): self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds diff --git a/param/parameterized.py b/param/parameterized.py index 4340f0070..84063c0d7 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -969,7 +969,8 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} - def __init__(self,default=None, doc=None, label=None, precedence=None, # pylint: disable-msg=R0913 + def __init__(self, default=_Undefined, doc=_Undefined, # pylint: disable-msg=R0913 + label=_Undefined, precedence=_Undefined, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True): @@ -1056,7 +1057,7 @@ class hierarchy (see ParameterizedMetaclass). self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = (default is None or allow_None) + self.allow_None = (self.default is None or allow_None) self.watchers = {} self.per_instance = per_instance @@ -1317,7 +1318,7 @@ def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): def __init__(self, default="", regex=None, allow_None=False, **kwargs): super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex - self.allow_None = (default is None or allow_None) + self.allow_None = (self.default is None or allow_None) self._validate(default) def _validate_regex(self, val, regex): @@ -1831,6 +1832,10 @@ def add_parameter(self_, param_name, param_obj): except AttributeError: pass + if hasattr(param_obj, '_validate'): + param_obj._validate(param_obj.default) + + # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 _add_parameter = add_parameter @@ -2680,6 +2685,13 @@ def __init__(mcs, name, bases, dict_): if docstring_signature: mcs.__class_docstring_signature() + # Validation is done here rather than in the Parameter itself so that slot + # values can be inherited along superclasses + for p in mcs.param.objects().values(): + if hasattr(p, '_validate'): + p._validate(p.default) + + def __class_docstring_signature(mcs, max_repr_len=15): """ Autogenerate a keyword signature in the class docstring for @@ -2847,14 +2859,14 @@ def __param_inheritance(mcs,param_name,param): param.instantiate=True del slots['instantiate'] - + supers = classlist(mcs)[::-1] for slot in slots.keys(): - superclasses = iter(classlist(mcs)[::-1]) + superclasses = iter(supers) # Search up the hierarchy until param.slot (which has to # be obtained using getattr(param,slot)) is not None, or # we run out of classes to search. - while getattr(param,slot) is None: + while getattr(param,slot) is _Undefined: try: param_super_class = next(superclasses) except StopIteration: @@ -2865,7 +2877,10 @@ def __param_inheritance(mcs,param_name,param): # (slot might not be there because could be a more # general type of Parameter) new_value = getattr(new_param,slot) - setattr(param,slot,new_value) + if new_value is not _Undefined: + setattr(param, slot, new_value) + if getattr(param, slot) is _Undefined: + setattr(param, slot, None) def get_param_descriptor(mcs,param_name): @@ -3163,6 +3178,7 @@ def __init__(self, **params): self.initialized = True + @property def param(self): return Parameters(self.__class__, self=self) From 8225ae4573528526331bf9c33844b55c50d26a0e Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Fri, 11 Feb 2022 17:10:24 -0600 Subject: [PATCH 02/45] Force TimeSampledFn to have a None default --- numbergen/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numbergen/__init__.py b/numbergen/__init__.py index 2faf2bcb0..363fce5de 100644 --- a/numbergen/__init__.py +++ b/numbergen/__init__.py @@ -699,7 +699,7 @@ class TimeSampledFn(NumberGenerator, TimeDependent): The offset from time 0.0 at which the first sample will be drawn. Must be less than the value of period.""") - fn = param.Callable(doc=""" + fn = param.Callable(None, allow_None=True, doc=""" The time-dependent function used to generate the sampled values.""") From 6aca3868b2d086ef609da31a1720692bb1d9adb9 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Sat, 12 Feb 2022 15:25:10 -0600 Subject: [PATCH 03/45] Reverted numbergen change --- numbergen/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numbergen/__init__.py b/numbergen/__init__.py index 363fce5de..2faf2bcb0 100644 --- a/numbergen/__init__.py +++ b/numbergen/__init__.py @@ -699,7 +699,7 @@ class TimeSampledFn(NumberGenerator, TimeDependent): The offset from time 0.0 at which the first sample will be drawn. Must be less than the value of period.""") - fn = param.Callable(None, allow_None=True, doc=""" + fn = param.Callable(doc=""" The time-dependent function used to generate the sampled values.""") From 98cc7c8ec583350f3006e4a94a73c0656d72e6af Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Sat, 12 Feb 2022 15:25:10 -0600 Subject: [PATCH 04/45] Changed validation to accept _Undefined instead of being deferred --- param/__init__.py | 73 ++++++++++++++++++++++++------------------ param/parameterized.py | 16 +++------ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 797b816b7..5f11fb663 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -730,6 +730,10 @@ def get_soft_bounds(bounds, softbounds): return (l, u) +def un(val): + """Replace _Undefined with None, if present, to allow validation""" + return None if val is _Undefined else val + class Number(Dynamic): """ @@ -792,6 +796,7 @@ def __init__(self, default=0.0, bounds=_Undefined, softbounds=_Undefined, self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step + self._validate(default) def __get__(self, obj, objtype): """ @@ -901,9 +906,9 @@ def _validate(self, val): Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - self._validate_value(val, self.allow_None) - self._validate_step(val, self.step) - self._validate_bounds(val, self.bounds, self.inclusive_bounds) + self._validate_value(un(val), self.allow_None) + self._validate_step(un(val), un(self.step)) + self._validate_bounds(un(val), un(self.bounds), un(self.inclusive_bounds)) def get_soft_bounds(self): return get_soft_bounds(self.bounds, self.softbounds) @@ -991,6 +996,7 @@ def __init__(self, default=(0,0), length=_Undefined, **params): (self.name)) else: self.length = length + self._validate(default) def _validate_value(self, val, allow_None): if val is None and allow_None: @@ -1010,8 +1016,8 @@ def _validate_length(self, val, length): (self.name, len(val), length)) def _validate(self, val): - self._validate_value(val, self.allow_None) - self._validate_length(val, self.length) + self._validate_value(un(val), self.allow_None) + self._validate_length(un(val), un(self.length)) @classmethod def serialize(cls, value): @@ -1135,7 +1141,7 @@ def _validate_attribs(self, val, attribs): (self.name, len(attribs), len(val))) def _validate(self, val): - self._validate_attribs(val, self.attribs) + self._validate_attribs(un(val), un(self.attribs)) def _post_setter(self, obj, val): if obj is None: @@ -1204,7 +1210,7 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, "ordered; should use an ordered dict or " "supply an explicit default value.") autodefault = list(objects.values())[0] - elif isinstance(objects, list): + elif isinstance(objects, list) and len(objects): autodefault = objects[0] default = autodefault if (not empty_default and default is _Undefined) else default @@ -1230,6 +1236,8 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, default=default, instantiate=instantiate, **params) # Required as Parameter sets allow_None=True if default is None self.allow_None = allow_None + if un(default) is not None and self.check_on_set is True: + self._validate(default) # Note that if the list of objects is changed, the current value for # this parameter in existing POs could be outside of the new range. @@ -1251,15 +1259,13 @@ def _validate(self, val): """ val must be None or one of the objects in self.objects. """ - - if self.default is None or self.check_on_set is True: - return + val = un(val) if not self.check_on_set: self._ensure_value_is_in_objects(val) return - if not (val in self.objects or (self.allow_None and val is None)): + if not (un(val) in self.objects or (self.allow_None and un(val) is None)): # This method can be called before __init__ has called # super's __init__, so there may not be any name set yet. if (hasattr(self, "name") and self.name): @@ -1324,10 +1330,11 @@ def __init__(self, class_, default=_Undefined, instantiate=True, is_instance=Tru self.class_ = class_ self.is_instance = is_instance super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params) + self._validate(default) def _validate(self, val): super(ClassSelector, self)._validate(val) - self._validate_class_(val, self.class_, self.is_instance) + self._validate_class_(un(val), self.class_, un(self.is_instance)) def _validate_class_(self, val, class_, is_instance): if (val is None and self.allow_None): @@ -1390,15 +1397,16 @@ def __init__(self, default=[], class_=_Undefined, item_type=_Undefined, self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, **params) + self._validate(default) def _validate(self, val): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - self._validate_value(val, self.allow_None) - self._validate_bounds(val, self.bounds) - self._validate_item_type(val, self.item_type) + self._validate_value(un(val), self.allow_None) + self._validate_bounds(un(val), un(self.bounds)) + self._validate_item_type(un(val), un(self.item_type)) def _validate_bounds(self, val, bounds): "Checks that the list is of the right length and has the right contents." @@ -1535,29 +1543,29 @@ def _validate(self, val): if isinstance(self.columns, set) and self.ordered is True: raise ValueError('Columns cannot be ordered when specified as a set') - if self.allow_None and val is None: + if self.allow_None and un(val) is None: return - if self.columns is None: + if un(self.columns) is None: pass elif (isinstance(self.columns, tuple) and len(self.columns)==2 and all(isinstance(v, (type(None), numbers.Number)) for v in self.columns)): # Numeric bounds tuple self._length_bounds_check(self.columns, len(val.columns), 'Columns') elif isinstance(self.columns, (list, set)): - self.ordered = isinstance(self.columns, list) if self.ordered is None else self.ordered + self.ordered = isinstance(self.columns, list) if un(self.ordered) is None else self.ordered difference = set(self.columns) - set([str(el) for el in val.columns]) if difference: msg = 'Provided DataFrame columns {found} does not contain required columns {expected}' raise ValueError(msg.format(found=list(val.columns), expected=sorted(self.columns))) else: - self._length_bounds_check(self.columns, len(val.columns), 'Column') + self._length_bounds_check(un(self.columns), len(val.columns), 'Column') - if self.ordered: + if un(self.ordered): if list(val.columns) != list(self.columns): msg = 'Provided DataFrame columns {found} must exactly match {expected}' raise ValueError(msg.format(found=list(val.columns), expected=self.columns)) - if self.rows is not None: + if un(self.rows) is not None: self._length_bounds_check(self.rows, len(val), 'Row') @classmethod @@ -1607,11 +1615,11 @@ def _length_bounds_check(self, bounds, length, name): def _validate(self, val): super(Series, self)._validate(val) - if self.allow_None and val is None: + if self.allow_None and un(val) is None: return - if self.rows is not None: - self._length_bounds_check(self.rows, len(val), 'Row') + if un(self.rows) is not None: + self._length_bounds_check(un(self.rows), len(val), 'Row') @@ -1738,7 +1746,7 @@ def _resolve(self, path): return resolve_path(path, path_to_file=None, search_paths=self.search_paths) def _validate(self, val): - if val is None: + if un(val) is None: if not self.allow_None: Parameterized(name="%s.%s"%(self.owner.name,self.name)).param.warning('None is not allowed') else: @@ -1868,7 +1876,7 @@ def compute_default(self): self.objects.append(o) def _validate(self, val): - if (val is None and self.allow_None): + if (un(val) is None and self.allow_None): return for o in val: super(ListSelector, self)._validate(o) @@ -1894,7 +1902,7 @@ def _on_set(self, attribute, old, new): def update(self): self.objects = sorted(glob.glob(self.path)) - if self.default and all([o in self.objects for o in self.default]): + if un(self.default) and all([o in self.objects for o in self.default]): return self.default = self.objects @@ -1915,10 +1923,10 @@ def _validate_value(self, val, allow_None): Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - if self.allow_None and val is None: + if self.allow_None and un(val) is None: return - if not isinstance(val, dt_types) and not (allow_None and val is None): + if not isinstance(un(val), dt_types) and not (allow_None and un(val) is None): raise ValueError( "Date parameter %r only takes datetime and date types, " "not type %r." % (self.name, type(val)) @@ -2030,10 +2038,11 @@ class Color(Parameter): def __init__(self, default=_Undefined, allow_named=True, **kwargs): super(Color, self).__init__(default=default, **kwargs) self.allow_named = allow_named + self._validate(default) def _validate(self, val): - self._validate_value(val, self.allow_None) - self._validate_allow_named(val, self.allow_named) + self._validate_value(un(val), self.allow_None) + self._validate_allow_named(un(val), un(self.allow_named)) def _validate_value(self, val, allow_None): if (allow_None and val is None): @@ -2072,7 +2081,7 @@ def __init__(self, default=_Undefined, bounds=_Undefined, softbounds=_Undefined, def _validate(self, val): super(Range, self)._validate(val) - self._validate_bounds(val, self.bounds, self.inclusive_bounds) + self._validate_bounds(un(val), un(self.bounds), un(self.inclusive_bounds)) def _validate_bounds(self, val, bounds, inclusive_bounds): if bounds is None or (val is None and self.allow_None): diff --git a/param/parameterized.py b/param/parameterized.py index 84063c0d7..1482e728f 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1057,7 +1057,8 @@ class hierarchy (see ParameterizedMetaclass). self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = (self.default is None or allow_None) + self.allow_None = (self.default is _Undefined or + self.default is None or allow_None) self.watchers = {} self.per_instance = per_instance @@ -1248,7 +1249,7 @@ def _validate_value(self, value, allow_None): def _validate(self, val): """Implements validation for the parameter value and attributes""" - self._validate_value(val, self.allow_None) + self._validate_value(None if val is _Undefined else val, self.allow_None) def _post_setter(self, obj, val): """Called after the parameter value has been validated and set""" @@ -1337,7 +1338,7 @@ def _validate_value(self, val, allow_None): def _validate(self, val): self._validate_value(val, self.allow_None) - self._validate_regex(val, self.regex) + self._validate_regex(val, None if self.regex is _Undefined else self.regex) class shared_parameters(object): @@ -1832,9 +1833,6 @@ def add_parameter(self_, param_name, param_obj): except AttributeError: pass - if hasattr(param_obj, '_validate'): - param_obj._validate(param_obj.default) - # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 _add_parameter = add_parameter @@ -2685,12 +2683,6 @@ def __init__(mcs, name, bases, dict_): if docstring_signature: mcs.__class_docstring_signature() - # Validation is done here rather than in the Parameter itself so that slot - # values can be inherited along superclasses - for p in mcs.param.objects().values(): - if hasattr(p, '_validate'): - p._validate(p.default) - def __class_docstring_signature(mcs, max_repr_len=15): """ From df0c9f7c330c89b6a3ea66544938d6b2d054d12d Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Sat, 12 Feb 2022 15:52:25 -0600 Subject: [PATCH 05/45] Minor cleanup --- param/__init__.py | 12 ++++++------ param/parameterized.py | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 5f11fb663..7d3664783 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -989,9 +989,9 @@ def __init__(self, default=(0,0), length=_Undefined, **params): length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) - if length is _Undefined and self.default is not None: - self.length = len(self.default) - elif length is _Undefined and self.default is None: + if length is _Undefined and un(default) is not None: + self.length = len(default) + elif length is _Undefined and un(default) is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: @@ -1201,7 +1201,7 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, allow_None=_Undefined, empty_default=False, **params): autodefault = None - if objects is not _Undefined: + if un(objects): if is_ordered_dict(objects): autodefault = list(objects.values())[0] elif isinstance(objects, dict): @@ -1210,7 +1210,7 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, "ordered; should use an ordered dict or " "supply an explicit default value.") autodefault = list(objects.values())[0] - elif isinstance(objects, list) and len(objects): + elif isinstance(objects, list): autodefault = objects[0] default = autodefault if (not empty_default and default is _Undefined) else default @@ -1265,7 +1265,7 @@ def _validate(self, val): self._ensure_value_is_in_objects(val) return - if not (un(val) in self.objects or (self.allow_None and un(val) is None)): + if not (val in self.objects or (self.allow_None and val is None)): # This method can be called before __init__ has called # super's __init__, so there may not be any name set yet. if (hasattr(self, "name") and self.name): diff --git a/param/parameterized.py b/param/parameterized.py index 1482e728f..9e7515667 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1319,7 +1319,7 @@ def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): def __init__(self, default="", regex=None, allow_None=False, **kwargs): super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex - self.allow_None = (self.default is None or allow_None) + self.allow_None = (default is None or default is _Undefined or allow_None) self._validate(default) def _validate_regex(self, val, regex): @@ -1833,7 +1833,6 @@ def add_parameter(self_, param_name, param_obj): except AttributeError: pass - # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 _add_parameter = add_parameter @@ -2683,7 +2682,6 @@ def __init__(mcs, name, bases, dict_): if docstring_signature: mcs.__class_docstring_signature() - def __class_docstring_signature(mcs, max_repr_len=15): """ Autogenerate a keyword signature in the class docstring for @@ -3170,7 +3168,6 @@ def __init__(self, **params): self.initialized = True - @property def param(self): return Parameters(self.__class__, self=self) From c7766096209f2dadf15f5a1166d8d2de0dab0366 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Sat, 12 Feb 2022 17:53:16 -0600 Subject: [PATCH 06/45] Renamed _Undefined to Undefined --- param/__init__.py | 70 +++++++++++++++++++++--------------------- param/parameterized.py | 28 ++++++++--------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 7d3664783..f8e6e6dfe 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -25,7 +25,7 @@ import datetime as dt import collections -from .parameterized import ( _Undefined, +from .parameterized import ( Undefined, Parameterized, Parameter, String, ParameterizedFunction, ParamOverrides, descendents, get_logger, instance_descriptor, basestring) @@ -731,8 +731,8 @@ def get_soft_bounds(bounds, softbounds): return (l, u) def un(val): - """Replace _Undefined with None, if present, to allow validation""" - return None if val is _Undefined else val + """Replace Undefined with None, if present, to allow validation""" + return None if val is Undefined else val class Number(Dynamic): @@ -782,8 +782,8 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - def __init__(self, default=0.0, bounds=_Undefined, softbounds=_Undefined, - inclusive_bounds=(True,True), step=_Undefined, **params): + def __init__(self, default=0.0, bounds=Undefined, softbounds=Undefined, + inclusive_bounds=(True,True), step=Undefined, **params): """ Initialize this parameter object and store the bounds. @@ -948,7 +948,7 @@ def _validate_step(self, val, step): class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" - def __init__(self, default=1.0, softbounds=_Undefined, **params): + def __init__(self, default=1.0, softbounds=Undefined, **params): Number.__init__(self, default=default, bounds=(0.0,1.0), softbounds=softbounds, **params) @@ -981,7 +981,7 @@ class Tuple(Parameter): __slots__ = ['length'] - def __init__(self, default=(0,0), length=_Undefined, **params): + def __init__(self, default=(0,0), length=Undefined, **params): """ Initialize a tuple parameter with a fixed length (number of elements). The length is determined by the initial default @@ -989,9 +989,9 @@ def __init__(self, default=(0,0), length=_Undefined, **params): length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) - if length is _Undefined and un(default) is not None: + if length is Undefined and un(default) is not None: self.length = len(default) - elif length is _Undefined and un(default) is None: + elif length is Undefined and un(default) is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: @@ -1118,10 +1118,10 @@ class Composite(Parameter): __slots__ = ['attribs', 'objtype'] - def __init__(self, attribs=_Undefined, **kw): - if attribs is _Undefined: + def __init__(self, attribs=Undefined, **kw): + if attribs is Undefined: attribs = [] - super(Composite, self).__init__(default=_Undefined, **kw) + super(Composite, self).__init__(default=Undefined, **kw) self.attribs = attribs def __get__(self, obj, objtype): @@ -1196,9 +1196,9 @@ class Selector(SelectorBase): # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. - def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, - compute_default_fn=_Undefined, check_on_set=_Undefined, - allow_None=_Undefined, empty_default=False, **params): + def __init__(self, objects=Undefined, default=Undefined, instantiate=False, + compute_default_fn=Undefined, check_on_set=Undefined, + allow_None=Undefined, empty_default=False, **params): autodefault = None if un(objects): @@ -1213,9 +1213,9 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, elif isinstance(objects, list): autodefault = objects[0] - default = autodefault if (not empty_default and default is _Undefined) else default + default = autodefault if (not empty_default and default is Undefined) else default - if objects is _Undefined: + if objects is Undefined: objects = [] if isinstance(objects, collections_abc.Mapping): self.names = objects @@ -1225,7 +1225,7 @@ def __init__(self, objects=_Undefined, default=_Undefined, instantiate=False, self.objects = objects self.compute_default_fn = compute_default_fn - if check_on_set is not _Undefined: + if check_on_set is not Undefined: self.check_on_set = check_on_set elif len(objects) == 0: self.check_on_set = False @@ -1311,7 +1311,7 @@ class ObjectSelector(Selector): Deprecated. Same as Selector, but with a different constructor for historical reasons. """ - def __init__(self, default=_Undefined, objects=_Undefined, **kwargs): + def __init__(self, default=Undefined, objects=Undefined, **kwargs): super(ObjectSelector,self).__init__(objects=objects, default=default, empty_default=True, **kwargs) @@ -1326,7 +1326,7 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - def __init__(self, class_, default=_Undefined, instantiate=True, is_instance=True, **params): + def __init__(self, class_, default=Undefined, instantiate=True, is_instance=True, **params): self.class_ = class_ self.is_instance = is_instance super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params) @@ -1390,7 +1390,7 @@ class List(Parameter): __slots__ = ['bounds', 'item_type', 'class_'] - def __init__(self, default=[], class_=_Undefined, item_type=_Undefined, + def __init__(self, default=[], class_=Undefined, item_type=Undefined, instantiate=True, bounds=(0, None), **params): self.item_type = item_type or class_ self.class_ = self.item_type @@ -1469,7 +1469,7 @@ class Dict(ClassSelector): Parameter whose value is a dictionary. """ - def __init__(self, default=_Undefined, **params): + def __init__(self, default=Undefined, **params): super(Dict, self).__init__(dict, default=default, **params) @@ -1478,7 +1478,7 @@ class Array(ClassSelector): Parameter whose value is a numpy array. """ - def __init__(self, default=_Undefined, **params): + def __init__(self, default=Undefined, **params): from numpy import ndarray super(Array, self).__init__(ndarray, allow_None=True, default=default, **params) @@ -1517,7 +1517,7 @@ class DataFrame(ClassSelector): __slots__ = ['rows', 'columns', 'ordered'] - def __init__(self, default=_Undefined, rows=_Undefined, columns=_Undefined, ordered=_Undefined, **params): + def __init__(self, default=Undefined, rows=Undefined, columns=Undefined, ordered=Undefined, **params): from pandas import DataFrame as pdDFrame self.rows = rows self.columns = columns @@ -1593,7 +1593,7 @@ class Series(ClassSelector): __slots__ = ['rows'] - def __init__(self, default=_Undefined, rows=_Undefined, allow_None=False, **params): + def __init__(self, default=Undefined, rows=Undefined, allow_None=False, **params): from pandas import Series as pdSeries self.rows = rows super(Series,self).__init__(pdSeries, default=default, allow_None=allow_None, @@ -1735,8 +1735,8 @@ class Path(Parameter): __slots__ = ['search_paths'] - def __init__(self, default=_Undefined, search_paths=_Undefined, **params): - if search_paths is _Undefined: + def __init__(self, default=Undefined, search_paths=Undefined, **params): + if search_paths is Undefined: search_paths = [] self.search_paths = search_paths @@ -1836,7 +1836,7 @@ class FileSelector(Selector): """ __slots__ = ['path'] - def __init__(self, default=_Undefined, path="", **kwargs): + def __init__(self, default=Undefined, path="", **kwargs): self.default = default self.path = path self.update() @@ -1864,7 +1864,7 @@ class ListSelector(Selector): a list of possible objects. """ - def __init__(self, default=_Undefined, objects=_Undefined, **kwargs): + def __init__(self, default=Undefined, objects=Undefined, **kwargs): super(ListSelector,self).__init__( objects=objects, default=default, empty_default=True, **kwargs) @@ -1889,7 +1889,7 @@ class MultiFileSelector(ListSelector): """ __slots__ = ['path'] - def __init__(self, default=_Undefined, path="", **kwargs): + def __init__(self, default=Undefined, path="", **kwargs): self.default = default self.path = path self.update() @@ -1915,7 +1915,7 @@ class Date(Number): Date parameter of datetime or date type. """ - def __init__(self, default=_Undefined, **kwargs): + def __init__(self, default=Undefined, **kwargs): super(Date, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): @@ -1959,7 +1959,7 @@ class CalendarDate(Number): Parameter specifically allowing dates (not datetimes). """ - def __init__(self, default=_Undefined, **kwargs): + def __init__(self, default=Undefined, **kwargs): super(CalendarDate, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): @@ -2035,7 +2035,7 @@ class Color(Parameter): __slots__ = ['allow_named'] - def __init__(self, default=_Undefined, allow_named=True, **kwargs): + def __init__(self, default=Undefined, allow_named=True, **kwargs): super(Color, self).__init__(default=default, **kwargs) self.allow_named = allow_named self._validate(default) @@ -2071,8 +2071,8 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] - def __init__(self, default=_Undefined, bounds=_Undefined, softbounds=_Undefined, - inclusive_bounds=(True,True), step=_Undefined, **params): + def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, + inclusive_bounds=(True,True), step=Undefined, **params): self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds diff --git a/param/parameterized.py b/param/parameterized.py index 9e7515667..b9ab05e04 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -82,7 +82,7 @@ def get_logger(name=None): object_count = 0 warning_count = 0 -class _Undefined: +class Undefined: """ Dummy value to signal completely undefined values rather than simple None values. @@ -631,11 +631,11 @@ def _skip_event(*events, **kwargs): for e in events: for p in changed: if what == 'value': - old = _Undefined if e.old is None else _getattrr(e.old, p, None) - new = _Undefined if e.new is None else _getattrr(e.new, p, None) + old = Undefined if e.old is None else _getattrr(e.old, p, None) + new = Undefined if e.new is None else _getattrr(e.new, p, None) else: - old = _Undefined if e.old is None else _getattrr(e.old.param[p], what, None) - new = _Undefined if e.new is None else _getattrr(e.new.param[p], what, None) + old = Undefined if e.old is None else _getattrr(e.old.param[p], what, None) + new = Undefined if e.new is None else _getattrr(e.new.param[p], what, None) if not Comparator.is_equal(old, new): return False return True @@ -969,8 +969,8 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} - def __init__(self, default=_Undefined, doc=_Undefined, # pylint: disable-msg=R0913 - label=_Undefined, precedence=_Undefined, + def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 + label=Undefined, precedence=Undefined, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True): @@ -1057,7 +1057,7 @@ class hierarchy (see ParameterizedMetaclass). self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = (self.default is _Undefined or + self.allow_None = (self.default is Undefined or self.default is None or allow_None) self.watchers = {} self.per_instance = per_instance @@ -1249,7 +1249,7 @@ def _validate_value(self, value, allow_None): def _validate(self, val): """Implements validation for the parameter value and attributes""" - self._validate_value(None if val is _Undefined else val, self.allow_None) + self._validate_value(None if val is Undefined else val, self.allow_None) def _post_setter(self, obj, val): """Called after the parameter value has been validated and set""" @@ -1319,7 +1319,7 @@ def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): def __init__(self, default="", regex=None, allow_None=False, **kwargs): super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex - self.allow_None = (default is None or default is _Undefined or allow_None) + self.allow_None = (default is None or default is Undefined or allow_None) self._validate(default) def _validate_regex(self, val, regex): @@ -1338,7 +1338,7 @@ def _validate_value(self, val, allow_None): def _validate(self, val): self._validate_value(val, self.allow_None) - self._validate_regex(val, None if self.regex is _Undefined else self.regex) + self._validate_regex(val, None if self.regex is Undefined else self.regex) class shared_parameters(object): @@ -2856,7 +2856,7 @@ def __param_inheritance(mcs,param_name,param): # Search up the hierarchy until param.slot (which has to # be obtained using getattr(param,slot)) is not None, or # we run out of classes to search. - while getattr(param,slot) is _Undefined: + while getattr(param,slot) is Undefined: try: param_super_class = next(superclasses) except StopIteration: @@ -2867,9 +2867,9 @@ def __param_inheritance(mcs,param_name,param): # (slot might not be there because could be a more # general type of Parameter) new_value = getattr(new_param,slot) - if new_value is not _Undefined: + if new_value is not Undefined: setattr(param, slot, new_value) - if getattr(param, slot) is _Undefined: + if getattr(param, slot) is Undefined: setattr(param, slot, None) From ce9b249462ff73849f84807b45769f0d89fe6489 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Wed, 16 Feb 2022 16:57:16 -0600 Subject: [PATCH 07/45] Replaced un() with _slot_defaults + getattribute --- param/__init__.py | 133 +++++++++++++++++++++++------------------ param/parameterized.py | 37 ++++++++---- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index f8e6e6dfe..10420538b 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -730,9 +730,11 @@ def get_soft_bounds(bounds, softbounds): return (l, u) -def un(val): - """Replace Undefined with None, if present, to allow validation""" - return None if val is Undefined else val + +def dict_update(dictionary, **kw): + d = dictionary.copy() + d.update(kw) + return d class Number(Dynamic): @@ -782,21 +784,22 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - def __init__(self, default=0.0, bounds=Undefined, softbounds=Undefined, - inclusive_bounds=(True,True), step=Undefined, **params): + _slot_defaults = dict_update(Parameter._slot_defaults, default=0.0, inclusive_bounds=(True,True)) + + def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, + inclusive_bounds=Undefined, step=Undefined, **params): """ Initialize this parameter object and store the bounds. Non-dynamic default values are checked against the bounds. """ super(Number,self).__init__(default=default, **params) - self.set_hook = identity_hook self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step - self._validate(default) + self._validate(self.default) def __get__(self, obj, objtype): """ @@ -804,6 +807,9 @@ def __get__(self, obj, objtype): dynamically generated, check the bounds. """ result = super(Number, self).__get__(obj, objtype) + if result is Undefined: + result = self._slot_defaults.default + # Should be able to optimize this commonly used method by # avoiding extra lookups (e.g. _value_is_dynamic() is also # looking up 'result' - should just pass it in). @@ -906,9 +912,9 @@ def _validate(self, val): Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - self._validate_value(un(val), self.allow_None) - self._validate_step(un(val), un(self.step)) - self._validate_bounds(un(val), un(self.bounds), un(self.inclusive_bounds)) + self._validate_value(val, self.allow_None) + self._validate_step(val, self.step) + self._validate_bounds(val, self.bounds, self.inclusive_bounds) def get_soft_bounds(self): return get_soft_bounds(self.bounds, self.softbounds) @@ -924,8 +930,7 @@ def __setstate__(self,state): class Integer(Number): """Numeric Parameter required to be an Integer""" - def __init__(self, default=0, **params): - Number.__init__(self, default=default, **params) + _slot_defaults = dict_update(Number._slot_defaults, default=0) def _validate_value(self, val, allow_None): if callable(val): @@ -948,8 +953,7 @@ def _validate_step(self, val, step): class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" - def __init__(self, default=1.0, softbounds=Undefined, **params): - Number.__init__(self, default=default, bounds=(0.0,1.0), softbounds=softbounds, **params) + _slot_defaults = dict_update(Number._slot_defaults, default=1.0, bounds=(0.0,1.0)) @@ -960,7 +964,9 @@ class Boolean(Parameter): # Bounds are set for consistency and are arguably accurate, but have # no effect since values are either False, True, or None (if allowed). - def __init__(self, default=False, bounds=(0,1), **params): + _slot_defaults = dict_update(Parameter._slot_defaults, default=False, bounds=(0,1)) + + def __init__(self, default=Undefined, bounds=Undefined, **params): self.bounds = bounds super(Boolean, self).__init__(default=default, **params) @@ -981,6 +987,8 @@ class Tuple(Parameter): __slots__ = ['length'] + _slot_defaults = dict_update(Parameter._slot_defaults, default=(0,0)) + def __init__(self, default=(0,0), length=Undefined, **params): """ Initialize a tuple parameter with a fixed length (number of @@ -989,14 +997,14 @@ def __init__(self, default=(0,0), length=Undefined, **params): length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) - if length is Undefined and un(default) is not None: + if length is Undefined and default is not None: self.length = len(default) - elif length is Undefined and un(default) is None: + elif length is Undefined and default is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: self.length = length - self._validate(default) + self._validate(self.default) def _validate_value(self, val, allow_None): if val is None and allow_None: @@ -1016,8 +1024,8 @@ def _validate_length(self, val, length): (self.name, len(val), length)) def _validate(self, val): - self._validate_value(un(val), self.allow_None) - self._validate_length(un(val), un(self.length)) + self._validate_value(val, self.allow_None) + self._validate_length(val, self.length) @classmethod def serialize(cls, value): @@ -1049,7 +1057,9 @@ def _validate_value(self, val, allow_None): class XYCoordinates(NumericTuple): """A NumericTuple for an X,Y coordinate.""" - def __init__(self, default=(0.0, 0.0), **params): + _slot_defaults = dict_update(Parameter._slot_defaults, default=(0.0, 0.0)) + + def __init__(self, default=Undefined, **params): super(XYCoordinates,self).__init__(default=default, length=2, **params) @@ -1141,7 +1151,7 @@ def _validate_attribs(self, val, attribs): (self.name, len(attribs), len(val))) def _validate(self, val): - self._validate_attribs(un(val), un(self.attribs)) + self._validate_attribs(val, self.attribs) def _post_setter(self, obj, val): if obj is None: @@ -1201,7 +1211,7 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=False, allow_None=Undefined, empty_default=False, **params): autodefault = None - if un(objects): + if objects: if is_ordered_dict(objects): autodefault = list(objects.values())[0] elif isinstance(objects, dict): @@ -1236,8 +1246,8 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=False, default=default, instantiate=instantiate, **params) # Required as Parameter sets allow_None=True if default is None self.allow_None = allow_None - if un(default) is not None and self.check_on_set is True: - self._validate(default) + if self.default is not None and self.check_on_set is True: + self._validate(self.default) # Note that if the list of objects is changed, the current value for # this parameter in existing POs could be outside of the new range. @@ -1259,8 +1269,6 @@ def _validate(self, val): """ val must be None or one of the objects in self.objects. """ - val = un(val) - if not self.check_on_set: self._ensure_value_is_in_objects(val) return @@ -1326,15 +1334,17 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - def __init__(self, class_, default=Undefined, instantiate=True, is_instance=True, **params): + _slot_defaults = dict_update(Parameter._slot_defaults, is_instance=True) + + def __init__(self, class_, default=Undefined, instantiate=Undefined, is_instance=Undefined, **params): self.class_ = class_ self.is_instance = is_instance super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params) - self._validate(default) + self._validate(self.default) def _validate(self, val): super(ClassSelector, self)._validate(val) - self._validate_class_(un(val), self.class_, un(self.is_instance)) + self._validate_class_(val, self.class_, self.is_instance) def _validate_class_(self, val, class_, is_instance): if (val is None and self.allow_None): @@ -1397,16 +1407,16 @@ def __init__(self, default=[], class_=Undefined, item_type=Undefined, self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, **params) - self._validate(default) + self._validate(self.default) def _validate(self, val): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - self._validate_value(un(val), self.allow_None) - self._validate_bounds(un(val), un(self.bounds)) - self._validate_item_type(un(val), un(self.item_type)) + self._validate_value(val, self.allow_None) + self._validate_bounds(val, self.bounds) + self._validate_item_type(val, self.item_type) def _validate_bounds(self, val, bounds): "Checks that the list is of the right length and has the right contents." @@ -1523,6 +1533,7 @@ def __init__(self, default=Undefined, rows=Undefined, columns=Undefined, ordered self.columns = columns self.ordered = ordered super(DataFrame,self).__init__(pdDFrame, default=default, **params) + self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' @@ -1543,29 +1554,29 @@ def _validate(self, val): if isinstance(self.columns, set) and self.ordered is True: raise ValueError('Columns cannot be ordered when specified as a set') - if self.allow_None and un(val) is None: + if self.allow_None and val is None: return - if un(self.columns) is None: + if self.columns is None: pass elif (isinstance(self.columns, tuple) and len(self.columns)==2 and all(isinstance(v, (type(None), numbers.Number)) for v in self.columns)): # Numeric bounds tuple self._length_bounds_check(self.columns, len(val.columns), 'Columns') elif isinstance(self.columns, (list, set)): - self.ordered = isinstance(self.columns, list) if un(self.ordered) is None else self.ordered + self.ordered = isinstance(self.columns, list) if self.ordered is None else self.ordered difference = set(self.columns) - set([str(el) for el in val.columns]) if difference: msg = 'Provided DataFrame columns {found} does not contain required columns {expected}' raise ValueError(msg.format(found=list(val.columns), expected=sorted(self.columns))) else: - self._length_bounds_check(un(self.columns), len(val.columns), 'Column') + self._length_bounds_check(self.columns, len(val.columns), 'Column') - if un(self.ordered): + if self.ordered: if list(val.columns) != list(self.columns): msg = 'Provided DataFrame columns {found} must exactly match {expected}' raise ValueError(msg.format(found=list(val.columns), expected=self.columns)) - if un(self.rows) is not None: + if self.rows is not None: self._length_bounds_check(self.rows, len(val), 'Row') @classmethod @@ -1593,11 +1604,12 @@ class Series(ClassSelector): __slots__ = ['rows'] - def __init__(self, default=Undefined, rows=Undefined, allow_None=False, **params): + def __init__(self, default=Undefined, rows=Undefined, allow_None=Undefined, **params): from pandas import Series as pdSeries self.rows = rows super(Series,self).__init__(pdSeries, default=default, allow_None=allow_None, **params) + self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' @@ -1615,11 +1627,11 @@ def _length_bounds_check(self, bounds, length, name): def _validate(self, val): super(Series, self)._validate(val) - if self.allow_None and un(val) is None: + if self.allow_None and val is None: return - if un(self.rows) is not None: - self._length_bounds_check(un(self.rows), len(val), 'Row') + if self.rows is not None: + self._length_bounds_check(self.rows, len(val), 'Row') @@ -1746,7 +1758,7 @@ def _resolve(self, path): return resolve_path(path, path_to_file=None, search_paths=self.search_paths) def _validate(self, val): - if un(val) is None: + if val is None: if not self.allow_None: Parameterized(name="%s.%s"%(self.owner.name,self.name)).param.warning('None is not allowed') else: @@ -1876,7 +1888,7 @@ def compute_default(self): self.objects.append(o) def _validate(self, val): - if (un(val) is None and self.allow_None): + if (val is None and self.allow_None): return for o in val: super(ListSelector, self)._validate(o) @@ -1902,7 +1914,7 @@ def _on_set(self, attribute, old, new): def update(self): self.objects = sorted(glob.glob(self.path)) - if un(self.default) and all([o in self.objects for o in self.default]): + if self.default and all([o in self.objects for o in self.default]): return self.default = self.objects @@ -1915,18 +1927,17 @@ class Date(Number): Date parameter of datetime or date type. """ - def __init__(self, default=Undefined, **kwargs): - super(Date, self).__init__(default=default, **kwargs) + _slot_defaults = dict_update(Number._slot_defaults, default=None) def _validate_value(self, val, allow_None): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ - if self.allow_None and un(val) is None: + if self.allow_None and val is None: return - if not isinstance(un(val), dt_types) and not (allow_None and un(val) is None): + if not isinstance(val, dt_types) and not (allow_None and val is None): raise ValueError( "Date parameter %r only takes datetime and date types, " "not type %r." % (self.name, type(val)) @@ -1959,8 +1970,7 @@ class CalendarDate(Number): Parameter specifically allowing dates (not datetimes). """ - def __init__(self, default=Undefined, **kwargs): - super(CalendarDate, self).__init__(default=default, **kwargs) + _slot_defaults = dict_update(Number._slot_defaults, default=None) def _validate_value(self, val, allow_None): """ @@ -2035,14 +2045,16 @@ class Color(Parameter): __slots__ = ['allow_named'] - def __init__(self, default=Undefined, allow_named=True, **kwargs): + _slot_defaults = dict_update(Parameter._slot_defaults, allow_named=True) + + def __init__(self, default=Undefined, allow_named=Undefined, **kwargs): super(Color, self).__init__(default=default, **kwargs) self.allow_named = allow_named - self._validate(default) + self._validate(self.default) def _validate(self, val): - self._validate_value(un(val), self.allow_None) - self._validate_allow_named(un(val), un(self.allow_named)) + self._validate_value(val, self.allow_None) + self._validate_allow_named(val, self.allow_named) def _validate_value(self, val, allow_None): if (allow_None and val is None): @@ -2071,8 +2083,11 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] + _slot_defaults = dict_update(Tuple._slot_defaults, + default=None, inclusive_bounds=(True,True)) + def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, - inclusive_bounds=(True,True), step=Undefined, **params): + inclusive_bounds=Undefined, step=Undefined, **params): self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds @@ -2081,7 +2096,7 @@ def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, def _validate(self, val): super(Range, self)._validate(val) - self._validate_bounds(un(val), un(self.bounds), un(self.inclusive_bounds)) + self._validate_bounds(val, self.bounds, self.inclusive_bounds) def _validate_bounds(self, val, bounds, inclusive_bounds): if bounds is None or (val is None and self.allow_None): diff --git a/param/parameterized.py b/param/parameterized.py index b9ab05e04..1a3be6695 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -969,11 +969,15 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} + _slot_defaults = dict(instantiate=False, constant=False, readonly=False, + pickle_default_value=True, allow_None=False, + per_instance=True) + def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, - instantiate=False, constant=False, readonly=False, - pickle_default_value=True, allow_None=False, - per_instance=True): + instantiate=False, constant=Undefined, readonly=Undefined, + pickle_default_value=Undefined, allow_None=False, + per_instance=Undefined): """Initialize a new Parameter object and store the supplied attributes: @@ -1051,14 +1055,14 @@ class hierarchy (see ParameterizedMetaclass). self.precedence = precedence self.default = default self.doc = doc - self.constant = constant or readonly # readonly => constant + self.constant = constant is True or readonly is True # readonly => constant self.readonly = readonly self._label = label self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = (self.default is Undefined or - self.default is None or allow_None) + self.allow_None = (default is Undefined or + default is None or allow_None) self.watchers = {} self.per_instance = per_instance @@ -1133,6 +1137,18 @@ def __setattr__(self, attribute, value): if not self.owner.param._BATCH_WATCH: self.owner.param._batch_call_watchers() + def __getattribute__(self, key): + """ + Allow slot values to be Undefined in an "unbound" parameter, i.e. one + that is not (yet) owned by a Parameterized object, in which case their + value will be retrieved from the _slot_defaults dictionary. + """ + v = object.__getattribute__(self, key) + # Safely checks for name (avoiding recursion) to decide if this object is unbound + if v is Undefined and key != "name" and getattr(self, "name", None) is None: + v = self._slot_defaults.get(key, None) + return v + def _on_set(self, attribute, old, value): """ Can be overridden on subclasses to handle changes when parameter @@ -1249,7 +1265,7 @@ def _validate_value(self, value, allow_None): def _validate(self, val): """Implements validation for the parameter value and attributes""" - self._validate_value(None if val is Undefined else val, self.allow_None) + self._validate_value(val, self.allow_None) def _post_setter(self, obj, val): """Called after the parameter value has been validated and set""" @@ -1320,7 +1336,7 @@ def __init__(self, default="", regex=None, allow_None=False, **kwargs): super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex self.allow_None = (default is None or default is Undefined or allow_None) - self._validate(default) + self._validate(self.default) def _validate_regex(self, val, regex): if (val is None and self.allow_None): @@ -1338,7 +1354,7 @@ def _validate_value(self, val, allow_None): def _validate(self, val): self._validate_value(val, self.allow_None) - self._validate_regex(val, None if self.regex is Undefined else self.regex) + self._validate_regex(val, self.regex) class shared_parameters(object): @@ -2870,7 +2886,8 @@ def __param_inheritance(mcs,param_name,param): if new_value is not Undefined: setattr(param, slot, new_value) if getattr(param, slot) is Undefined: - setattr(param, slot, None) + default_val = param._slot_defaults.get(slot, None) + setattr(param, slot, default_val) def get_param_descriptor(mcs,param_name): From 20215a480ca179bb49eaa906aef72f4a814ebf61 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Tue, 15 Mar 2022 19:31:36 -0500 Subject: [PATCH 08/45] Added support for callable default values --- param/__init__.py | 4 ++-- param/parameterized.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 10420538b..decd9a382 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1204,6 +1204,8 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] + _slot_defaults = dict_update(Parameter._slot_defaults, allow_None=False) + # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. def __init__(self, objects=Undefined, default=Undefined, instantiate=False, @@ -1244,8 +1246,6 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=False, super(Selector,self).__init__( default=default, instantiate=instantiate, **params) - # Required as Parameter sets allow_None=True if default is None - 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 1a3be6695..1edba76e0 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -820,6 +820,17 @@ def __getattribute__(mcs,name): else: return type.__getattribute__(mcs,name) +def allow_None_default(self): + """Forces allow_None=True if the param default is None""" + return self.default is None + + +def instantiate_default(self): + """Constant parameters must be instantiated.""" + # instantiate doesn't actually matter for read-only + # parameters, since they can't be set even on a class. But + # having this code avoids needless instantiation. + return False if self.readonly else self.constant @add_metaclass(ParameterMetaclass) @@ -970,13 +981,13 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} _slot_defaults = dict(instantiate=False, constant=False, readonly=False, - pickle_default_value=True, allow_None=False, + pickle_default_value=True, allow_None=allow_None_default, per_instance=True) def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, instantiate=False, constant=Undefined, readonly=Undefined, - pickle_default_value=Undefined, allow_None=False, + pickle_default_value=Undefined, allow_None=Undefined, per_instance=Undefined): """Initialize a new Parameter object and store the supplied attributes: @@ -1061,8 +1072,7 @@ class hierarchy (see ParameterizedMetaclass). self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = (default is Undefined or - default is None or allow_None) + self.allow_None = allow_None self.watchers = {} self.per_instance = per_instance @@ -1147,6 +1157,8 @@ def __getattribute__(self, key): # Safely checks for name (avoiding recursion) to decide if this object is unbound if v is Undefined and key != "name" and getattr(self, "name", None) is None: v = self._slot_defaults.get(key, None) + if callable(v): + v = v(self) return v def _on_set(self, attribute, old, value): @@ -1332,10 +1344,9 @@ def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): __slots__ = ['regex'] - def __init__(self, default="", regex=None, allow_None=False, **kwargs): - super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) + def __init__(self, default="", regex=None, **kwargs): + super(String, self).__init__(default=default, **kwargs) self.regex = regex - self.allow_None = (default is None or default is Undefined or allow_None) self._validate(self.default) def _validate_regex(self, val, regex): @@ -2866,6 +2877,7 @@ def __param_inheritance(mcs,param_name,param): del slots['instantiate'] supers = classlist(mcs)[::-1] + callables = {} for slot in slots.keys(): superclasses = iter(supers) @@ -2887,8 +2899,15 @@ def __param_inheritance(mcs,param_name,param): setattr(param, slot, new_value) if getattr(param, slot) is Undefined: default_val = param._slot_defaults.get(slot, None) - setattr(param, slot, default_val) + if callable(default_val): + callables[slot] = default_val + else: + setattr(param, slot, default_val) + # Once all the static slots have been filled in, fill in the dynamic ones + # (which are only allowed to use static values or results are undefined) + for slot, fn in callables.items(): + setattr(param, slot, fn(param)) def get_param_descriptor(mcs,param_name): """ From 0ad7368c525a1ba8cd06a3a7923ff7f8f8f3010b Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 20 Feb 2023 22:41:12 +0100 Subject: [PATCH 09/45] make dict_update private --- param/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 95c8a35a1..3cfc8aa88 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -735,7 +735,7 @@ def get_soft_bounds(bounds, softbounds): return (l, u) -def dict_update(dictionary, **kw): +def _dict_update(dictionary, **kw): d = dictionary.copy() d.update(kw) return d @@ -824,7 +824,7 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - _slot_defaults = dict_update(Parameter._slot_defaults, default=0.0, inclusive_bounds=(True,True)) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=0.0, inclusive_bounds=(True,True)) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, **params): @@ -970,7 +970,7 @@ def __setstate__(self,state): class Integer(Number): """Numeric Parameter required to be an Integer""" - _slot_defaults = dict_update(Number._slot_defaults, default=0) + _slot_defaults = _dict_update(Number._slot_defaults, default=0) def _validate_value(self, val, allow_None): if callable(val): @@ -993,7 +993,7 @@ def _validate_step(self, val, step): class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" - _slot_defaults = dict_update(Number._slot_defaults, default=1.0, bounds=(0.0,1.0)) + _slot_defaults = _dict_update(Number._slot_defaults, default=1.0, bounds=(0.0,1.0)) @@ -1004,7 +1004,7 @@ class Boolean(Parameter): # Bounds are set for consistency and are arguably accurate, but have # no effect since values are either False, True, or None (if allowed). - _slot_defaults = dict_update(Parameter._slot_defaults, default=False, bounds=(0,1)) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=False, bounds=(0,1)) def __init__(self, default=Undefined, bounds=Undefined, **params): self.bounds = bounds @@ -1027,7 +1027,7 @@ class Tuple(Parameter): __slots__ = ['length'] - _slot_defaults = dict_update(Parameter._slot_defaults, default=(0,0)) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0)) def __init__(self, default=(0,0), length=Undefined, **params): """ @@ -1097,7 +1097,7 @@ def _validate_value(self, val, allow_None): class XYCoordinates(NumericTuple): """A NumericTuple for an X,Y coordinate.""" - _slot_defaults = dict_update(Parameter._slot_defaults, default=(0.0, 0.0)) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0.0, 0.0)) def __init__(self, default=Undefined, **params): super(XYCoordinates,self).__init__(default=default, length=2, **params) @@ -1244,7 +1244,7 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] - _slot_defaults = dict_update(Parameter._slot_defaults, allow_None=False) + _slot_defaults = _dict_update(Parameter._slot_defaults, allow_None=False) # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. @@ -1374,7 +1374,7 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - _slot_defaults = dict_update(Parameter._slot_defaults, is_instance=True) + _slot_defaults = _dict_update(Parameter._slot_defaults, is_instance=True) def __init__(self, class_, default=Undefined, instantiate=Undefined, is_instance=Undefined, **params): self.class_ = class_ @@ -1970,7 +1970,7 @@ class Date(Number): Date parameter of datetime or date type. """ - _slot_defaults = dict_update(Number._slot_defaults, default=None) + _slot_defaults = _dict_update(Number._slot_defaults, default=None) def _validate_value(self, val, allow_None): """ @@ -2013,7 +2013,7 @@ class CalendarDate(Number): Parameter specifically allowing dates (not datetimes). """ - _slot_defaults = dict_update(Number._slot_defaults, default=None) + _slot_defaults = _dict_update(Number._slot_defaults, default=None) def _validate_value(self, val, allow_None): """ @@ -2088,7 +2088,7 @@ class Color(Parameter): __slots__ = ['allow_named'] - _slot_defaults = dict_update(Parameter._slot_defaults, allow_named=True) + _slot_defaults = _dict_update(Parameter._slot_defaults, allow_named=True) def __init__(self, default=Undefined, allow_named=Undefined, **kwargs): super(Color, self).__init__(default=default, **kwargs) @@ -2126,7 +2126,7 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] - _slot_defaults = dict_update(Tuple._slot_defaults, + _slot_defaults = _dict_update(Tuple._slot_defaults, default=None, inclusive_bounds=(True,True)) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, From 5978e38b69d5a2b74f5e47d380d4313ac330ce5d Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 00:45:22 +0100 Subject: [PATCH 10/45] remove unused and broken check --- param/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 3cfc8aa88..aa85f9973 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -847,8 +847,6 @@ def __get__(self, obj, objtype): dynamically generated, check the bounds. """ result = super(Number, self).__get__(obj, objtype) - if result is Undefined: - result = self._slot_defaults.default # Should be able to optimize this commonly used method by # avoiding extra lookups (e.g. _value_is_dynamic() is also From 8d5b09519b4af74bf2dcc89443a295c5d0904c92 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 11:07:46 +0100 Subject: [PATCH 11/45] set slot defaults for String --- param/parameterized.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/param/parameterized.py b/param/parameterized.py index dffb06fa3..ef7fd9c4a 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1355,7 +1355,9 @@ def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): __slots__ = ['regex'] - def __init__(self, default="", regex=None, **kwargs): + _slot_defaults = _dict_update(Parameter._slot_defaults, default="", regex=None) + + def __init__(self, default=Undefined, regex=Undefined, **kwargs): super(String, self).__init__(default=default, **kwargs) self.regex = regex self._validate(self.default) From 86e8d858829898feb32b5327f620f1ea2e267d9e Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 11:08:39 +0100 Subject: [PATCH 12/45] move _dict_update to parameterized --- param/__init__.py | 9 ++------- param/parameterized.py | 9 +++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index aa85f9973..2378d891b 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -27,7 +27,8 @@ from .parameterized import ( Undefined, Parameterized, Parameter, String, ParameterizedFunction, ParamOverrides, - descendents, get_logger, instance_descriptor, basestring, dt_types) + descendents, get_logger, instance_descriptor, basestring, dt_types, + _dict_update) from .parameterized import (batch_watch, depends, output, script_repr, # noqa: api import discard_events, edit_constant, instance_descriptor) @@ -735,12 +736,6 @@ def get_soft_bounds(bounds, softbounds): return (l, u) -def _dict_update(dictionary, **kw): - d = dictionary.copy() - d.update(kw) - return d - - class Bytes(Parameter): """ A Bytes Parameter, with a default value and optional regular diff --git a/param/parameterized.py b/param/parameterized.py index ef7fd9c4a..22050b0a4 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -671,6 +671,15 @@ def caller(*events): return caller +def _dict_update(dictionary, **kwargs): + """ + Small utility to update a copy of a dict with the provided keyword args. + """ + d = dictionary.copy() + d.update(kwargs) + return d + + def _add_doc(obj, docstring): """Add a docstring to a namedtuple, if on python3 where that's allowed""" if sys.version_info[0]>2: From cd2537a48a23c4df8c4e2dbb4c035c81d4fc9e3f Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 11:08:56 +0100 Subject: [PATCH 13/45] add _slots_defaults to Bytes --- param/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/param/__init__.py b/param/__init__.py index 2378d891b..e8cb9729f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -747,6 +747,10 @@ class Bytes(Parameter): __slots__ = ['regex'] + _slot_defaults = _dict_update( + Parameter._slot_defaults, default=b"", regex=None, + ) + def __init__(self, default=b"", regex=None, allow_None=False, **kwargs): super(Bytes, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex From 73a2d4bfca9b902559b65559040d60242c172e6d Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 11:13:22 +0100 Subject: [PATCH 14/45] update slot defaults of the direct super class --- param/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index e8cb9729f..54f3aa781 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -823,7 +823,7 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - _slot_defaults = _dict_update(Parameter._slot_defaults, default=0.0, inclusive_bounds=(True,True)) + _slot_defaults = _dict_update(Dynamic._slot_defaults, default=0.0, inclusive_bounds=(True,True)) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, **params): @@ -1094,7 +1094,7 @@ def _validate_value(self, val, allow_None): class XYCoordinates(NumericTuple): """A NumericTuple for an X,Y coordinate.""" - _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0.0, 0.0)) + _slot_defaults = _dict_update(NumericTuple._slot_defaults, default=(0.0, 0.0)) def __init__(self, default=Undefined, **params): super(XYCoordinates,self).__init__(default=default, length=2, **params) @@ -1241,7 +1241,7 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] - _slot_defaults = _dict_update(Parameter._slot_defaults, allow_None=False) + _slot_defaults = _dict_update(SelectorBase._slot_defaults, allow_None=False) # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. @@ -1371,7 +1371,7 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - _slot_defaults = _dict_update(Parameter._slot_defaults, is_instance=True) + _slot_defaults = _dict_update(SelectorBase._slot_defaults, is_instance=True) def __init__(self, class_, default=Undefined, instantiate=Undefined, is_instance=Undefined, **params): self.class_ = class_ @@ -2123,7 +2123,7 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] - _slot_defaults = _dict_update(Tuple._slot_defaults, + _slot_defaults = _dict_update(NumericTuple._slot_defaults, default=None, inclusive_bounds=(True,True)) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, From 42266e7ce7915eaf7def19193f135285eebc86eb Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 11:14:04 +0100 Subject: [PATCH 15/45] update the docstring to refer to Undefined --- param/parameterized.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index 22050b0a4..02ebf8575 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -2858,9 +2858,9 @@ def __param_inheritance(mcs,param_name,param): given in the object's class, or in its superclasses. For Parameters owned by Parameterized classes, we have implemented an additional level of default lookup, should this ordinary - lookup return only None. + lookup return only `Undefined`. - In such a case, i.e. when no non-None value was found for a + In such a case, i.e. when no non-`Undefined` value was found for a Parameter by the usual inheritance mechanisms, we explicitly look for Parameters with the same name in superclasses of this Parameterized class, and use the first such value that we @@ -2907,7 +2907,7 @@ def __param_inheritance(mcs,param_name,param): superclasses = iter(supers) # Search up the hierarchy until param.slot (which has to - # be obtained using getattr(param,slot)) is not None, or + # be obtained using getattr(param,slot)) is not Undefined, or # we run out of classes to search. while getattr(param,slot) is Undefined: try: From 208356f92b2f2e4b7df058deb78a241d13398aa4 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 13:26:50 +0100 Subject: [PATCH 16/45] update Bytes parameter to use the sentinel --- param/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 54f3aa781..0c2dd106f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -751,11 +751,10 @@ class Bytes(Parameter): Parameter._slot_defaults, default=b"", regex=None, ) - def __init__(self, default=b"", regex=None, allow_None=False, **kwargs): - super(Bytes, self).__init__(default=default, allow_None=allow_None, **kwargs) + def __init__(self, default=Undefined, regex=Undefined, **kwargs): + super(Bytes, self).__init__(default=default, **kwargs) self.regex = regex - self.allow_None = (default is None or allow_None) - self._validate(default) + self._validate(self.default) def _validate_regex(self, val, regex): if (val is None and self.allow_None): From 9e95d28d9d14a9b25f0d9e97bc75a50d4220e088 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 21 Feb 2023 13:46:24 +0100 Subject: [PATCH 17/45] make allow_None_default private --- param/parameterized.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index 02ebf8575..d9a5b1ce5 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -840,7 +840,7 @@ def __getattribute__(mcs,name): else: return type.__getattribute__(mcs,name) -def allow_None_default(self): +def _allow_None_default(self): """Forces allow_None=True if the param default is None""" return self.default is None @@ -1001,7 +1001,7 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} _slot_defaults = dict(instantiate=False, constant=False, readonly=False, - pickle_default_value=True, allow_None=allow_None_default, + pickle_default_value=True, allow_None=_allow_None_default, per_instance=True) def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 From cc6b1da56b2b4b73b31f73fe14abdbd3eab075eb Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 00:49:18 +0100 Subject: [PATCH 18/45] refactor setting allow_None --- param/parameterized.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index 94df916fb..c6eec6e3d 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1001,8 +1001,7 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} _slot_defaults = dict(instantiate=False, constant=False, readonly=False, - pickle_default_value=True, allow_None=_allow_None_default, - per_instance=True) + pickle_default_value=True, allow_None=False, per_instance=True) def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, @@ -1092,7 +1091,7 @@ class hierarchy (see ParameterizedMetaclass). self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value - self.allow_None = allow_None + self._set_allow_None(allow_None) self.watchers = {} self.per_instance = per_instance @@ -1126,6 +1125,18 @@ def label(self): def label(self, val): self._label = val + def _set_allow_None(self, allow_None): + # allow_None is set following these rules (last takes precedence): + # 1. to False by default + # 2. to the value provided in the constructor, if any + # 3. to True if default is None + if self.default is None: + self.allow_None = True + elif allow_None is not Undefined: + self.allow_None = allow_None + else: + self.allow_None = self._slot_defaults['allow_None'] + def _set_instantiate(self,instantiate): """Constant parameters must be instantiated.""" # instantiate doesn't actually matter for read-only From 107df6fbb7a24d664e83c69621a3033730e6b8bb Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 00:49:54 +0100 Subject: [PATCH 19/45] refactor setting instantiate --- param/parameterized.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index c6eec6e3d..5ed9c1bf8 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1005,7 +1005,7 @@ class Foo(Bar): def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, - instantiate=False, constant=Undefined, readonly=Undefined, + instantiate=Undefined, constant=Undefined, readonly=Undefined, pickle_default_value=Undefined, allow_None=Undefined, per_instance=Undefined): @@ -1144,8 +1144,11 @@ def _set_instantiate(self,instantiate): # having this code avoids needless instantiation. if self.readonly: self.instantiate = False - else: + elif instantiate is not Undefined: self.instantiate = instantiate or self.constant # pylint: disable-msg=W0201 + else: + # Default value + self.instantiate = self._slot_defaults['instantiate'] def __setattr__(self, attribute, value): if attribute == 'name' and getattr(self, 'name', None) and value != self.name: From 538e826356d09e7c9e63e776a3518072952c2f56 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 01:14:05 +0100 Subject: [PATCH 20/45] clean up --- param/parameterized.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index d35bf5a0b..f0e20c9a9 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -840,18 +840,6 @@ def __getattribute__(mcs,name): else: return type.__getattribute__(mcs,name) -def _allow_None_default(self): - """Forces allow_None=True if the param default is None""" - return self.default is None - - -def instantiate_default(self): - """Constant parameters must be instantiated.""" - # instantiate doesn't actually matter for read-only - # parameters, since they can't be set even on a class. But - # having this code avoids needless instantiation. - return False if self.readonly else self.constant - @add_metaclass(ParameterMetaclass) class Parameter(object): From 2379afb1956feaea7da47e40ae5d8764d5ffa188 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 01:17:38 +0100 Subject: [PATCH 21/45] fix comparison with Undefined --- param/__init__.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 540f96cbf..0738a68e3 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1240,16 +1240,16 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] - _slot_defaults = _dict_update(SelectorBase._slot_defaults, allow_None=False) + _slot_defaults = _dict_update(SelectorBase._slot_defaults, allow_None=None) # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. - def __init__(self, objects=Undefined, default=Undefined, instantiate=False, + def __init__(self, objects=Undefined, default=Undefined, instantiate=Undefined, compute_default_fn=Undefined, check_on_set=Undefined, allow_None=Undefined, empty_default=False, **params): autodefault = None - if objects: + if objects is not Undefined and objects: if is_ordered_dict(objects): autodefault = list(objects.values())[0] elif isinstance(objects, dict): @@ -1282,6 +1282,9 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=False, super(Selector,self).__init__( default=default, instantiate=instantiate, **params) + # Required as Parameter sets allow_None=True if default is None + if allow_None is Undefined: + self.allow_None = self._slot_defaults['allow_None'] if self.default is not None and self.check_on_set is True: self._validate(self.default) @@ -1370,7 +1373,7 @@ class ClassSelector(SelectorBase): __slots__ = ['class_', 'is_instance'] - _slot_defaults = _dict_update(SelectorBase._slot_defaults, is_instance=True) + _slot_defaults = _dict_update(SelectorBase._slot_defaults, instantiate=True, is_instance=True) def __init__(self, class_, default=Undefined, instantiate=Undefined, is_instance=Undefined, **params): self.class_ = class_ @@ -1436,9 +1439,16 @@ class List(Parameter): __slots__ = ['bounds', 'item_type', 'class_'] + _slot_defaults = _dict_update(Parameter._slot_defaults, class_=None, item_type=None) + def __init__(self, default=[], class_=Undefined, item_type=Undefined, instantiate=True, bounds=(0, None), **params): - self.item_type = item_type or class_ + if item_type is not Undefined: + self.item_type = item_type + elif class_ is not Undefined: + self.item_type = class_ + else: + self.item_type = self._slot_defaults['item_type'] self.class_ = self.item_type self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, @@ -1884,7 +1894,9 @@ class FileSelector(Selector): """ __slots__ = ['path'] - def __init__(self, default=Undefined, path="", **kwargs): + _slot_defaults = _dict_update(Selector._slot_defaults, path="") + + def __init__(self, default=Undefined, path=Undefined, **kwargs): self.default = default self.path = path self.update() From 2298f038b5ceae3786f04eb567f3870f686e6053 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 01:35:58 +0100 Subject: [PATCH 22/45] undo FileSelector changes --- param/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 0738a68e3..dc98aa180 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1894,9 +1894,9 @@ class FileSelector(Selector): """ __slots__ = ['path'] - _slot_defaults = _dict_update(Selector._slot_defaults, path="") + _slot_defaults = _dict_update(Selector._slot_defaults) - def __init__(self, default=Undefined, path=Undefined, **kwargs): + def __init__(self, default=Undefined, path="", **kwargs): self.default = default self.path = path self.update() From 8b5a5e0ca599312082d86dc4a3900a8d60afe00b Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 01:41:25 +0100 Subject: [PATCH 23/45] re-set signature of Bytes --- param/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index dc98aa180..0b2bc04db 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -748,10 +748,10 @@ class Bytes(Parameter): __slots__ = ['regex'] _slot_defaults = _dict_update( - Parameter._slot_defaults, default=b"", regex=None, + Parameter._slot_defaults, default=b"", regex=None, allow_None=False, ) - def __init__(self, default=Undefined, regex=Undefined, **kwargs): + def __init__(self, default=Undefined, regex=Undefined, allow_None=Undefined, **kwargs): super(Bytes, self).__init__(default=default, **kwargs) self.regex = regex self._validate(self.default) From 8fb5f0911b0c8ac6d5717227fff0ac264bdc092f Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 15 Mar 2023 02:38:19 +0100 Subject: [PATCH 24/45] all slots must have a default value --- param/__init__.py | 24 +++++++++++++--- param/parameterized.py | 11 ++++--- tests/API1/testdefaults.py | 59 ++++++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 0b2bc04db..c63ab80b8 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -822,7 +822,10 @@ class Number(Dynamic): __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] - _slot_defaults = _dict_update(Dynamic._slot_defaults, default=0.0, inclusive_bounds=(True,True)) + _slot_defaults = _dict_update( + Dynamic._slot_defaults, default=0.0, bounds=None, softbounds=None, + inclusive_bounds=(True,True), step=None + ) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, **params): @@ -1240,7 +1243,10 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] - _slot_defaults = _dict_update(SelectorBase._slot_defaults, allow_None=None) + _slot_defaults = _dict_update( + SelectorBase._slot_defaults, objects=None, compute_default_fn=None, + check_on_set=None, allow_None=None + ) # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. @@ -1573,6 +1579,10 @@ class DataFrame(ClassSelector): __slots__ = ['rows', 'columns', 'ordered'] + _slot_defaults = _dict_update( + ClassSelector._slot_defaults, rows=None, columns=None, ordered=None + ) + def __init__(self, default=Undefined, rows=Undefined, columns=Undefined, ordered=Undefined, **params): from pandas import DataFrame as pdDFrame self.rows = rows @@ -1650,6 +1660,10 @@ class Series(ClassSelector): __slots__ = ['rows'] + _slot_defaults = _dict_update( + ClassSelector._slot_defaults, rows=None, allow_None=False + ) + def __init__(self, default=Undefined, rows=Undefined, allow_None=Undefined, **params): from pandas import Series as pdSeries self.rows = rows @@ -2134,8 +2148,10 @@ class Range(NumericTuple): __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] - _slot_defaults = _dict_update(NumericTuple._slot_defaults, - default=None, inclusive_bounds=(True,True)) + _slot_defaults = _dict_update( + NumericTuple._slot_defaults, default=None, bounds=None, + inclusive_bounds=(True,True), softbounds=None, step=None + ) def __init__(self, default=Undefined, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, **params): diff --git a/param/parameterized.py b/param/parameterized.py index f0e20c9a9..f13c0d4e0 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -988,8 +988,11 @@ class Foo(Bar): _serializers = {'json': serializer.JSONSerialization} - _slot_defaults = dict(instantiate=False, constant=False, readonly=False, - pickle_default_value=True, allow_None=False, per_instance=True) + _slot_defaults = dict( + default=None, precedence=None, doc=None, _label=None, instantiate=False, + constant=False, readonly=False, pickle_default_value=True, allow_None=False, + per_instance=True + ) def __init__(self, default=Undefined, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, @@ -1178,7 +1181,7 @@ def __getattribute__(self, key): v = object.__getattribute__(self, key) # Safely checks for name (avoiding recursion) to decide if this object is unbound if v is Undefined and key != "name" and getattr(self, "name", None) is None: - v = self._slot_defaults.get(key, None) + v = self._slot_defaults[key] if callable(v): v = v(self) return v @@ -2925,7 +2928,7 @@ def __param_inheritance(mcs,param_name,param): if new_value is not Undefined: setattr(param, slot, new_value) if getattr(param, slot) is Undefined: - default_val = param._slot_defaults.get(slot, None) + default_val = param._slot_defaults[slot] if callable(default_val): callables[slot] = default_val else: diff --git a/tests/API1/testdefaults.py b/tests/API1/testdefaults.py index 2fff70146..3c8dd2a2f 100644 --- a/tests/API1/testdefaults.py +++ b/tests/API1/testdefaults.py @@ -15,10 +15,10 @@ from .utils import check_defaults positional_args = { -# ClassSelector: (object,) + ClassSelector: (object,) } -skip = ['ClassSelector'] +skip = [] try: import numpy # noqa @@ -37,15 +37,64 @@ def __new__(mcs, name, bases, dict_): def test_skip(*args,**kw): pytest.skip() - def add_test(p): + def add_test_unbound(parameter): def test(self): # instantiate parameter with no default (but supply # any required args) - p(*positional_args.get(p,tuple())) + p = parameter(*positional_args.get(parameter,tuple())) + + for slot in param.parameterized.get_all_slots(parameter): + # Handled in a special way, skip it + if parameter == param.Composite and slot == 'objtype': + continue + assert getattr(p, slot) is not param.Undefined + + return test + + def add_test_class(parameter): + def test(self): + # instantiate parameter with no default (but supply + # any required args) + class P(param.Parameterized): + p = parameter(*positional_args.get(parameter,tuple())) + + for slot in param.parameterized.get_all_slots(parameter): + # Handled in a special way, skip it + if type(parameter) == param.Composite and slot == 'objtype': + continue + assert getattr(P.param.p, slot) is not param.Undefined + # Handled in a special way, skip it + if parameter == param.Composite: + continue + assert P.p == P.param.p.default + + return test + + def add_test_inst(parameter): + def test(self): + # instantiate parameter with no default (but supply + # any required args) + class P(param.Parameterized): + p = parameter(*positional_args.get(parameter,tuple())) + + inst = P() + + for slot in param.parameterized.get_all_slots(parameter): + # Handled in a special way, skip it + if type(parameter) == param.Composite and slot == 'objtype': + continue + assert getattr(inst.param.p, slot) is not param.Undefined + # Handled in a special way, skip it + if parameter == param.Composite: + continue + assert inst.p == inst.param.p.default + return test for p_name, p_type in concrete_descendents(Parameter).items(): - dict_["test_default_of_%s"%p_name] = add_test(p_type) if p_name not in skip else test_skip + dict_["test_default_of_unbound_%s"%p_name] = add_test_unbound(p_type) if p_name not in skip else test_skip + dict_["test_default_of_class_%s"%p_name] = add_test_class(p_type) if p_name not in skip else test_skip + dict_["test_default_of_inst_%s"%p_name] = add_test_inst(p_type) if p_name not in skip else test_skip return type.__new__(mcs, name, bases, dict_) From dd634a6f2984900a6463d4f870c8a7f58f465d85 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:12:28 +0100 Subject: [PATCH 25/45] dynamic update of Tuple length --- param/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index c63ab80b8..2fabb0c3f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1020,15 +1020,18 @@ def _validate_value(self, val, allow_None): "not %s." % (self.name, val)) +def _update_length(p): + return len(p.default) + class Tuple(Parameter): """A tuple Parameter (e.g. ('a',7.6,[3,5])) with a fixed tuple length.""" __slots__ = ['length'] - _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0)) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0), length=_update_length) - def __init__(self, default=(0,0), length=Undefined, **params): + def __init__(self, default=Undefined, length=Undefined, **params): """ Initialize a tuple parameter with a fixed length (number of elements). The length is determined by the initial default @@ -1036,9 +1039,7 @@ def __init__(self, default=(0,0), length=Undefined, **params): length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) - if length is Undefined and default is not None: - self.length = len(default) - elif length is Undefined and default is None: + if length is Undefined and self.default is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: From f765b5629ec1c03d651422cce8f04e4bc417507e Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:12:58 +0100 Subject: [PATCH 26/45] define Selector default values --- param/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/param/__init__.py b/param/__init__.py index 2fabb0c3f..bb44e9d51 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1246,7 +1246,7 @@ class Selector(SelectorBase): _slot_defaults = _dict_update( SelectorBase._slot_defaults, objects=None, compute_default_fn=None, - check_on_set=None, allow_None=None + check_on_set=None, allow_None=None, instantiate=False, default=None, ) # Selector is usually used to allow selection from a list of From 88f9a9d8a91a0531584b78dfac258943014b7122 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:13:11 +0100 Subject: [PATCH 27/45] define List default values --- param/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index bb44e9d51..25d5ed378 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1446,10 +1446,13 @@ class List(Parameter): __slots__ = ['bounds', 'item_type', 'class_'] - _slot_defaults = _dict_update(Parameter._slot_defaults, class_=None, item_type=None) + _slot_defaults = _dict_update( + Parameter._slot_defaults, class_=None, item_type=None, bounds=(0, None), + instantiate=True, + ) def __init__(self, default=[], class_=Undefined, item_type=Undefined, - instantiate=True, bounds=(0, None), **params): + instantiate=Undefined, bounds=Undefined, **params): if item_type is not Undefined: self.item_type = item_type elif class_ is not Undefined: From d1c44aae9f18f10c3bb57cad473831941450562b Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:36:10 +0100 Subject: [PATCH 28/45] fix List inheritance --- param/__init__.py | 10 +++++----- tests/API1/testlist.py | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 25d5ed378..3313d0237 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1448,17 +1448,17 @@ class List(Parameter): _slot_defaults = _dict_update( Parameter._slot_defaults, class_=None, item_type=None, bounds=(0, None), - instantiate=True, + instantiate=True, default=[], ) - def __init__(self, default=[], class_=Undefined, item_type=Undefined, + def __init__(self, default=Undefined, class_=Undefined, item_type=Undefined, instantiate=Undefined, bounds=Undefined, **params): - if item_type is not Undefined: + if item_type is not Undefined and class_ is not Undefined: self.item_type = item_type - elif class_ is not Undefined: + elif item_type is Undefined or item_type is None: self.item_type = class_ else: - self.item_type = self._slot_defaults['item_type'] + self.item_type = item_type self.class_ = self.item_type self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, diff --git a/tests/API1/testlist.py b/tests/API1/testlist.py index 9004d1b33..b298e93ef 100644 --- a/tests/API1/testlist.py +++ b/tests/API1/testlist.py @@ -184,13 +184,13 @@ class B(A): p = param.List() # B does not inherit default from A - assert B.param.p.default == [] + assert B.param.p.default == [0 ,1] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) b = B() - assert b.param.p.default == [] + assert b.param.p.default == [0, 1] assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) @@ -202,15 +202,15 @@ class B(A): p = param.List() # B does not inherit default and bounds from A - assert B.param.p.default == [] + assert B.param.p.default == [0, 1] assert B.param.p.instantiate is True - assert B.param.p.bounds == (0, None) + assert B.param.p.bounds == (1, 10) b = B() - assert b.param.p.default == [] + assert b.param.p.default == [0, 1] assert b.param.p.instantiate is True - assert b.param.p.bounds == (0, None) + assert b.param.p.bounds == (1, 10) def test_inheritance_behavior4(self): class A(param.Parameterized): @@ -220,14 +220,14 @@ class B(A): p = param.List() # B inherit item_type - assert B.param.p.default == [] + assert B.param.p.default == [0] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) assert B.param.p.item_type == int b = B() - assert b.param.p.default == [] + assert b.param.p.default == [0] assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) assert b.param.p.item_type == int @@ -240,14 +240,14 @@ class B(A): p = param.List() # B does not inherit allow_None - assert B.param.p.default == [] + assert B.param.p.default == [0, 1] assert B.param.p.allow_None is False assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) b = B() - assert b.param.p.default == [] + assert b.param.p.default == [0, 1] assert b.param.p.allow_None is False assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) @@ -261,10 +261,10 @@ class B(A): assert B.param.p.default == [0, 1, 2, 3] assert B.param.p.instantiate is True - assert B.param.p.bounds == (0, None) + assert B.param.p.bounds == (1, 10) b = B() assert b.param.p.default == [0, 1, 2, 3] assert b.param.p.instantiate is True - assert b.param.p.bounds == (0, None) + assert b.param.p.bounds == (1, 10) From 15c3cd69f5f569da3559a8995347477e37791c55 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:37:45 +0100 Subject: [PATCH 29/45] fix comments --- tests/API1/testlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/API1/testlist.py b/tests/API1/testlist.py index b298e93ef..6d687d646 100644 --- a/tests/API1/testlist.py +++ b/tests/API1/testlist.py @@ -183,7 +183,7 @@ class A(param.Parameterized): class B(A): p = param.List() - # B does not inherit default from A + # B inherits default from A assert B.param.p.default == [0 ,1] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) @@ -201,7 +201,7 @@ class A(param.Parameterized): class B(A): p = param.List() - # B does not inherit default and bounds from A + # B inherits default and bounds from A assert B.param.p.default == [0, 1] assert B.param.p.instantiate is True assert B.param.p.bounds == (1, 10) From 1211088964e3ca4698653b3c4a9cd71044da58b9 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 11:40:19 +0100 Subject: [PATCH 30/45] update Number behavior test --- tests/API1/testnumberparameter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/API1/testnumberparameter.py b/tests/API1/testnumberparameter.py index 2e1983c52..3e22bbc7a 100644 --- a/tests/API1/testnumberparameter.py +++ b/tests/API1/testnumberparameter.py @@ -310,15 +310,15 @@ class B(A): assert A.param.p.default == f assert A.param.p.instantiate is True - # Default is not inherited but instantiate is still set to True - assert B.p == 0.0 - assert B.param.p.default == 0.0 + # Default is inherited + assert B.p == 2 + assert B.param.p.default == f assert B.param.p.instantiate is True b = B() - assert b.p == 0.0 - assert b.param.p.default == 0.0 + assert b.p == 2 + assert b.param.p.default == f assert b.param.p.instantiate is True From 3d9f6fd233ec198ef10f27be64de691cf967dc48 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 12:54:35 +0100 Subject: [PATCH 31/45] fix Selector behavior and adjust tests --- param/__init__.py | 28 ++++++++++++---------- tests/API1/testobjectselector.py | 39 ++++++++++++++----------------- tests/API1/testselector.py | 40 ++++++++++++++------------------ 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 3313d0237..6ab1a801f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1215,6 +1215,17 @@ def get_range(self): raise NotImplementedError("get_range() must be implemented in subclasses.") +def _update_selector_default(p): + return [] + + +def _update_selector_check_on_set(p): + if len(p.objects) == 0: + return False + else: + return True + + class Selector(SelectorBase): """ Parameter whose value must be one object from a list of possible objects. @@ -1245,8 +1256,9 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] _slot_defaults = _dict_update( - SelectorBase._slot_defaults, objects=None, compute_default_fn=None, - check_on_set=None, allow_None=None, instantiate=False, default=None, + SelectorBase._slot_defaults, objects=_update_selector_default, + compute_default_fn=None, check_on_set=_update_selector_check_on_set, + allow_None=None, instantiate=False, default=None, ) # Selector is usually used to allow selection from a list of @@ -1255,7 +1267,7 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=Undefined, compute_default_fn=Undefined, check_on_set=Undefined, allow_None=Undefined, empty_default=False, **params): - autodefault = None + autodefault = Undefined if objects is not Undefined and objects: if is_ordered_dict(objects): autodefault = list(objects.values())[0] @@ -1270,8 +1282,6 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=Undefined, default = autodefault if (not empty_default and default is Undefined) else default - if objects is Undefined: - objects = [] if isinstance(objects, collections_abc.Mapping): self.names = objects self.objects = list(objects.values()) @@ -1279,13 +1289,7 @@ def __init__(self, objects=Undefined, default=Undefined, instantiate=Undefined, self.names = None self.objects = objects self.compute_default_fn = compute_default_fn - - if check_on_set is not Undefined: - self.check_on_set = check_on_set - elif len(objects) == 0: - self.check_on_set = False - else: - self.check_on_set = True + self.check_on_set = check_on_set super(Selector,self).__init__( default=default, instantiate=instantiate, **params) diff --git a/tests/API1/testobjectselector.py b/tests/API1/testobjectselector.py index 90aeb41e1..4b790ff8d 100644 --- a/tests/API1/testobjectselector.py +++ b/tests/API1/testobjectselector.py @@ -261,16 +261,15 @@ class A(param.Parameterized): class B(A): p = param.ObjectSelector() - # B does not inherit objects from A - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default is None - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default is None - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior3(self): class A(param.Parameterized): @@ -279,17 +278,15 @@ class A(param.Parameterized): class B(A): p = param.ObjectSelector() - # B does not inherit objects from A but the default gets anyway set to 1 - # and check_on_set is False - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior4(self): class A(param.Parameterized): @@ -298,13 +295,13 @@ class A(param.Parameterized): class B(A): p = param.ObjectSelector() - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default is None assert B.param.p.check_on_set is False b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default is None assert b.param.p.check_on_set is False @@ -315,15 +312,15 @@ class A(param.Parameterized): class B(A): p = param.ObjectSelector() - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default is None - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default is None - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior6(self): class A(param.Parameterized): @@ -332,12 +329,12 @@ class A(param.Parameterized): class B(A): p = param.ObjectSelector(default=1) - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True diff --git a/tests/API1/testselector.py b/tests/API1/testselector.py index 8b7f2e46c..7a3d0cd0a 100644 --- a/tests/API1/testselector.py +++ b/tests/API1/testselector.py @@ -259,17 +259,15 @@ class A(param.Parameterized): class B(A): p = param.Selector() - # B does not inherit objects from A but the default gets anyway auto-set - # to 0 and check_on_set is False - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior3(self): class A(param.Parameterized): @@ -278,17 +276,15 @@ class A(param.Parameterized): class B(A): p = param.Selector() - # B does not inherit objects from A but the default gets anyway set to 1 - # and check_on_set is False - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior4(self): class A(param.Parameterized): @@ -297,13 +293,13 @@ class A(param.Parameterized): class B(A): p = param.Selector() - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 assert B.param.p.check_on_set is False b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 assert b.param.p.check_on_set is False @@ -314,15 +310,15 @@ class A(param.Parameterized): class B(A): p = param.Selector() - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True def test_inheritance_behavior6(self): class A(param.Parameterized): @@ -331,12 +327,12 @@ class A(param.Parameterized): class B(A): p = param.Selector(default=1) - assert B.param.p.objects == [] + assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 - assert B.param.p.check_on_set is False + assert B.param.p.check_on_set is True b = B() - assert b.param.p.objects == [] + assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 - assert b.param.p.check_on_set is False + assert b.param.p.check_on_set is True From 854a8cb2fae16fa1d0c03111dca6b5f5ae9b7e7a Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 12:56:02 +0100 Subject: [PATCH 32/45] adjust tuple tests --- tests/API1/testtupleparam.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/API1/testtupleparam.py b/tests/API1/testtupleparam.py index a333b062f..dad753c1b 100644 --- a/tests/API1/testtupleparam.py +++ b/tests/API1/testtupleparam.py @@ -139,13 +139,14 @@ class A(param.Parameterized): class B(A): p = param.Tuple() - # B inherits the default (0, 0) and its computed length - assert B.p == (0, 0) - assert B.param.p.length == 2 + assert B.p == (0, 1, 2) + assert B.param.p.length == 3 b = B() - assert b.param.p.length == 2 + assert b.p == (0, 1, 2) + assert b.param.p.default == (0, 1, 2) + assert b.param.p.length == 3 def test_inheritance_length_behavior2(self): class A(param.Parameterized): @@ -154,13 +155,14 @@ class A(param.Parameterized): class B(A): p = param.Tuple() - # B inherits the default (0, 0) and its explicit length - assert B.p == (0, 0) - assert B.param.p.length == 2 + assert B.p == (0, 1, 2) + assert B.param.p.length == 3 b = B() - assert b.param.p.length == 2 + assert b.p == (0, 1, 2) + assert b.param.p.default == (0, 1, 2) + assert b.param.p.length == 3 class TestNumericTupleParameters(API1TestCase): From 59843b28a7dedf7dcc2610b315763f176f35e342 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 16 Mar 2023 12:57:38 +0100 Subject: [PATCH 33/45] adjust parameterized tests --- tests/API1/testparameterizedobject.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/API1/testparameterizedobject.py b/tests/API1/testparameterizedobject.py index f6f56b1e9..190387801 100644 --- a/tests/API1/testparameterizedobject.py +++ b/tests/API1/testparameterizedobject.py @@ -511,7 +511,7 @@ class B(A): b = B() - assert b.p == 'test' + assert b.p is None @pytest.mark.parametrize('attribute', [ @@ -543,7 +543,7 @@ class B(A): b = B() - assert getattr(b.param.p, attribute) == 'test' + assert getattr(b.param.p, attribute) is None def test_inheritance_no_default_declared_in_subclass(): @@ -555,7 +555,7 @@ class B(A): p = param.Number() b = B() - assert b.p == 0.0 + assert b.p == 5.0 def test_inheritance_attribute_from_non_subclass_not_inherited(): @@ -591,7 +591,8 @@ class B(A): b = B() - assert b.p == 0.0 + # Could argue this should not be allowed. + assert b.p == '1' def test_inheritance_default_is_None_in_sub(): From b522d4a62355011a7512c63ad44fcc8b880dc957 Mon Sep 17 00:00:00 2001 From: maximlt Date: Fri, 17 Mar 2023 09:50:12 +0100 Subject: [PATCH 34/45] set Tuple length when default is provided --- param/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/param/__init__.py b/param/__init__.py index 6ab1a801f..8ef080147 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1042,6 +1042,8 @@ def __init__(self, default=Undefined, length=Undefined, **params): if length is Undefined and self.default is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) + elif default is not Undefined and default: + self.length = len(default) else: self.length = length self._validate(self.default) From 953edb482a786f857b4ae61d0f2a01a39455a7de Mon Sep 17 00:00:00 2001 From: maximlt Date: Fri, 17 Mar 2023 09:52:15 +0100 Subject: [PATCH 35/45] raise when Undefined is compared --- param/parameterized.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/param/parameterized.py b/param/parameterized.py index f13c0d4e0..e5962ad52 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -91,12 +91,21 @@ def get_logger(name=None): object_count = 0 warning_count = 0 -class Undefined: +class _Undefined: """ Dummy value to signal completely undefined values rather than simple None values. """ + def __bool__(self): + # Haven't defined whether Undefined is falsy or truthy, + # so to avoid subtle bugs raise an error when it + # is used in a comparison without `is`. + raise RuntimeError('Use `is` to compare Undefined') + +Undefined = _Undefined() + + @contextmanager def logging_level(level): """ From 576e07973ab0b5ff410050f1d6ae34183b8c9814 Mon Sep 17 00:00:00 2001 From: maximlt Date: Fri, 17 Mar 2023 10:17:47 +0100 Subject: [PATCH 36/45] raise better error when a slot has no default value --- param/parameterized.py | 16 +++++++++++-- tests/API1/testcustomparam.py | 22 ++++++++++++++++++ tests/API1/testparameterizedobject.py | 33 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index e5962ad52..8a6629158 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1190,7 +1190,13 @@ def __getattribute__(self, key): v = object.__getattribute__(self, key) # Safely checks for name (avoiding recursion) to decide if this object is unbound if v is Undefined and key != "name" and getattr(self, "name", None) is None: - v = self._slot_defaults[key] + try: + v = self._slot_defaults[key] + except KeyError as e: + raise KeyError( + f'Slot {key!r} on unbound parameter {self.__class__.__name__!r} ' + 'has no default value defined in `_slot_defaults`' + ) from e if callable(v): v = v(self) return v @@ -2937,7 +2943,13 @@ def __param_inheritance(mcs,param_name,param): if new_value is not Undefined: setattr(param, slot, new_value) if getattr(param, slot) is Undefined: - default_val = param._slot_defaults[slot] + try: + default_val = param._slot_defaults[slot] + except KeyError as e: + raise KeyError( + f'Slot {slot!r} of parameter {param_name!r} has no ' + 'default value defined in `_slot_defaults`' + ) from e if callable(default_val): callables[slot] = default_val else: diff --git a/tests/API1/testcustomparam.py b/tests/API1/testcustomparam.py index ae20520a2..d5431fc72 100644 --- a/tests/API1/testcustomparam.py +++ b/tests/API1/testcustomparam.py @@ -114,3 +114,25 @@ class B(A): assert B.param.c.doc == 'bar' assert B().param.c.doc == 'bar' + +def test_inheritance_parameter_attribute_without_default(): + + class CustomParameter(param.Parameter): + + __slots__ = ['foo'] + + # foo has no default value defined in _slot_defaults + + def __init__(self, foo=param.Undefined, **params): + super().__init__(**params) + # To trigger Parameter.__getattribute__ + self.foo = foo + if self.foo == 'bar': + pass + + with pytest.raises( + KeyError, + match="Slot 'foo' on unbound parameter 'CustomParameter' has no default value defined in `_slot_defaults`" + ): + c = CustomParameter() + \ No newline at end of file diff --git a/tests/API1/testparameterizedobject.py b/tests/API1/testparameterizedobject.py index 80103f70d..00e8117d4 100644 --- a/tests/API1/testparameterizedobject.py +++ b/tests/API1/testparameterizedobject.py @@ -814,3 +814,36 @@ class B(A): # Should be 2? # https://github.com/holoviz/param/issues/718 assert B.p == 1 + + +@pytest.fixture +def custom_parameter1(): + class CustomParameter(param.Parameter): + + __slots__ = ['foo', 'bar'] + + # foo has no default value defined in _slot_defaults + + def __init__(self, foo=param.Undefined, **params): + super().__init__(**params) + self.foo = foo + + return CustomParameter + + +def test_inheritance_parameter_attribute_without_default(): + + class CustomParameter(param.Parameter): + + __slots__ = ['foo'] + + # foo has no default value defined in _slot_defaults + + def __init__(self, foo=param.Undefined, **params): + super().__init__(**params) + self.foo = foo + + with pytest.raises(KeyError, match="Slot 'foo' of parameter 'c' has no default value defined in `_slot_defaults`"): + class A(param.Parameterized): + c = CustomParameter() + \ No newline at end of file From c021efec73a699a1ef64d08dfe5cafedd7aa29fd Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Sat, 25 Mar 2023 07:08:22 -0500 Subject: [PATCH 37/45] Apply suggestions from code review --- param/__init__.py | 2 -- tests/API1/testcustomparam.py | 2 +- tests/API1/testparameterizedobject.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 8ef080147..b338196d2 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1918,8 +1918,6 @@ class FileSelector(Selector): """ __slots__ = ['path'] - _slot_defaults = _dict_update(Selector._slot_defaults) - def __init__(self, default=Undefined, path="", **kwargs): self.default = default self.path = path diff --git a/tests/API1/testcustomparam.py b/tests/API1/testcustomparam.py index d5431fc72..47b493d16 100644 --- a/tests/API1/testcustomparam.py +++ b/tests/API1/testcustomparam.py @@ -135,4 +135,4 @@ def __init__(self, foo=param.Undefined, **params): match="Slot 'foo' on unbound parameter 'CustomParameter' has no default value defined in `_slot_defaults`" ): c = CustomParameter() - \ No newline at end of file + diff --git a/tests/API1/testparameterizedobject.py b/tests/API1/testparameterizedobject.py index 00e8117d4..7559b0828 100644 --- a/tests/API1/testparameterizedobject.py +++ b/tests/API1/testparameterizedobject.py @@ -846,4 +846,4 @@ 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() - \ No newline at end of file + From 58c688c9746cec7210439d7ecdf7159e657239ca Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Apr 2023 11:37:09 +0200 Subject: [PATCH 38/45] rename to _compute_length_of_default --- param/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index b338196d2..9e9c4ddc0 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1020,7 +1020,7 @@ def _validate_value(self, val, allow_None): "not %s." % (self.name, val)) -def _update_length(p): +def _compute_length_of_default(p): return len(p.default) @@ -1029,7 +1029,7 @@ class Tuple(Parameter): __slots__ = ['length'] - _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0), length=_update_length) + _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0), length=_compute_length_of_default) def __init__(self, default=Undefined, length=Undefined, **params): """ From f294b577aa0ae51c0b12e16adceb77a794b30c16 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Apr 2023 12:05:01 +0200 Subject: [PATCH 39/45] apply suggestion and rename --- param/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 9e9c4ddc0..b70b850b5 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1221,12 +1221,8 @@ def _update_selector_default(p): return [] -def _update_selector_check_on_set(p): - if len(p.objects) == 0: - return False - else: - return True - +def _compute_selector_checking_default(p): + return len(p.objects) != 0 class Selector(SelectorBase): """ @@ -1259,7 +1255,7 @@ class Selector(SelectorBase): _slot_defaults = _dict_update( SelectorBase._slot_defaults, objects=_update_selector_default, - compute_default_fn=None, check_on_set=_update_selector_check_on_set, + compute_default_fn=None, check_on_set=_compute_selector_checking_default, allow_None=None, instantiate=False, default=None, ) From 03b9f4c6fb97d55c6ed7e285f99dc714b22e17d2 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Apr 2023 12:05:35 +0200 Subject: [PATCH 40/45] rename selector default function --- param/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index b70b850b5..6e3c44088 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1217,7 +1217,7 @@ def get_range(self): raise NotImplementedError("get_range() must be implemented in subclasses.") -def _update_selector_default(p): +def _compute_selector_default(p): return [] @@ -1254,7 +1254,7 @@ class Selector(SelectorBase): __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] _slot_defaults = _dict_update( - SelectorBase._slot_defaults, objects=_update_selector_default, + SelectorBase._slot_defaults, objects=_compute_selector_default, compute_default_fn=None, check_on_set=_compute_selector_checking_default, allow_None=None, instantiate=False, default=None, ) From 81cd1b4c31b00bc3c640101c3a115a3324d5866b Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Apr 2023 12:47:31 +0200 Subject: [PATCH 41/45] validate the default attribute of Boolean --- param/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/param/__init__.py b/param/__init__.py index d832a2114..c579c3e3f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1008,7 +1008,7 @@ class Boolean(Parameter): def __init__(self, default=Undefined, bounds=Undefined, **params): self.bounds = bounds super(Boolean, self).__init__(default=default, **params) - self._validate(default) + self._validate(self.default) def _validate_value(self, val, allow_None): if allow_None: From 68b6e012aceeae4126c80edf4d530c6f71dbc1d7 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 11 Apr 2023 18:32:55 +0200 Subject: [PATCH 42/45] add __repr__ to Undefined --- param/parameterized.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/param/parameterized.py b/param/parameterized.py index 8a6629158..fd08a88b3 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -103,6 +103,10 @@ def __bool__(self): # is used in a comparison without `is`. raise RuntimeError('Use `is` to compare Undefined') + def __repr__(self): + return '' + + Undefined = _Undefined() From 2e19f34f80269420638e25037b63a1ee8b76edd3 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 11 Apr 2023 18:33:06 +0200 Subject: [PATCH 43/45] improve docs on inheritance --- examples/user_guide/Parameters.ipynb | 172 +++++++++++++++++++-------- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/examples/user_guide/Parameters.ipynb b/examples/user_guide/Parameters.ipynb index d3eb13989..6bf8030f9 100644 --- a/examples/user_guide/Parameters.ipynb +++ b/examples/user_guide/Parameters.ipynb @@ -48,7 +48,7 @@ "import param\n", "from param import Parameter, Parameterized\n", "\n", - "p = Parameter(42, doc=\"The answer\", constant=True)\n", + "p = Parameter(default=42, doc=\"The answer\", constant=True)\n", "p.default" ] }, @@ -87,8 +87,8 @@ "source": [ "class A(Parameterized):\n", " question = Parameter(\"What is it?\", doc=\"The question\")\n", - " answer = Parameter(2, constant=True, doc=\"The answer\")\n", - " ultimate_answer = Parameter(42, readonly=True, doc=\"The real answer\")\n", + " answer = Parameter(default=2, constant=True, doc=\"The answer\")\n", + " ultimate_answer = Parameter(default=42, readonly=True, doc=\"The real answer\")\n", "\n", "a = A(question=\"How is it?\", answer=\"6\")" ] @@ -166,7 +166,7 @@ "metadata": {}, "outputs": [], "source": [ - "b=A()\n", + "b = A()\n", "b.answer" ] }, @@ -174,7 +174,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If accessing the attribute always gives us a value whether on the instance or the class, what happened to the `Parameter` objects? They are stored on the Parameterized instance or class, and are accessible via a special `param` accessor object at either the instance or class levels:" + "If accessing the attribute always gives us a value whether on the instance or the class, what happened to the `Parameter` objects? They are stored on the Parameterized instance or class, and are accessible via a special `param` accessor object at either the instance or class levels, via attribute or key:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a.param['question']" ] }, { @@ -227,8 +236,8 @@ "outputs": [], "source": [ "with param.exceptions_summarized():\n", - " a.question=True\n", - " a.answer=5" + " a.question = True\n", + " a.answer = 5" ] }, { @@ -254,7 +263,7 @@ "outputs": [], "source": [ "with param.edit_constant(a):\n", - " a.answer=30\n", + " a.answer = 30\n", "a.answer" ] }, @@ -269,9 +278,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Parameter inheritance and instantiation\n", + "## Parameter inheritance\n", "\n", - "Much of the parameter metadata is there to help you control whether and how the parameter value is instantiated on Parameterized objects as they are created or new Parameterized subclasses as they are defined. Depending on how you want to use that Parameter and what values it might take, controlling instantiation can be very important when mutable values are involved. First, let's look at the default behavior, which is appropriate for immutable attributes:" + "`Parameter` objects and their metadata are inherited in a hierarchy of `Parameterized` objects. Let's see how that works:" ] }, { @@ -280,8 +289,13 @@ "metadata": {}, "outputs": [], "source": [ + "class A(Parameterized):\n", + " question = Parameter(\"What is it?\", doc=\"The question\")\n", + " answer = Parameter(default=2, constant=True, doc=\"The answer\")\n", + " ultimate_answer = Parameter(default=42, readonly=True, doc=\"The real answer\")\n", + "\n", "class B(A):\n", - " ultimate_answer = Parameter(84, readonly=True)\n", + " ultimate_answer = Parameter(default=84)\n", "\n", "b = B()\n", "b.question" @@ -293,7 +307,7 @@ "metadata": {}, "outputs": [], "source": [ - "A.question=\"How are you?\"" + "A.question = \"How are you?\"" ] }, { @@ -309,7 +323,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here you can see that B inherits the `question` parameter from A, and as long as `question` has not been set explicitly on `b`, `b.question` will report the value from where that Parameter was defined, i.e. A in this case. If `question` is subsequently set on `b`, `b.question` will no longer be affected by the value in `A`:" + "Here you can see that B inherits `question` from A, and as long as `question` has not been set explicitly on `b`, `b.question` will report the value from where that Parameter was defined, i.e. A in this case. If `question` is subsequently set on `b`, `b.question` will no longer be affected by the value in `A`:" ] }, { @@ -318,8 +332,8 @@ "metadata": {}, "outputs": [], "source": [ - "b.question=\"Why?\"\n", - "A.question=\"Who?\"\n", + "b.question = \"Why?\"\n", + "A.question = \"Who?\"\n", "b.question" ] }, @@ -327,7 +341,67 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you can see, parameters not specified in B are still fully usable in it, if they were declared in a superclass. Metadata associated with that parameter is also inherited if not explicitly overidden in `B`. E.g. `help(b)` or `help(B)` will list all parameters:" + "As you can see, parameters not specified in B are still fully usable in it, if they were declared in a superclass. **Metadata associated with that parameter is also inherited**, if not explicitly overidden in `B`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b.param.ultimate_answer.constant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b.param.ultimate_answer.readonly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b.ultimate_answer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b.param.ultimate_answer.default" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b.param.ultimate_answer.doc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looking at the metadata values of `ultimate_answer` on `b` or `B` you can see that:\n", + "\n", + "- All the default metadata values like `constant`, `allow_none`, ..., were inherited from the base `Parameter` object provided by Param\n", + "- The `read_only` and `doc` metadata values were inherited from `A`\n", + "- The `default` metadata value of `ultimate_answer` in `B` overrode the value provided in `A`.\n", + "\n", + "Parameter inheritance like this lets you (a) use a parameter in many subclasses without having to define it more than once, and (b) control the value of that parameter conveniently across the entire set of subclasses and instances, as long as that attribute has not been set on those objects already. Using inheritance in this way is a very convenient mechanism for setting default values and other \"global\" parameters, whether before a program starts executing or during it.\n", + "\n", + "`help(b)` or `help(B)` will list all parameters:" ] }, { @@ -341,9 +415,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Parameter inheritance like this lets you (a) use a parameter in many subclasses without having to define it more than once, and (b) control the value of that parameter conveniently across the entire set of subclasses and instances, as long as that attribute has not been set on those objects already. Using inheritance in this way is a very convenient mechanism for setting default values and other \"global\" parameters, whether before a program starts executing or during it.\n", + "## Parameter value instantiation\n", "\n", - "However, what happens if the value (unlike Python strings) is mutable? Things get a lot more complex:" + "So much of the parameter metadata is there to help you control whether and how the parameter value is instantiated on Parameterized objects as they are created or new Parameterized subclasses as they are defined. Depending on how you want to use that Parameter and what values it might take, controlling instantiation can be very important when mutable values are involved. While the default behavior shown above is appropriate for **immutable attributes**, what happens if the value (unlike Python strings) is mutable? Things get a lot more complex." ] }, { @@ -352,11 +426,11 @@ "metadata": {}, "outputs": [], "source": [ - "s = [1,2,3]\n", + "s = [1, 2, 3]\n", "\n", "class C(Parameterized):\n", - " s1 = param.Parameter(s, doc=\"A sequence\")\n", - " s2 = param.Parameter(s, doc=\"Another sequence\")\n", + " s1 = Parameter(s, doc=\"A sequence\")\n", + " s2 = Parameter(s, doc=\"Another sequence\")\n", "\n", "c = C()" ] @@ -365,7 +439,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here, both parameters `s1` and `s2` effectively point to the same underlying sequence `s`:" + "Here, both parameters `s1` and `s2`, on both `A` and `B` effectively point to the same underlying sequence `s`:" ] }, { @@ -383,7 +457,7 @@ "metadata": {}, "outputs": [], "source": [ - "s[1]*=5" + "s[1] *= 5" ] }, { @@ -410,7 +484,7 @@ "metadata": {}, "outputs": [], "source": [ - "c.s1[2]='a'" + "c.s1[2] = 'a'" ] }, { @@ -435,7 +509,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you can see, there is only one actual sequence here, and `s`, `s1`, and `s2` all point to it. In some cases such behavior is desirable, e.g. if the mutable object is a specific global list (e.g. a set of search paths) with a unique identity and all of the parameters are meant to point to that specific item. In other cases, it's the contents of the mutable item that are important, and no sharing of contents is intended. Luckily, Param supports that case as well, if you provide `instantiate=True`:" + "As you can see, there is only one actual sequence here, and `s`, `s1`, and `s2` all point to it. In some cases such behavior is desirable, e.g. if the mutable object is a specific global list (e.g. a set of search paths) with a unique identity and all of the parameters are meant to point to that specific item. In other cases, it's the contents of the mutable item that are important, and no sharing of contents is intended. Luckily, Param supports that case as well, if you provide `instantiate=True` (default is `False`):" ] }, { @@ -447,8 +521,8 @@ "s = [1,2,3]\n", "\n", "class D(Parameterized):\n", - " s1 = Parameter(s, doc=\"A sequence\", instantiate=True)\n", - " s2 = Parameter(s, doc=\"Another sequence\", instantiate=True)\n", + " s1 = Parameter(default=s, doc=\"A sequence\", instantiate=True)\n", + " s2 = Parameter(default=s, doc=\"Another sequence\", instantiate=True)\n", "\n", "d = D()" ] @@ -475,7 +549,7 @@ "metadata": {}, "outputs": [], "source": [ - "s*=2" + "s *= 2" ] }, { @@ -502,7 +576,7 @@ "metadata": {}, "outputs": [], "source": [ - "d.s1[2]='a'" + "d.s1[2] = 'a'" ] }, { @@ -525,9 +599,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Parameter metadata inheritance and instantiation\n", + "## Parameter object instantiation\n", "\n", - "`instantiate` controls how parameter _values_ behave, but similar issues arise for Parameter _objects_, which offer similar control via the `per_instance` metadata declaration. `per_instance` (True by default) provides a logically distinct Parameter object for every Parameterized instance, allowing each such instance to have different metadata for that parameter. For example, we can set the label separately for each instance without clobbering each other:" + "`instantiate` controls how parameter _values_ behave, but similar issues arise for Parameter _objects_, which offer similar control via the `per_instance` metadata declaration. `per_instance` (`True` by default) provides a logically distinct Parameter object for every Parameterized instance, allowing each such instance to have different metadata for that parameter. For example, we can set the label separately for each instance without clobbering each other:" ] }, { @@ -538,8 +612,8 @@ "source": [ "d1 = D()\n", "d2 = D()\n", - "d1.param.s1.label=\"sequence 1\"\n", - "d2.param.s1.label=\"Sequence 1\"\n", + "d1.param.s1.label = \"sequence 1\"\n", + "d2.param.s1.label = \"(sequence 1)\"\n", "d2.param.s1.label" ] }, @@ -566,11 +640,11 @@ "outputs": [], "source": [ "class E(Parameterized):\n", - " a = param.Parameter(3.14, label=\"pi\", per_instance=False)\n", + " a = Parameter(default=3.14, label=\"pi\", per_instance=False)\n", "\n", "e1 = E()\n", "e2 = E()\n", - "e2.param.a.label=\"Pie\"\n", + "e2.param.a.label = \"Pie\"\n", "e1.param.a.label" ] }, @@ -592,10 +666,10 @@ "outputs": [], "source": [ "class S(param.Parameterized):\n", - " l = param.Parameter([1,2,3], instantiate=True)\n", + " l = Parameter(default=[1,2,3], instantiate=True)\n", "\n", "ss = [S() for i in range(10)]\n", - "ss[0].l[2]=5\n", + "ss[0].l[2] = 5\n", "ss[1].l" ] }, @@ -617,7 +691,7 @@ "with param.shared_parameters():\n", " ps = [S() for i in range(10)]\n", " \n", - "ps[0].l[2]=5\n", + "ps[0].l[2] = 5\n", "ps[1].l" ] }, @@ -650,13 +724,13 @@ "import param\n", "\n", "class Q(param.Parameterized):\n", - " a = param.Number(39, bounds=(0,50))\n", - " b = param.String(\"str\")\n", + " a = param.Number(default=39, bounds=(0,50))\n", + " b = param.String(default=\"str\")\n", "\n", "class P(Q):\n", - " c = param.ClassSelector(Q, Q())\n", - " e = param.ClassSelector(param.Parameterized, param.Parameterized())\n", - " f = param.Range((0,1))\n", + " c = param.ClassSelector(default=Q(), class_=Q)\n", + " e = param.ClassSelector(default=param.Parameterized(), class_=param.Parameterized)\n", + " f = param.Range(default=(0,1))\n", "\n", "p = P(f=(2,3), c=P(f=(42,43)), name=\"demo\")" ] @@ -745,14 +819,16 @@ " \"\"\"Integer Parameter that must be even\"\"\"\n", "\n", " def _validate_value(self, val, allow_None):\n", - " super(EvenInteger, self)._validate_value(val, allow_None)\n", + " super()._validate_value(val, allow_None)\n", " if not isinstance(val, numbers.Number):\n", - " raise ValueError(\"EvenInteger parameter %r must be a number, \"\n", - " \"not %r.\" % (self.name, val))\n", + " raise ValueError(\n", + " f\"EvenInteger parameter {self.name!r} must be a number, not {val!r}.\"\n", + " )\n", " \n", " if not (val % 2 == 0):\n", - " raise ValueError(\"EvenInteger parameter %r must be even, \"\n", - " \"not %r.\" % (self.name, val))\n", + " raise ValueError(\n", + " f\"EvenInteger parameter {self.name!r} must be even, not {val!r}.\"\n", + " )\n", "\n", "class P(param.Parameterized):\n", " n = param.Number()\n", From 9e3f2a981817a46b996c83998463bb71ed915e2b Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 11 Apr 2023 18:42:40 +0200 Subject: [PATCH 44/45] add docstring to _compute_selector_default --- param/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/param/__init__.py b/param/__init__.py index c579c3e3f..d11e7d036 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1222,6 +1222,12 @@ def get_range(self): def _compute_selector_default(p): + """ + Using a function instead of setting default to [] in _slot_defaults, as + if it were modified in place later, which would happen with check_on_set set to False, + then the object in _slot_defaults would itself be updated and the next Selector + instance created wouldn't have [] as the default but a populated list. + """ return [] From 0ca004297a185dd0dff403aabb4fdf2e2f54ba4e Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 11 Apr 2023 18:43:54 +0200 Subject: [PATCH 45/45] add comment on empty_default --- param/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/param/__init__.py b/param/__init__.py index d11e7d036..74c852409 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -1259,6 +1259,8 @@ class Selector(SelectorBase): names to objects. If a dictionary is supplied, the objects will need to be hashable so that their names can be looked up from the object value. + + empty_default is an internal argument that does not have a slot. """ __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names']