Skip to content

Commit

Permalink
observe via tags
Browse files Browse the repository at this point in the history
  • Loading branch information
rmorshea committed Dec 21, 2015
1 parent e48fb06 commit 6a0770b
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 34 deletions.
48 changes: 44 additions & 4 deletions traitlets/tests/test_traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,10 @@ class A(HasTraits):
a.on_trait_change(callback4, 'a')
a.a = 100000
self.assertEqual(self.cb,('a',10000,100000,a))
self.assertEqual(len(a._trait_notifiers['a']['change']), 1)
self.assertEqual(len(a._trait_notifiers['names']['a']['change']), 1)
a.on_trait_change(callback4, 'a', remove=True)

self.assertEqual(len(a._trait_notifiers['a']['change']), 0)
self.assertEqual(len(a._trait_notifiers['names']['a']['change']), 0)

def test_notify_only_once(self):

Expand Down Expand Up @@ -614,10 +614,10 @@ class A(HasTraits):
a.a = 100
change = change_dict('a', 10, 100, a, 'change')
self.assertEqual(self.cb, change)
self.assertEqual(len(a._trait_notifiers['a']['change']), 1)
self.assertEqual(len(a._trait_notifiers['names']['a']['change']), 1)
a.unobserve(callback1, 'a')

self.assertEqual(len(a._trait_notifiers['a']['change']), 0)
self.assertEqual(len(a._trait_notifiers['names']['a']['change']), 0)

def test_notify_only_once(self):

Expand Down Expand Up @@ -658,6 +658,46 @@ def _a_changed(self, change):
self.assertEqual(b.b, b.c)
self.assertEqual(b.b, b.d)

def test_observe_decorator_via_tags(self):
class A(HasTraits):
foo = Int()
bar = Int().tag(test=True)

@observe(tags={'test':True})
def _test_observer(self, change):
self.foo = change['new']

a = A()
a.bar = 1
self.assertEqual(a.foo, a.bar)

a.add_traits(baz=Int().tag(test=True))
a.baz = 2
self.assertEqual(a.foo, a.baz)

def test_observe_via_tags(self):

class A(HasTraits):
foo = Int()
bar = Int().tag(test=True)

a = A()

def _test_observer(change):
a.foo = change['new']

a.observe(_test_observer, tags={'test':True})
a.bar = 1
self.assertEqual(a.foo, a.bar)

a.add_traits(baz=Int().tag(test=True))
a.baz = 2
self.assertEqual(a.foo, a.baz)

a.unobserve(_test_observer)
a.bar = 3
self.assertNotEqual(a.bar, a.baz)


class TestHasTraits(TestCase):

Expand Down
126 changes: 96 additions & 30 deletions traitlets/traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,9 @@ def observe(*names, **kwargs):
*names
The str names of the Traits to observe on the object.
"""
return ObserveHandler(names, type=kwargs.get('type', 'change'))
tags = kwargs.get('tags', {})
type = kwargs.get('type', 'change')
return ObserveHandler(names, type=type, tags=tags)

def validate(*names):
"""A decorator to register cross validator of HasTraits object's state
Expand Down Expand Up @@ -837,12 +839,14 @@ def __get__(self, inst, cls=None):

class ObserveHandler(EventHandler):

def __init__(self, names, type):
def __init__(self, names, type, tags):
self.trait_names = names
self.tags = tags
self.type = type

def instance_init(self, inst):
inst.observe(self, self.trait_names, type=self.type)
inst.observe(self, self.trait_names,
tags=self.tags, type=self.type)


class ValidateHandler(EventHandler):
Expand Down Expand Up @@ -898,7 +902,7 @@ class HasTraits(py3compat.with_metaclass(MetaHasTraits, HasDescriptors)):

def setup_instance(self):
self._trait_values = {}
self._trait_notifiers = {}
self._trait_notifiers = {'names': {}, 'tags': {}}
self._trait_validators = {}
super(HasTraits, self).setup_instance()

Expand All @@ -916,7 +920,7 @@ def __getstate__(self):
# event handlers stored on an instance are
# expected to be reinstantiated during a
# recall of instance_init during __setstate__
d['_trait_notifiers'] = {}
d['_trait_notifiers'] = {'names': {}, 'tags': {}}
d['_trait_validators'] = {}
return d

Expand Down Expand Up @@ -1022,10 +1026,25 @@ def notify_change(self, change):
name, type = change['name'], change['type']

callables = []
callables.extend(self._trait_notifiers.get(name, {}).get(type, []))
callables.extend(self._trait_notifiers.get(name, {}).get(All, []))
callables.extend(self._trait_notifiers.get(All, {}).get(type, []))
callables.extend(self._trait_notifiers.get(All, {}).get(All, []))
callables.extend(self._trait_notifiers['names'].get(name, {}).get(type, []))
callables.extend(self._trait_notifiers['names'].get(name, {}).get(All, []))
callables.extend(self._trait_notifiers['names'].get(All, {}).get(type, []))
callables.extend(self._trait_notifiers['names'].get(All, {}).get(All, []))

trait = getattr(self.__class__, name)
for k, v in trait.metadata.items():
try:
for n in self._trait_notifiers['tags'][k][v][type]:
if n not in callables:
callables.append(n)
except KeyError:
pass
try:
for n in self._trait_notifiers['tags'][k][v][All]:
if n not in callables:
callables.append(n)
except KeyError:
pass

# Now static ones
magic_name = '_%s_changed' % name
Expand All @@ -1051,27 +1070,63 @@ def notify_change(self, change):

c(change)

def _add_notifiers(self, handler, name, type):
if name not in self._trait_notifiers:
nlist = []
self._trait_notifiers[name] = {type: nlist}
else:
if type not in self._trait_notifiers[name]:
def _add_notifiers(self, handler, name, tags, type):
if name:
if name not in self._trait_notifiers['names']:
nlist = []
self._trait_notifiers['names'][name] = {type: nlist}
else:
if type not in self._trait_notifiers['names'][name]:
nlist = []
self._trait_notifiers['names'][name][type] = nlist
else:
nlist = self._trait_notifiers['names'][name][type]
if handler not in nlist:
nlist.append(handler)

if tags:
tagged = self._trait_notifiers['tags']
for k, v in tags.items():
if k in tagged and v in tagged[k]:
d = tagged[k][v]
else:
d = {}
if k in tagged:
tagged[k][v] = d
else:
tagged[k] = {v: d}
if type not in d:
nlist = []
self._trait_notifiers[name][type] = nlist
d[type] = nlist
else:
nlist = self._trait_notifiers[name][type]
if handler not in nlist:
nlist.append(handler)
nlist = d[type]
if handler not in nlist:
nlist.append(handler)

def _remove_notifiers(self, handler, name, type):
def _remove_notifiers(self, handler, name, tags, type):
try:
if handler is None:
del self._trait_notifiers[name][type]
del self._trait_notifiers['names'][name][type]
else:
self._trait_notifiers[name][type].remove(handler)
self._trait_notifiers['names'][name][type].remove(handler)
if name is not All:
trait = getattr(self.__class__, name, None)
if trait is not None:
for k, v in trait.metadata.items():
if handler is None:
del self._trait_notifiers['tags'][k][v]
else:
self._trait_notifiers['tags'][k][v].remove(handler)
if tags:
for k, v in tags.items():
if handler is None:
del self._trait_notifiers['tags'][k][v]
else:
self._trait_notifiers['tags'][k][v].remove(handler)
except KeyError:
pass
except AttributeError:
pass

def on_trait_change(self, handler=None, name=None, remove=False):
"""DEPRECATED: Setup a handler to be called when a trait changes.
Expand Down Expand Up @@ -1110,7 +1165,7 @@ def on_trait_change(self, handler=None, name=None, remove=False):
else:
self.observe(_callback_wrapper(handler), names=name)

def observe(self, handler, names=All, type='change'):
def observe(self, handler, names=None, tags=None, type='change'):
"""Setup a handler to be called when a trait changes.
This is used to setup dynamic notifications of trait changes.
Expand All @@ -1121,26 +1176,37 @@ def observe(self, handler, names=All, type='change'):
A callable that is called when a trait changes. Its
signature can be ``handler()`` or ``handler(change)``, where change
is a dictionary. The change dictionary at least holds a 'type' key.
* `type``: the type of notification.
* ``type``: the type of notification.
Other keys may be passed depending on the value of 'type'. In the
case where type is 'change', we also have the following keys:
* ``owner`` : the HasTraits instance
* ``old`` : the old value of the modified trait attribute
* ``new`` : the new value of the modified trait attribute
* ``name`` : the name of the modified trait attribute.
names : list, str, All
names : list, str, All, None
If no tags and no names are given, ``names`` will defaults to All.
If names is All, the handler will apply to all traits. If a list
of str, handler will apply to all names in the list. If a
str, the handler will apply just to that name.
tags: dict
Allows the handler to apply to traits which have been tagged with
metadata that match that tags given here. Tags are dyanmic, so if
trait metadata changes, the handlers which they are associated with
will as well.
type : str, All (default: 'change')
The type of notification to filter by. If equal to All, then all
notifications are passed to the observe handler.
"""
names = parse_notifier_name(names)
for n in names:
self._add_notifiers(handler, n, type)
if names is None and tags is not None:
self._add_notifiers(handler, None, tags, type)
else:
if names is None and tags is None:
names = All
names = parse_notifier_name(names)
for n in names:
self._add_notifiers(handler, n, tags, type)

def unobserve(self, handler, names=All, type='change'):
def unobserve(self, handler, names=All, tags=None, type='change'):
"""Remove a trait change handler.
This is used to unregister handlers to trait change notificiations.
Expand All @@ -1159,7 +1225,7 @@ def unobserve(self, handler, names=All, type='change'):
"""
names = parse_notifier_name(names)
for n in names:
self._remove_notifiers(handler, n, type)
self._remove_notifiers(handler, n, tags, type)

def unobserve_all(self, name=All):
"""Remove trait change handlers of any type for the specified name.
Expand Down

0 comments on commit 6a0770b

Please sign in to comment.