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 :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 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 :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 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)