How can I fire a Traits static event notification on a List?
Question:
I am working through the traits
presentation from PyCon 2010. At about 2:30:45 the presenter starts covering trait event notifications, which allow (among other things) the ability to automatically call a subroutine any time a trait
has changed.
I am running a modified copy of the example he gave… In this trial, I am trying to see whether I can fire a static event whenever I make a change to volume
or volume_inputs
.
# Filename: spinaltap.py
from traits.api import HasTraits, Range, List, Float
import traits
class Amplifier(HasTraits):
"""
Define an Amplifier (ref -> Spinal Tap) with Enthought's traits. Use traits
to enforce values boundaries on the Amplifier's attributes. Use events to
notify via the console when the volume trait is changed and when new volume
traits are added to inputs.
"""
# Define a volume trait as a Float between 0.0 and 11.0 (inclusive)
# see self._volume_changed()
volume = Range(value=5.0, trait=Float, low=0.0, high=11.0)
# Define an inputs trait as a List() containing volume traits
volume_inputs = List(volume) # <-- fire a static trait notification
# when another volume element is added
# see self._volume_inputs_changed()
def __init__(self, volume=5.0):
super(Amplifier, self).__init__()
self.volume = volume
self.volume_inputs.append(volume)
def _volume_changed(self, old, new):
# This is a static event listener for self.volume
# ^^^^^^^^^^^
if not (new in self.inputs):
self.inputs.append(self.volume)
if new == 11.0:
print("This one goes to eleven... so far, we have seen", self.inputs)
def _volume_inputs_changed(self, old, new):
# This is a static event listener for self.volume_inputs
# ^^^^^^^^^^^^^^^^^^
print("Check it out!!")
if __name__=='__main__':
spinal_tap = Amplifier()
candidate_volume = 4.0
spinal_tap.event_fired = False
print("- INITIAL_VALUE var volume_inputs = {}".format(spinal_tap.volume_inputs))
print("- APPEND a new volume of 4.0")
print(" - volume_inputs = {} # BEGIN".format(spinal_tap.volume_inputs))
print(" - volume_inputs.append({})".format(candidate_volume))
spinal_tap.volume_inputs.append(candidate_volume)
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
if spinal_tap.event_fired is False:
print(" - Test FAILED: Traits did not fire _volume_inputs_changed()")
else:
print(" - Test PASSED: Traits fired _volume_inputs_changed()")
try:
spinal_tap.event_fired = False
print("- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds")
print(" - volume_inputs: {} # BEGIN".format(spinal_tap.volume_inputs))
candidate_volume = 12.0
print(" - volume_inputs.append({})".format(candidate_volume))
spinal_tap.volume_inputs.append(candidate_volume)
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
if spinal_tap.event_fired is False:
print(" - Test FAILED: Traits did not fire _volume_inputs_changed()")
except traits.trait_errors.TraitError:
print(" - TraitError raised --> HERE <--")
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
print(" - Test PASSED: traits correctly raised TraitError instead of appending {}.".format(candidate_volume))
When I run this script, I can see This one goes to eleven... so far, we have seen [5.0, 11.0]
in the console output, so I know that _volume_changed()
gets fired when I assign 11.0
to spinal_tap.volume
.
However, I never see any events from _volume_inputs_changed()
. No matter what example I cook up, I can’t get a List
to fire an event.
This is the output I am seeing… note that there is no evidence that _volume_inputs_changed()
ever fires.
[[email protected] ~]$ python spinaltap.py
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
- volume_inputs = [5.0] # BEGIN
- volume_inputs.append(4.0)
- volume_inputs: [5.0, 4.0] # END
- Test FAILED: Traits did not fire _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds
- volume_inputs: [5.0, 4.0] # BEGIN
- volume_inputs.append(12.0)
- TraitError raised --> HERE <--
- volume_inputs: [5.0, 4.0] # END
- Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[[email protected] ~]$
Should a List()
be able to fire a static List()
event (such as _inputs_changed()
) when using traits? If so, am I doing something wrong?
Answers:
After browsing their unit tests, I found a test for Dict
traits in enthought’s event unittest coverage… it looks like when you have a container like a Dict
or List
that you need to set up the magic event listener method like this:
## Broken method definition: def _inputs_changed(self, old, new):
# container event static listeners must be in the form of _foo_items_changed()
def _volume_inputs_items_changed(self, old, new):
# static event listener for self.volume_inputs
if len(new.added) > 0:
print "Check it out, we added %s to self.items" % new.added
elif len(new.removed) > 0:
print "Check it out, we removed %s from self.items" % new.removed
Likewise, I also discovered that the on_trait_change
decorator (used for dynamic traits
event notification) requires similar nomenclature if you are calling it with a traits.api.List
or traits.api.Dict
… so I could also write the code above as:
from traits.api import on_trait_change
# ...
@on_trait_change('volume_inputs_items')
def something_changed(self, name, new):
# static event listener for self.volume_inputs
if len(new.added) > 0:
print "Check it out, we added %s to self.items" % new.added
elif len(new.removed) > 0:
print "Check it out, we removed %s from self.items" % new.removed
Either way, when I run the code, I get expected output:
[[email protected] ~]$ python spinaltap.py
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
- volume_inputs = [5.0] # BEGIN
- volume_inputs.append(4.0)
- volume_inputs: [5.0, 4.0] # END
- Test FAILED: Traits did not fire _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds
- volume_inputs: [5.0, 4.0] # BEGIN
- volume_inputs.append(12.0)
- TraitError raised --> HERE <--
- volume_inputs: [5.0, 4.0] # END
- Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[[email protected] ~]$
As this also caught me out recently, I’ve just verified Mike Pennington’s answer with Traits 4.2.1. There does seem to be a distinction between changes to the List trait itself (such as assigning a new list to it), and changes to the membership of the List (such as appending or setting by index). The former uses the same name as the trait (e.g. inputs
), whereas the latter uses the “_items” suffix. This example seems to demonstrate this:
from traits.api import Float, HasTraits, Instance, List
class Part(HasTraits):
costs = List(Float)
# called when the actual List trait changes:
def _costs_changed(self, old, new):
print("Part::_costs_changed %s -> %s" % (str(old), str(new)))
# called when the contents of the List trait changes:
def _costs_items_changed(self, old, new):
print("Part::_costs_changed %s -> %s" % (str(old), str(new)))
class Widget(HasTraits):
part = Instance(Part)
def __init__(self):
self.part = Part()
self.part.on_trait_change(self.update_costs, 'costs')
self.part.on_trait_change(self.update_costs_items, 'costs_items')
def update_costs(self, name, new):
print("update_costs: %s = %s" % (name, str(new),))
def update_costs_items(self, name, new):
print("update_costs_items: %s = %s" % (name, str(new),))
w = Widget()
w.part.costs = [ 1.0, 2.0, 3.0 ]
# Part::_costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]
w.part.costs = [ 1.0, 2.0, 3.1 ]
# Part::_costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]
w.part.costs[0] = 5.0
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
w.part.costs.append(4.0)
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
This behaviour is hinted at in the documentation here.
However if an extended name is used it does seem possible to have the same handler called when the entire list or membership is changed:
from traits.api import Float, HasTraits, Instance, List
class Part(HasTraits):
costs = List(Float)
def _costs_changed(self, old, new):
print("_costs_changed %s -> %s" % (str(old), str(new)))
def _costs_items_changed(self, old, new):
print("_costs_items_changed %s -> %s" % (str(old), str(new)))
class Widget(HasTraits):
part = Instance(Part)
def __init__(self):
self.part = Part()
self.part.on_trait_change(self.update_costs, 'costs[]') # <-- extended name
def update_costs(self, name, new):
print("update_costs: %s = %s" % (name, str(new),))
w = Widget()
w.part.costs = [ 1.0, 2.0, 3.0 ]
# _costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]
w.part.costs = [ 1.0, 2.0, 3.1 ]
# _costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]
w.part.costs[0] = 5.0
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [5.0]
w.part.costs.append(4.0)
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [4.0]
In this case, the name
parameter of the update_costs
handler can be used to differentiate between the container itself changing, or a single item within the container changing.
I am working through the traits
presentation from PyCon 2010. At about 2:30:45 the presenter starts covering trait event notifications, which allow (among other things) the ability to automatically call a subroutine any time a trait
has changed.
I am running a modified copy of the example he gave… In this trial, I am trying to see whether I can fire a static event whenever I make a change to volume
or volume_inputs
.
# Filename: spinaltap.py
from traits.api import HasTraits, Range, List, Float
import traits
class Amplifier(HasTraits):
"""
Define an Amplifier (ref -> Spinal Tap) with Enthought's traits. Use traits
to enforce values boundaries on the Amplifier's attributes. Use events to
notify via the console when the volume trait is changed and when new volume
traits are added to inputs.
"""
# Define a volume trait as a Float between 0.0 and 11.0 (inclusive)
# see self._volume_changed()
volume = Range(value=5.0, trait=Float, low=0.0, high=11.0)
# Define an inputs trait as a List() containing volume traits
volume_inputs = List(volume) # <-- fire a static trait notification
# when another volume element is added
# see self._volume_inputs_changed()
def __init__(self, volume=5.0):
super(Amplifier, self).__init__()
self.volume = volume
self.volume_inputs.append(volume)
def _volume_changed(self, old, new):
# This is a static event listener for self.volume
# ^^^^^^^^^^^
if not (new in self.inputs):
self.inputs.append(self.volume)
if new == 11.0:
print("This one goes to eleven... so far, we have seen", self.inputs)
def _volume_inputs_changed(self, old, new):
# This is a static event listener for self.volume_inputs
# ^^^^^^^^^^^^^^^^^^
print("Check it out!!")
if __name__=='__main__':
spinal_tap = Amplifier()
candidate_volume = 4.0
spinal_tap.event_fired = False
print("- INITIAL_VALUE var volume_inputs = {}".format(spinal_tap.volume_inputs))
print("- APPEND a new volume of 4.0")
print(" - volume_inputs = {} # BEGIN".format(spinal_tap.volume_inputs))
print(" - volume_inputs.append({})".format(candidate_volume))
spinal_tap.volume_inputs.append(candidate_volume)
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
if spinal_tap.event_fired is False:
print(" - Test FAILED: Traits did not fire _volume_inputs_changed()")
else:
print(" - Test PASSED: Traits fired _volume_inputs_changed()")
try:
spinal_tap.event_fired = False
print("- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds")
print(" - volume_inputs: {} # BEGIN".format(spinal_tap.volume_inputs))
candidate_volume = 12.0
print(" - volume_inputs.append({})".format(candidate_volume))
spinal_tap.volume_inputs.append(candidate_volume)
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
if spinal_tap.event_fired is False:
print(" - Test FAILED: Traits did not fire _volume_inputs_changed()")
except traits.trait_errors.TraitError:
print(" - TraitError raised --> HERE <--")
print(" - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
print(" - Test PASSED: traits correctly raised TraitError instead of appending {}.".format(candidate_volume))
When I run this script, I can see This one goes to eleven... so far, we have seen [5.0, 11.0]
in the console output, so I know that _volume_changed()
gets fired when I assign 11.0
to spinal_tap.volume
.
However, I never see any events from _volume_inputs_changed()
. No matter what example I cook up, I can’t get a List
to fire an event.
This is the output I am seeing… note that there is no evidence that _volume_inputs_changed()
ever fires.
[[email protected] ~]$ python spinaltap.py
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
- volume_inputs = [5.0] # BEGIN
- volume_inputs.append(4.0)
- volume_inputs: [5.0, 4.0] # END
- Test FAILED: Traits did not fire _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds
- volume_inputs: [5.0, 4.0] # BEGIN
- volume_inputs.append(12.0)
- TraitError raised --> HERE <--
- volume_inputs: [5.0, 4.0] # END
- Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[[email protected] ~]$
Should a List()
be able to fire a static List()
event (such as _inputs_changed()
) when using traits? If so, am I doing something wrong?
After browsing their unit tests, I found a test for Dict
traits in enthought’s event unittest coverage… it looks like when you have a container like a Dict
or List
that you need to set up the magic event listener method like this:
## Broken method definition: def _inputs_changed(self, old, new):
# container event static listeners must be in the form of _foo_items_changed()
def _volume_inputs_items_changed(self, old, new):
# static event listener for self.volume_inputs
if len(new.added) > 0:
print "Check it out, we added %s to self.items" % new.added
elif len(new.removed) > 0:
print "Check it out, we removed %s from self.items" % new.removed
Likewise, I also discovered that the on_trait_change
decorator (used for dynamic traits
event notification) requires similar nomenclature if you are calling it with a traits.api.List
or traits.api.Dict
… so I could also write the code above as:
from traits.api import on_trait_change
# ...
@on_trait_change('volume_inputs_items')
def something_changed(self, name, new):
# static event listener for self.volume_inputs
if len(new.added) > 0:
print "Check it out, we added %s to self.items" % new.added
elif len(new.removed) > 0:
print "Check it out, we removed %s from self.items" % new.removed
Either way, when I run the code, I get expected output:
[[email protected] ~]$ python spinaltap.py
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
- volume_inputs = [5.0] # BEGIN
- volume_inputs.append(4.0)
- volume_inputs: [5.0, 4.0] # END
- Test FAILED: Traits did not fire _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0. This should fail; 12.0 is out of bounds
- volume_inputs: [5.0, 4.0] # BEGIN
- volume_inputs.append(12.0)
- TraitError raised --> HERE <--
- volume_inputs: [5.0, 4.0] # END
- Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[[email protected] ~]$
As this also caught me out recently, I’ve just verified Mike Pennington’s answer with Traits 4.2.1. There does seem to be a distinction between changes to the List trait itself (such as assigning a new list to it), and changes to the membership of the List (such as appending or setting by index). The former uses the same name as the trait (e.g. inputs
), whereas the latter uses the “_items” suffix. This example seems to demonstrate this:
from traits.api import Float, HasTraits, Instance, List
class Part(HasTraits):
costs = List(Float)
# called when the actual List trait changes:
def _costs_changed(self, old, new):
print("Part::_costs_changed %s -> %s" % (str(old), str(new)))
# called when the contents of the List trait changes:
def _costs_items_changed(self, old, new):
print("Part::_costs_changed %s -> %s" % (str(old), str(new)))
class Widget(HasTraits):
part = Instance(Part)
def __init__(self):
self.part = Part()
self.part.on_trait_change(self.update_costs, 'costs')
self.part.on_trait_change(self.update_costs_items, 'costs_items')
def update_costs(self, name, new):
print("update_costs: %s = %s" % (name, str(new),))
def update_costs_items(self, name, new):
print("update_costs_items: %s = %s" % (name, str(new),))
w = Widget()
w.part.costs = [ 1.0, 2.0, 3.0 ]
# Part::_costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]
w.part.costs = [ 1.0, 2.0, 3.1 ]
# Part::_costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]
w.part.costs[0] = 5.0
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
w.part.costs.append(4.0)
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
This behaviour is hinted at in the documentation here.
However if an extended name is used it does seem possible to have the same handler called when the entire list or membership is changed:
from traits.api import Float, HasTraits, Instance, List
class Part(HasTraits):
costs = List(Float)
def _costs_changed(self, old, new):
print("_costs_changed %s -> %s" % (str(old), str(new)))
def _costs_items_changed(self, old, new):
print("_costs_items_changed %s -> %s" % (str(old), str(new)))
class Widget(HasTraits):
part = Instance(Part)
def __init__(self):
self.part = Part()
self.part.on_trait_change(self.update_costs, 'costs[]') # <-- extended name
def update_costs(self, name, new):
print("update_costs: %s = %s" % (name, str(new),))
w = Widget()
w.part.costs = [ 1.0, 2.0, 3.0 ]
# _costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]
w.part.costs = [ 1.0, 2.0, 3.1 ]
# _costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]
w.part.costs[0] = 5.0
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [5.0]
w.part.costs.append(4.0)
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [4.0]
In this case, the name
parameter of the update_costs
handler can be used to differentiate between the container itself changing, or a single item within the container changing.