Skip to content

Commit 4bc8482

Browse files
authored
unwatch no longer logs a warning and idempotent behavior clarified (#1018)
1 parent 54bfe9d commit 4bc8482

3 files changed

Lines changed: 33 additions & 19 deletions

File tree

doc/user_guide/Dependencies_and_Watchers.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@
480480
"\n",
481481
"- `obj.param.watch_values(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)`: <br>Easier-to-use version of `obj.param.watch` specific to watching for changes in parameter values. Same as `watch`, but hard-codes `what='value'` and invokes the callback `fn` using keyword arguments _param_name_=_new_value_ rather than with a positional-argument list of `Event` objects.\n",
482482
"\n",
483-
"- `obj.param.unwatch(watcher)`: <br>Remove the given `Watcher` (typically obtained as the return value from `watch` or `watch_values`) from those registered on this `obj`.\n",
483+
"- `obj.param.unwatch(watcher)`: <br>Remove the given `Watcher` (typically obtained as the return value from `watch` or `watch_values`) from those registered on this `obj`. Calling this method again after the watcher has already been unregistered is a no-op and won't raise any error.\n",
484484
"\n",
485485
"To see how to use `watch` and `watch_values`, let's make a class with parameters `a` and `b` and various watchers with corresponding callback methods:"
486486
]

param/parameterized.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3905,7 +3905,11 @@ def _spec_to_obj(self_, spec, dynamic=True, intermediate=True):
39053905
deps.append(info)
39063906
return deps, dynamic_deps
39073907

3908-
def _register_watcher(self_, action, watcher, what='value'):
3908+
def _register_watcher(
3909+
self_,
3910+
action: Literal['append', 'remove'],
3911+
watcher: Watcher, what: str = 'value',
3912+
):
39093913
if self_.self is not None and not self_.self._param__private.initialized:
39103914
raise RuntimeError(
39113915
'(Un)registering a watcher on a partially initialized Parameterized instance '
@@ -3925,12 +3929,19 @@ def _register_watcher(self_, action, watcher, what='value'):
39253929
watchers[parameter_name] = {}
39263930
if what not in watchers[parameter_name]:
39273931
watchers[parameter_name][what] = []
3928-
getattr(watchers[parameter_name][what], action)(watcher)
3932+
method = getattr(watchers[parameter_name][what], action)
39293933
else:
39303934
watchers = self_[parameter_name].watchers
39313935
if what not in watchers:
39323936
watchers[what] = []
3933-
getattr(watchers[what], action)(watcher)
3937+
method = getattr(watchers[what], action)
3938+
try:
3939+
method(watcher)
3940+
except ValueError:
3941+
# ValueError raised when attempting to remove an already
3942+
# removed watcher. Error swallowed as unwatch is idempotent.
3943+
if action != 'remove':
3944+
raise
39343945

39353946
def watch(
39363947
self_,
@@ -4040,7 +4051,8 @@ def unwatch(self_, watcher: Watcher) -> None:
40404051
40414052
This method unregisters a previously registered `Watcher` object,
40424053
effectively stopping it from being triggered by events on the associated
4043-
parameters.
4054+
parameters. Calling unwatch with an already unregistered watcher
4055+
is a no-op.
40444056
40454057
Parameters
40464058
----------
@@ -4080,11 +4092,12 @@ def unwatch(self_, watcher: Watcher) -> None:
40804092
No callback is triggered after removing the watcher:
40814093
40824094
>>> instance.a = 20 # No output
4095+
4096+
Calling unwatch() again has no effect:
4097+
4098+
>>> instance.param.unwatch(watcher)
40834099
"""
4084-
try:
4085-
self_._register_watcher('remove', watcher, what=watcher.what)
4086-
except Exception:
4087-
self_.warning(f'No such watcher {str(watcher)} to remove.')
4100+
self_._register_watcher('remove', watcher, what=watcher.what)
40884101

40894102
def watch_values(
40904103
self_,

tests/testwatch.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from param.parameterized import Skip, discard_events
1010

11-
from .utils import MockLoggingHandler, warnings_as_excepts
11+
from .utils import MockLoggingHandler
1212

1313

1414
class Accumulator:
@@ -250,15 +250,8 @@ def accumulator(change):
250250
obj = SimpleWatchExample()
251251
watcher = obj.param.watch(accumulator, 'a')
252252
obj.param.unwatch(watcher)
253-
with warnings_as_excepts(match='No such watcher'):
254-
obj.param.unwatch(watcher)
255-
try:
256-
param.parameterized.warnings_as_exceptions = False
257-
obj.param.unwatch(watcher)
258-
self.log_handler.assertEndsWith('WARNING',
259-
' to remove.')
260-
finally:
261-
param.parameterized.warnings_as_exceptions = True
253+
# Idempotent, not error raised.
254+
obj.param.unwatch(watcher)
262255

263256
def test_simple_batched_watch_setattr(self):
264257

@@ -720,6 +713,14 @@ def __init__(self, **params):
720713
P()
721714

722715

716+
def test_watch_raises_bad_parameter(self):
717+
obj = SimpleWatchExample()
718+
with pytest.raises(
719+
ValueError,
720+
match="does_not_exist parameter was not found in list of parameters of class SimpleWatchExample"
721+
):
722+
obj.param.watch(lambda e: print(e), 'does_not_exist')
723+
723724

724725
class TestWatchMethod(unittest.TestCase):
725726

0 commit comments

Comments
 (0)