holoviz/param

Class-level watchers and copying watchers behaviors

maximlt opened this issue · 1 comments

In #826 a discussion started on how the watchers are specially handled when a Parameter is copied from class to an instance. I've come up with some examples that exhibit some bugs and interesting behavior (e.g. side effects of calling p.param.x).

  1. Class-level watcher on default
store = []
def cb(event): store.append(event)

class P(param.Parameterized):
    x = param.Parameter()

P.param.watch(cb, 'x')

P.x = 10

assert len(store) == 1

print(P.param.x.watchers)
assert 'value' in P.param.x.watchers

p = P()

print(P.param.x.watchers)
assert 'value' in P.param.x.watchers

p.param.x  # side-effect

print(P.param.x.watchers)
assert P.param.x.watchers == {}  # :( Watchers no longer saved on the class Parameter

print(p.param.x.watchers)
assert 'value' in p.param.x.watchers  # :( Now saved on the instance Parameter

P.x = 20
p.x = 30

assert len(store) == 1  # Changes at the class or instance level no longer re-trigger the callback.
  1. Class-level watcher on an Parameter attribute (constant)
store = []
def cb(event): store.append(event)

class P(param.Parameterized):
    x = param.Parameter()

P.param.watch(cb, 'x', 'constant')

assert 'constant' in P.param.x.watchers

P.param.x.constant = True

assert len(store) == 1

p = P()

assert 'constant' in P.param.x.watchers
print(P.param.x.watchers)

p.param.x  # side-effect

assert P.param.x.watchers == {}  # :( Watchers no longer saved on the class Parameter
print(P.param.x.watchers)

print(p.param.x.watchers)
assert 'constant' in p.param.x.watchers  # :( Now saved on the instance Parameter

p.param.x.constant = False

assert len(store) == 2  # Changing constant on the instance triggers the callback

P.param.x.constant is True  # just checking the value

P.param.x.constant = False

assert len(store) == 2  # Changes at the class level no longer re-trigger the callback.
  1. Instance-level watcher on value
class P(param.Parameterized):
    x = param.Parameter()
    l = param.Parameter([])
    
    @param.depends('x', watch=True)
    def cb(self):
        self.l.append(self.x)

print(P.param.x.watchers)
assert P.param.x.watchers == {} 

print(P.param._depends)
assert P.param._depends['watch'][0][3][0].what == 'value'

p = P()

print(P.param.x.watchers)
assert P.param.x.watchers == {}

print(p.param.watchers)
assert 'x' in p.param.watchers

print(P.param._depends)
assert P.param._depends['watch'][0][3][0].what == 'value'

p.x = 30

assert len(p.l) == 1
# Nothing wrong in this example, just checking how things work
  1. Instance-level watcher on an attribute, two instances
class P(param.Parameterized):
    x = param.Parameter()
    l = param.List([])
    
    @param.depends('x:constant', watch=True)
    def cb(self):
        print('cb', self.param.x.constant)
        self.l.append(self.param.x.constant)

print(P.param.x.watchers)
assert P.param.x.watchers == {}

print(P.param._depends)
assert P.param._depends['watch'][0][3][0].what == 'constant'

p = P()

print(P.param.x.watchers)
assert 'constant' in P.param.x.watchers

p2 = P()
p2.param.x.constant = True

assert p2.l == [True]  # ok

assert p.l == [False]  # :( Changing the attribute on another instance triggered the callback attached to instance `p` too
  1. Instance-level watcher on an attribute
class P(param.Parameterized):
    x = param.Parameter()
    l = param.List([])
    
    @param.depends('x:constant', watch=True)
    def cb(self):
        self.l.append(self.param.x.constant)

print(P.param.x.watchers)
assert P.param.x.watchers == {}

print(P.param._depends)
assert P.param._depends['watch'][0][3][0].what == 'constant'

p = P()

print(P.param.x.watchers)
assert 'constant' in P.param.x.watchers   # The watchers are temporarily set on the class Parameter?
P.param.x.constant = False

print(p.l, P.l)
assert p.l == P.l == []

p.param.x  # side-effect

print(P.param.x.watchers)
assert P.param.x.watchers  == {}

print(p.param.x.watchers)
assert 'constant' in p.param.x.watchers

p.param.x.constant is False  # just checking the value

p.param.x.constant = True

print(p.l)
assert p.l == [True]

Somewhat related to #773