holoviz / holoviz/param

Add `param.watch` function/decorator

Open
#806 1 comment 3 reactions 0 assignees View on GitHub
type-feature
Dominant language
Python
Stars
521
Forks
86
Avg merge
1d 13h
Merged PRs (30d)
35

Description

I've long been thinking that it'd be nice for Param to have a `param.watch` function and decorator and finally decided it was time to write this down. I'm going to completely ignore any implementation discussion for now and whether what I'm asking for can actually be implemented. My two main asks for `param.watch` are:

1. being able to register side-effecty callbacks with a top-level `param.watch` function, a la `obj.param.watch`
2. in the scope of a Parameterized class being able to replace `@param.depends(..., watch=True)` with a decorator `@param.watch(...)`

### being able to register side-effecty callbacks with a top-level `param.watch` function, a la `obj.param.watch`

Registering side-effecty callbacks outside of the scope of the Parameterized class can currently be done using `@pn.depends(..., watch=True)` or with `pn.bind(func, ..., watch=True)`. In the Panel world we've been moving away from suggesting using `@param.depends` in favor of `pn.bind`. However, I feel that while `pn.bind` is a great API on its own that is easy to understand as modeled on `functools.partial`, `pn.bind` together with `watch=True` changes the nature of the API in an unexpected and hard to understand way. `watch` being a parameter of `pn.bind` also pollutes it. On the other hand in the context of a Parameterized class the low-level `obj.param.watch` is available and allows for the fine-grained control you sometimes need when dealing with callbacks, e.g. comparing the new and old values of a parameter, doing something when the value is set even if it's not changed.

I have observed there are somehow three callback signatures I wish I could use when creating a callback:

1. I don't care at all about the event passed, e.g. for a callback triggered by a button click
2. I only care about the new value(s), like how the top-level `@param.depends` decorator works currently by calling the callback only with the new values
3. I care about the `Event` objects passed to the callback as I want to inspect them further

`obj.param.watch` is modeled on 3) which is the most powerful approach but also the one that requires the most code to handle the payload specially when you're watching more than one parameters, it's also more difficult to write tests for these callbacks:

```python
obj.param.watch(cb, ['a', 'b'])

def cb(*events):
# yuk
for event in events:
if event.name == 'a':
...
elif event.name == 'b':
pass
```

It'd be great if I could declare with `param.watch` what payload I'd like to get. I'm guessing the default could be 2) instead of 3) as being the more common case.

The other features of `obj.param.watch` (*what*, *onlychanged*, *precedence*, *unwatch*, etc.) would certainly also be appropriate for `param.watch`.

Using `param.watch` would look like:

```python
def cb1(a, b):
print(a, b)

watchers = param.watch(cb1, [widget_a.param.value, widget_b.param.value])

def cb2(*events):
print(events)

watchers = param.watch(cb2, [widget_a.param.value, widget_b.param.value], mode='event')
```

### in the scope of a Parameterized class being able to replace `@param.depends(..., watch=True)` with a decorator `@param.watch(...)`

`@param.depends()` being only declarative by default certainly causes confusion (for me at least when I got started), it's a bit strange that decorating a callback with a set of parameters to depend on isn't enough for the callback to be called on their changes. Of course this all makes sense when you realize this declaration is to be used by a library like Panel to set up its own update mechanisms. If you use Param on its own, that's definitely a weird default.

I can't count how many times I have forgotten to add `watch=True` and I have at least one large app in which every `@param.depends` decorator needs to have `watch=True`.

For all these reasons I'm interested in a more explicit `param.watch` decorator to be used in the scope of a Parameterized class. I think what I suggested above with the ability to select the signature/payload mode could also be nice to have.

```python
class P(param.Parameterized):

x = param.Number()

@param.watch('x')
def cb(self): pass
```

Contributor guide

Open the contributing guide

Research direction

No files or tests are named. Start by reading the existing obj.param.watch and @param.depends(..., watch=True) entry points, then compare their callback signatures and options with the proposed top-level param.watch and decorator APIs. Done requires a settled API design and tests covering registration, payload modes, and decorator behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.