Skip to content

Commit

Permalink
Ensure that Parameterized remaps watchers on copy (#409)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored May 12, 2020
1 parent f1ea7a9 commit a8e34b2
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 5 deletions.
36 changes: 34 additions & 2 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,17 @@ def _f(self, obj, val):
return _f


def get_method_owner(method):
"""
Gets the instance that owns the supplied method
"""
if not inspect.ismethod(method):
return None
if isinstance(method, partial):
method = method.func
return method.__self__ if sys.version_info.major >= 3 else method.im_self


@accept_arguments
def depends(func, *dependencies, **kw):
"""
Expand Down Expand Up @@ -475,8 +486,10 @@ def _params_depended_on(minfo):
return params


def _m_caller(self,n):
return lambda event: getattr(self,n)()
def _m_caller(self, n):
caller = lambda event: getattr(self,n)()
caller._watcher_name = n
return caller


PInfo = namedtuple("PInfo","inst cls name pobj what")
Expand Down Expand Up @@ -2432,6 +2445,25 @@ def __setstate__(self, state):
"""
self.initialized=False

# When making a copy the internal watchers have to be
# recreated and point to the new instance
if '_param_watchers' in state:
param_watchers = state['_param_watchers']
for p, attrs in param_watchers.items():
for attr, watchers in attrs.items():
new_watchers = []
for watcher in watchers:
watcher_args = list(watcher)
if watcher.inst is not None:
watcher_args[0] = self
fn = watcher.fn
if hasattr(fn, '_watcher_name'):
watcher_args[2] = _m_caller(self, fn._watcher_name)
elif get_method_owner(fn) is watcher.inst:
watcher_args[2] = getattr(self, fn.__name__)
new_watchers.append(Watcher(*watcher_args))
param_watchers[p][attr] = new_watchers

if '_instance__params' not in state:
state['_instance__params'] = {}
if '_param_watchers' not in state:
Expand Down
31 changes: 28 additions & 3 deletions tests/API1/testwatch.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Unit test for watch mechanism
"""
from . import API1TestCase

from .utils import MockLoggingHandler
import copy

import param

from param.parameterized import discard_events

from . import API1TestCase
from .utils import MockLoggingHandler


class Accumulator(object):

Expand Down Expand Up @@ -37,6 +38,9 @@ class SimpleWatchExample(param.Parameterized):
c = param.Parameter(default=0)
d = param.Integer(default=0)

def method(self, event):
self.b = self.a * 2


class SimpleWatchSubclass(SimpleWatchExample):
pass
Expand Down Expand Up @@ -466,6 +470,18 @@ def test_nested_batched_watch_not_onlychanged(self):
self.assertEqual(args[1].new, 0)
self.assertEqual(args[1].type, 'set')

def test_watch_deepcopy(self):
obj = SimpleWatchExample()

obj.param.watch(obj.method, ['a'])

copied = copy.deepcopy(obj)

copied.a = 2

self.assertEqual(copied.b, 4)
self.assertEqual(obj.b, 0)


class TestWatchMethod(API1TestCase):

Expand Down Expand Up @@ -523,6 +539,15 @@ def test_depends_with_watch_on_subclass(self):
obj.b = 3
self.assertEqual(obj.c, 6)

def test_watcher_method_deepcopy(self):
obj = WatchMethodExample(b=5)

copied = copy.deepcopy(obj)

copied.b = 11
self.assertEqual(copied.b, 10)
self.assertEqual(obj.b, 5)


class TestWatchValues(API1TestCase):

Expand Down

0 comments on commit a8e34b2

Please sign in to comment.