Skip to content

Commit

Permalink
Address deprecations: remove _param_watchers, raise RuntimeError on u…
Browse files Browse the repository at this point in the history
…nsafe ops during init, and failed validation of a parameter default after inheritance (#973)
  • Loading branch information
maximlt authored Nov 26, 2024
1 parent 1868240 commit 08dd44d
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 228 deletions.
3 changes: 1 addition & 2 deletions doc/user_guide/Parameters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,7 @@
" - `param`: Property that helps keep the Parameterized namespace clean and disambiguate between Parameter objects and parameter values, it gives access to a namespace that offers various methods (see the section below) to update and inspect the Parameterized object at hand.\n",
"- Private attributes:\n",
" - `_param__parameters`: Store the object returned by `.param` on the class\n",
" - `_param__private`: Store various internal data on Parameterized class and instances\n",
" - `_param_watchers` (deprecated in Param 2.0 and to be removed soon): Store a dictionary of instance watchers"
" - `_param__private`: Store various internal data on Parameterized class and instances"
]
},
{
Expand Down
45 changes: 9 additions & 36 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -2419,15 +2419,13 @@ def objects(self_, instance=True):
instance='existing'.
"""
if self_.self is not None and not self_.self._param__private.initialized and instance is True:
warnings.warn(
raise RuntimeError(
'Looking up instance Parameter objects (`.param.objects()`) until '
'the Parameterized instance has been fully initialized is deprecated and will raise an error in a future version. '
'the Parameterized instance has been fully initialized is not allowed. '
'Ensure you have called `super().__init__(**params)` in your Parameterized '
'constructor before trying to access instance Parameter objects, or '
'looking up the class Parameter objects with `.param.objects(instance=False)` '
'may be enough for your use case.',
category=_ParamFutureWarning,
stacklevel=2,
)

pdict = self_._cls_parameters
Expand All @@ -2449,13 +2447,10 @@ def trigger(self_, *param_names):
that it is clear which Event parameter has been triggered.
"""
if self_.self is not None and not self_.self._param__private.initialized:
warnings.warn(
raise RuntimeError(
'Triggering watchers on a partially initialized Parameterized instance '
'is deprecated and will raise an error in a future version. '
'Ensure you have called super().__init__(**params) in '
'is not allowed. Ensure you have called super().__init__(**params) in '
'the Parameterized instance constructor before trying to set up a watcher.',
category=_ParamFutureWarning,
stacklevel=2,
)

trigger_params = [p for p in self_
Expand Down Expand Up @@ -2792,8 +2787,6 @@ def outputs(self_):
outputs = {}
for cls in classlist(self_.cls):
for name in dir(cls):
if name == '_param_watchers':
continue
method = getattr(self_.self_or_cls, name)
dinfo = getattr(method, '_dinfo', {})
if 'outputs' not in dinfo:
Expand Down Expand Up @@ -2904,13 +2897,10 @@ def _spec_to_obj(self_, spec, dynamic=True, intermediate=True):

def _register_watcher(self_, action, watcher, what='value'):
if self_.self is not None and not self_.self._param__private.initialized:
warnings.warn(
raise RuntimeError(
'(Un)registering a watcher on a partially initialized Parameterized instance '
'is deprecated and will raise an error in a future version. Ensure '
'you have called super().__init__(**) in the Parameterized instance '
'constructor before trying to set up a watcher.',
category=_ParamFutureWarning,
stacklevel=4,
'is not allowed. Ensure you have called super().__init__(**) in the '
'Parameterized instance constructor before trying to set up a watcher.',
)

parameter_names = watcher.parameter_names
Expand Down Expand Up @@ -3652,8 +3642,7 @@ def __param_inheritance(mcs, param_name, param):
# might raise other types of error, so we catch them all.
except Exception as e:
msg = f'{_validate_error_prefix(param)} failed to validate its ' \
'default value on class creation, this is going to raise ' \
'an error in the future. '
'default value on class creation. '
parents = ', '.join(klass.__name__ for klass in mcs.__mro__[1:-2])
if not type_change and slot_overridden:
msg += (
Expand All @@ -3674,11 +3663,7 @@ def __param_inheritance(mcs, param_name, param):
# performance reasons.
pass
msg += f'\nValidation failed with:\n{e}'
warnings.warn(
msg,
category=_ParamFutureWarning,
stacklevel=4,
)
raise RuntimeError(msg) from e

def get_param_descriptor(mcs,param_name):
"""
Expand Down Expand Up @@ -4196,18 +4181,6 @@ def __init__(self, **params):
def param(self):
return Parameters(self.__class__, self=self)

#PARAM3_DEPRECATION
@property
@_deprecated(extra_msg="Use `inst.param.watchers` instead.", warning_cat=_ParamFutureWarning)
def _param_watchers(self):
return self._param__private.watchers

#PARAM3_DEPRECATION
@_param_watchers.setter
@_deprecated(extra_msg="Use `inst.param.watchers = ...` instead.", warning_cat=_ParamFutureWarning)
def _param_watchers(self, value):
self._param__private.watchers = value

# 'Special' methods

def __getstate__(self):
Expand Down
156 changes: 0 additions & 156 deletions tests/testdeprecations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Test deprecation warnings.
"""
import re
import warnings

import param
Expand Down Expand Up @@ -102,161 +101,6 @@ def test_deprecate_all_equal(self):
with pytest.raises(param._utils.ParamDeprecationWarning):
param.parameterized.all_equal(1, 1)

def test_deprecate_param_watchers(self):
with pytest.raises(param._utils.ParamFutureWarning):
param.parameterized.Parameterized()._param_watchers

def test_deprecate_param_watchers_setter(self):
with pytest.raises(param._utils.ParamFutureWarning):
param.parameterized.Parameterized()._param_watchers = {}

def test_param_error_unsafe_ops_before_initialized(self):
class P(param.Parameterized):

x = param.Parameter()

def __init__(self, **params):
with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
'Looking up instance Parameter objects (`.param.objects()`) until '
'the Parameterized instance has been fully initialized is deprecated and will raise an error in a future version. '
'Ensure you have called `super().__init__(**params)` in your Parameterized '
'constructor before trying to access instance Parameter objects, or '
'looking up the class Parameter objects with `.param.objects(instance=False)` '
'may be enough for your use case.',
)
):
self.param.objects()

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
'Triggering watchers on a partially initialized Parameterized instance '
'is deprecated and will raise an error in a future version. '
'Ensure you have called super().__init__(**params) in '
'the Parameterized instance constructor before trying to set up a watcher.',
)
):
self.param.trigger('x')

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
'(Un)registering a watcher on a partially initialized Parameterized instance '
'is deprecated and will raise an error in a future version. Ensure '
'you have called super().__init__(**) in the Parameterized instance '
'constructor before trying to set up a watcher.',
)
):
self.param.watch(print, 'x')

self.param.objects(instance=False)
super().__init__(**params)

P()

# Inheritance tests to be move to testparameterizedobject.py when warnings will be turned into errors

def test_inheritance_with_incompatible_defaults(self):
class A(param.Parameterized):
p = param.String()

class B(A): pass

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
"Number parameter 'C.p' failed to validate its "
"default value on class creation, this is going to raise "
"an error in the future. The Parameter type changed between class 'C' "
"and one of its parent classes (B, A) which made it invalid. "
"Please fix the Parameter type."
"\nValidation failed with:\nNumber parameter 'C.p' only takes numeric values, not <class 'str'>."
)
):
class C(B):
p = param.Number()

def test_inheritance_default_validation_with_more_specific_type(self):
class A(param.Parameterized):
p = param.Tuple(default=('a', 'b'))

class B(A): pass

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
"NumericTuple parameter 'C.p' failed to validate its "
"default value on class creation, this is going to raise "
"an error in the future. The Parameter type changed between class 'C' "
"and one of its parent classes (B, A) which made it invalid. "
"Please fix the Parameter type."
"\nValidation failed with:\nNumericTuple parameter 'C.p' only takes numeric values, not <class 'str'>."
)
):
class C(B):
p = param.NumericTuple()

def test_inheritance_with_changing_bounds(self):
class A(param.Parameterized):
p = param.Number(default=5)

class B(A): pass

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
"Number parameter 'C.p' failed to validate its "
"default value on class creation, this is going to raise "
"an error in the future. The Parameter is defined with attributes "
"which when combined with attributes inherited from its parent "
"classes (B, A) make it invalid. Please fix the Parameter attributes."
"\nValidation failed with:\nNumber parameter 'C.p' must be at most 3, not 5."
)
):
class C(B):
p = param.Number(bounds=(-1, 3))

def test_inheritance_with_changing_default(self):
class A(param.Parameterized):
p = param.Number(default=5, bounds=(3, 10))

class B(A): pass

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
"Number parameter 'C.p' failed to validate its "
"default value on class creation, this is going to raise "
"an error in the future. The Parameter is defined with attributes "
"which when combined with attributes inherited from its parent "
"classes (B, A) make it invalid. Please fix the Parameter attributes."
"\nValidation failed with:\nNumber parameter 'C.p' must be at least 3, not 1."
)
):
class C(B):
p = param.Number(default=1)

def test_inheritance_with_changing_class_(self):
class A(param.Parameterized):
p = param.ClassSelector(class_=int, default=5)

class B(A): pass

with pytest.raises(
param._utils.ParamFutureWarning,
match=re.escape(
"ClassSelector parameter 'C.p' failed to validate its "
"default value on class creation, this is going to raise "
"an error in the future. The Parameter is defined with attributes "
"which when combined with attributes inherited from its parent "
"classes (B, A) make it invalid. Please fix the Parameter attributes."
"\nValidation failed with:\nClassSelector parameter 'C.p' value must be an instance of str, not 5."
)
):
class C(B):
p = param.ClassSelector(class_=str)

class TestDeprecateParameters:

Expand Down
Loading

0 comments on commit 08dd44d

Please sign in to comment.