
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 enthought.contexts.api import DataContext
    >>> d = DataContext()
    >>> d['a'] = 1
    >>> d['b'] = 2
    >>> d.items()
    [('a', 1), ('b', 2)]

Internally, the DataContext has a :attr:`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 :class:`ItemsModifiedEvent` object, which has three trait attributes:

:attr:`added`
    a list of keys which have been added to the DataContext
:attr:`modified`
    a list of keys which have been modified in the DataContext
:attr:`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 enthought.traits.api import HasTraits, Instance, on_trait_change
    from enthought.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
:attr:`items_modified` events that it generates. When one occurs, the
:meth:`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 :ref:`codetools-tutorial-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 enthought.traits.api import HasTraits, Instance, on_trait_change
    from enthought.blocks.api import Block
    from enthought.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)
