Skip to content

Commit

Permalink
Deprecate ._param_watchers in favor of .param.watchers (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximlt authored Jul 30, 2023
1 parent 754dcfd commit ea6b366
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 37 deletions.
21 changes: 21 additions & 0 deletions examples/user_guide/Dependencies_and_Watchers.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@
"- [`batch_call_watchers`](#batch_call_watchers): context manager accumulating and eliding multiple Events to be applied on exit from the context \n",
"- [`discard_events`](#discard_events): context manager silently discarding events generated while in the context\n",
"- [`.param.trigger`](#.param.trigger): method to force creation of an Event for this Parameter's Watchers without a corresponding change to the Parameter\n",
"- [`.param.watchers`](#.param.watchers): writable property to access the instance watchers\n",
"- [Event Parameter](#Event-Parameter): Special Parameter type providing triggerable transient Events (like a momentary push button)\n",
"- [Async executor](#Async-executor): Support for asynchronous processing of Events, e.g. for interfacing to external servers\n",
"\n",
Expand Down Expand Up @@ -717,6 +718,26 @@
"p.param.trigger('a')"
]
},
{
"cell_type": "markdown",
"id": "fc3cb5ba",
"metadata": {},
"source": [
"### `.param.watchers`\n",
"\n",
"For more advanced purposes it can be useful to inspect all the watchers set up on an instance, in which case you can use `inst.param.watchers` to obtain a dictionary with the following structure: `{parameter_name: {what: [Watcher(), ...], ...}, ...}`"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0b2ae598",
"metadata": {},
"outputs": [],
"source": [
"p.param.watchers"
]
},
{
"cell_type": "markdown",
"id": "42ffa0a0",
Expand Down
2 changes: 1 addition & 1 deletion examples/user_guide/Parameters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@
"- Private attributes:\n",
" - `_param__parameters`: Store the object returned by `.param` on the class\n",
" - `_param__private`: Store various internal data on Parameterized class and instances\n",
" - `_param_watchers` (to be removed soon): Store a dictionary of instance watchers"
" - `_param_watchers` (deprecated in Param 2.0 and to be removed soon): Store a dictionary of instance watchers"
]
},
{
Expand Down
85 changes: 49 additions & 36 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ def discard_events(parameterized):
"""
batch_watch = parameterized.param._BATCH_WATCH
parameterized.param._BATCH_WATCH = True
watchers, events = (list(parameterized.param._watchers),
watchers, events = (list(parameterized.param._state_watchers),
list(parameterized.param._events))
try:
yield
except:
raise
finally:
parameterized.param._BATCH_WATCH = batch_watch
parameterized.param._watchers = watchers
parameterized.param._state_watchers = watchers
parameterized.param._events = events


Expand Down Expand Up @@ -1431,10 +1431,8 @@ def __set__(self, obj, val):

if obj is None:
watchers = self.watchers.get("value")
# elif hasattr(obj, '_param__private') and hasattr(obj._param__private, 'watchers') and obj._param__private.watchers is not None and self.name in obj._param__private.watchers:
elif hasattr(obj, '_param_watchers') and self.name in obj._param_watchers:
# watchers = obj._param__private.watchers[self.name].get('value')
watchers = obj._param_watchers[self.name].get('value')
elif self.name in obj._param__private.watchers:
watchers = obj._param__private.watchers[self.name].get('value')
if watchers is None:
watchers = self.watchers.get("value")
else:
Expand Down Expand Up @@ -1726,17 +1724,25 @@ def _events(self_, value):
self_.self_or_cls._param__private.parameters_state['events'] = value

@property
def _watchers(self_):
def _state_watchers(self_):
return self_.self_or_cls._param__private.parameters_state['watchers']

@_watchers.setter
def _watchers(self_, value):
@_state_watchers.setter
def _state_watchers(self_, value):
self_.self_or_cls._param__private.parameters_state['watchers'] = value

@property
def watchers(self):
"""Read-only list of watchers on this Parameterized"""
return self._watchers
def watchers(self_):
"""Dictionary of instance watchers."""
if self_.self is None:
raise TypeError('Accessing `.param.watchers` is only supported on a Parameterized instance, not class.')
return self_.self._param__private.watchers

@watchers.setter
def watchers(self_, value):
if self_.self is None:
raise TypeError('Setting `.param.watchers` is only supported on a Parameterized instance, not class.')
self_.self._param__private.watchers = value

@property
def self_or_cls(self_):
Expand Down Expand Up @@ -2224,16 +2230,16 @@ def trigger(self_, *param_names):
for p in trigger_params if p in param_names}

events = self_.self_or_cls.param._events
watchers = self_.self_or_cls.param._watchers
watchers = self_.self_or_cls.param._state_watchers
self_.self_or_cls.param._events = []
self_.self_or_cls.param._watchers = []
self_.self_or_cls.param._state_watchers = []
param_values = self_.values()
params = {name: param_values[name] for name in param_names}
self_.self_or_cls.param._TRIGGER = True
self_.update(dict(params, **triggers))
self_.self_or_cls.param._TRIGGER = False
self_.self_or_cls.param._events += events
self_.self_or_cls.param._watchers += watchers
self_.self_or_cls.param._state_watchers += watchers


def _update_event_type(self_, watcher, event, triggered):
Expand Down Expand Up @@ -2275,8 +2281,8 @@ def _call_watcher(self_, watcher, event):

if self_.self_or_cls.param._BATCH_WATCH:
self_._events.append(event)
if not any(watcher is w for w in self_._watchers):
self_._watchers.append(watcher)
if not any(watcher is w for w in self_._state_watchers):
self_._state_watchers.append(watcher)
else:
event = self_._update_event_type(watcher, event, self_.self_or_cls.param._TRIGGER)
with _batch_call_watchers(self_.self_or_cls, enable=watcher.queued, run=False):
Expand All @@ -2290,9 +2296,9 @@ def _batch_call_watchers(self_):
while self_.self_or_cls.param._events:
event_dict = OrderedDict([((event.name, event.what), event)
for event in self_.self_or_cls.param._events])
watchers = self_.self_or_cls.param._watchers[:]
watchers = self_.self_or_cls.param._state_watchers[:]
self_.self_or_cls.param._events = []
self_.self_or_cls.param._watchers = []
self_.self_or_cls.param._state_watchers = []

for watcher in sorted(watchers, key=lambda w: w.precedence):
events = [self_._update_event_type(watcher, event_dict[(name, watcher.what)],
Expand Down Expand Up @@ -2550,6 +2556,8 @@ 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 @@ -2658,8 +2666,7 @@ def _register_watcher(self_, action, watcher, what='value'):
"parameters of class {}".format(parameter_name, self_.cls.__name__))

if self_.self is not None and what == "value":
# watchers = self_.self._param__private.watchers
watchers = self_.self._param_watchers
watchers = self_.self._param__private.watchers
if parameter_name not in watchers:
watchers[parameter_name] = {}
if what not in watchers[parameter_name]:
Expand Down Expand Up @@ -3678,10 +3685,10 @@ class _InstancePrivate:
Dynamic watchers
params: dict
Dict of parameter_name:parameter
# watchers: dict
# Dict of dict:
# parameter_name:
# parameter_attribute (e.g. 'value'): list of `Watcher`s
watchers: dict
Dict of dict:
parameter_name:
parameter_attribute (e.g. 'value'): list of `Watcher`s
values: dict
Dict of parameter name: value
"""
Expand All @@ -3691,7 +3698,7 @@ class _InstancePrivate:
'parameters_state',
'dynamic_watchers',
'params',
# 'watchers',
'watchers',
'values',
]

Expand All @@ -3701,7 +3708,7 @@ def __init__(
parameters_state=None,
dynamic_watchers=None,
params=None,
# watchers=None,
watchers=None,
values=None,
):
self.initialized = initialized
Expand All @@ -3715,7 +3722,7 @@ def __init__(
self.parameters_state = parameters_state
self.dynamic_watchers = defaultdict(list) if dynamic_watchers is None else dynamic_watchers
self.params = {} if params is None else params
# self.watchers = {} if watchers is None else watchers
self.watchers = {} if watchers is None else watchers
self.values = {} if values is None else values

def __getstate__(self):
Expand Down Expand Up @@ -3781,7 +3788,6 @@ def __init__(self, **params):
# (see Parameter.__set__) so we shouldn't override it here.
if not isinstance(self._param__private, _InstancePrivate):
self._param__private = _InstancePrivate()
self._param_watchers = {}

# Skip generating a custom instance name when a class in the hierarchy
# has overriden the default of the `name` Parameter.
Expand All @@ -3798,6 +3804,18 @@ 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=FutureWarning)
def _param_watchers(self):
return self._param__private.watchers

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

# 'Special' methods

def __getstate__(self):
Expand Down Expand Up @@ -3835,10 +3853,8 @@ def __setstate__(self, state):

# When making a copy the internal watchers have to be
# recreated and point to the new instance
# if _param__private.watchers:
if '_param_watchers' in state:
# param_watchers = _param__private.watchers
param_watchers = state['_param_watchers']
if _param__private.watchers:
param_watchers = _param__private.watchers
for p, attrs in param_watchers.items():
for attr, watchers in attrs.items():
new_watchers = []
Expand All @@ -3854,9 +3870,6 @@ def __setstate__(self, state):
new_watchers.append(Watcher(*watcher_args))
param_watchers[p][attr] = new_watchers

if '_param_watchers' not in state:
state['_param_watchers'] = {}

state.pop('param', None)

for name,value in state.items():
Expand Down
9 changes: 9 additions & 0 deletions tests/testdeprecations.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ 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(FutureWarning):
param.parameterized.Parameterized()._param_watchers

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


class TestDeprecateParameters:

def test_deprecate_print_param_defaults(self):
Expand Down
1 change: 1 addition & 0 deletions tests/testparameterizedobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,7 @@ def foo(self): pass
assert _dir(P) == [
'_param__parameters',
'_param__private',
'_param_watchers',
'foo',
'name',
'param',
Expand Down
88 changes: 88 additions & 0 deletions tests/testwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest

import param
import pytest

from param.parameterized import discard_events

Expand Down Expand Up @@ -545,6 +546,93 @@ def test_watch_event_batched_trigger_method(self):
self.assertEqual(obj.e, False)
self.assertEqual(obj.f, False)

def test_watch_watchers_exposed(self):
obj = SimpleWatchExample()

obj.param.watch(lambda: '', ['a', 'b'])

with pytest.warns(FutureWarning):
pw = obj._param_watchers
assert isinstance(pw, dict)
for pname in ('a', 'b'):
assert pname in pw
assert 'value' in pw[pname]
assert isinstance(pw[pname]['value'], list) and len(pw[pname]['value']) == 1
assert isinstance(pw[pname]['value'][0], param.parameterized.Watcher)

def test_watch_watchers_modified(self):
accumulator = Accumulator()
obj = SimpleWatchExample()

obj.param.watch(accumulator, ['a', 'b'])

with pytest.warns(FutureWarning):
pw = obj._param_watchers
del pw['a']

obj.param.update(a=1, b=1)

assert accumulator.call_count() == 1
args = accumulator.args_for_call(0)
assert len(args) == 1
assert args[0].name == 'b'

def test_watch_watchers_exposed_public(self):
obj = SimpleWatchExample()

obj.param.watch(lambda: '', ['a', 'b'])

pw = obj.param.watchers
assert isinstance(pw, dict)
for pname in ('a', 'b'):
assert pname in pw
assert 'value' in pw[pname]
assert isinstance(pw[pname]['value'], list) and len(pw[pname]['value']) == 1
assert isinstance(pw[pname]['value'][0], param.parameterized.Watcher)

def test_watch_watchers_modified_public(self):
accumulator = Accumulator()
obj = SimpleWatchExample()

obj.param.watch(accumulator, ['a', 'b'])

pw = obj.param.watchers
del pw['a']

obj.param.update(a=1, b=1)

assert accumulator.call_count() == 1
args = accumulator.args_for_call(0)
assert len(args) == 1
assert args[0].name == 'b'

def test_watch_watchers_setter_public(self):
accumulator = Accumulator()
obj = SimpleWatchExample()

obj.param.watch(accumulator, ['a', 'b'])

obj.param.watchers = {}

obj.param.update(a=1, b=1)

assert accumulator.call_count() == 0

def test_watch_watchers_class_error(self):
with pytest.raises(
TypeError,
match=r"Accessing `\.param\.watchers` is only supported on a Parameterized instance, not class\."
):
SimpleWatchExample.param.watchers

def test_watch_watchers_class_set_error(self):
with pytest.raises(
TypeError,
match=r"Setting `\.param\.watchers` is only supported on a Parameterized instance, not class\."
):
SimpleWatchExample.param.watchers = {}


class TestWatchMethod(unittest.TestCase):

def test_dependent_params(self):
Expand Down

0 comments on commit ea6b366

Please sign in to comment.