DataContextsΒΆ

The DataContext class is a HasTraits subclass that provides a dictionary-like interface, and wraps another dictionary-like object (including other DataContexts, if desired). When the DataContext is modified, the wrapper layer generates items_modified events that other Traits objects can listen for and react to. In addition, there is a suite of subclasses of DataContext which perform different sorts of manipulations to items in the wrapped object.

At its most basic level, a DataContext object looks like a dictionary:

>>> from codetools.contexts.api import DataContext
>>> d = DataContext()
>>> d['a'] = 1
>>> d['b'] = 2
>>> d.items()
[('a', 1), ('b', 2)]

Internally, the DataContext has a subcontext trait attribute which holds the wrapped dictionary-like object:

>>> d.subcontext
{'a': 1, 'b': 2}

In the above case, the subcontext is a regular dictionary, but we can pass in any dictionary-like object into the constructor, including another DataContext object:

>>> data = {'c': 3, 'd': 4}
>>> d1 = DataContext(subcontext=data)
>>> d1.subcontext is data
True
>>> d2 = DataContext(subcontext=d)
>>> d2.subcontext.subcontext
{'a': 1, 'b': 2}

Whenever a DataContext object is modified, it generates a Traits event named items_modified. The object returned to listeners for this event is an ItemsModifiedEvent object, which has three trait attributes:

added
a list of keys which have been added to the DataContext
modified
a list of keys which have been modified in the DataContext
removed
a list of keys which have been deleted from the DataContext

To listen for the Traits events generated by the DataContext, you need to do something like the following:

from traits.api import HasTraits, Instance, on_trait_change
from codetools.contexts.api import DataContext

class DataContextListener(HasTraits):
    # the data context we are listening to
    data = Instance(DataContext)

    @on_trait_change('data.items_modified')
    def data_items_modified(self, event):
        if not self.traits_inited():
            return
        print "Event: items_modified"
        for added in event.added:
            print "  Added:", added, "=", repr(self.data[added])
        for modified in event.modified:
            print "  Modified:", modified, "=", repr(self.data[modified])
        for removed in event.removed:
            print "  Removed:", removed

This class keeps a reference to a DataContext object, and listens for any items_modified events that it generates. When one occurs, the data_items_modified() method gets the event and prints the details. The following code shows the DataContextListener in action:

>>> d = DataContext()
>>> listener = DataContextListener(data=d)
>>> d['a'] = 1
Event: items_modified
  Added: a = 1
>>> d['a'] = 'red'
Event: items_modified
  Modified: a = 'red'
>>> del d['a']
Event: items_modified
  Removed: a

Where this event generation becomes powerful is when a DataContext object is used as a namespace of a Block. By listening to events, we can have code which reacts to changes in a Block’s namespace as they occur. Consider the simple example from the Blocks section used in conjunction with a DataContext which is being listened to:

>>> block = Block("""# my calculations
... velocity = distance/time
... momentum = mass*velocity
... """)
>>> namespace = DataContext(subcontext={'distance': 10.0, 'time': 2.5, 'mass': 3.0})
>>> listener = DataContextListener(data=namespace)
>>> block.execute(namespace)
Event: items_modified
  Added: velocity = 4.0
Event: items_modified
  Added: momentum = 12.0
>>> namespace['mass'] = 4.0
Event: items_modified
  Modified: mass = 4.0
>>> block.restrict(inputs=('mass',)).execute(namespace)
Event: items_modified
  Modified: momentum = 16.0

The final piece in the pattern is to automate the execution of the block in the listener. When the listener detects a change in the input values for a block, it can restrict the block to the changed inputs and then execute the restricted block in the context, automatically closing the loop between changes in inputs and the resulting changes in outputs. Because the code is being restricted, only the absolute minimum of calculation is performed. The following example shows how to implement such an execution manager:

from traits.api import HasTraits, Instance, on_trait_change
from codetools.blocks.api import Block
from codetools.contexts.api import DataContext

class ExecutionManager(HasTraits):

    # the data context we are listening to
    data = Instance(DataContext)

    # the block we are executing
    block = Instance(Block)

    @on_trait_change('data.items_modified')
    def data_items_modified(self, event):
        if not self.traits_inited():
            return
        changed = set(event.added + event.modified + event.removed)
        inputs = changed & self.block.inputs
        outputs = changed & self.block.outputs
        for output in outputs:
            print "%s: %s" % (repr(output), repr(self.data[output]))
        self.execute(inputs)

    def execute(self, inputs):
        # Only execute if we have a non-empty set of inputs that are
        # available in the data.
        if len(inputs) > 0 and inputs.issubset(set(self.data.keys())):
            self.block.restrict(inputs=inputs).execute(self.data)