Repository: ipython/traitlets
Pull request #393 (2017-04-13 16:44:22)

Link tranform
Add ``transform`` argument to ``link`` that takes a tuple with the forward and backward transform.


Patch:
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index d3aa148..518c2df 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -258,6 +258,8 @@ class link(object):
     ----------
     source : (object / attribute name) pair
     target : (object / attribute name) pair
+    transform: iterable with two callables (optional)
+        Data transformation between source and target and target and source.
 
     Examples
     --------
@@ -267,15 +269,19 @@ class link(object):
     """
     updating = False
 
-    def __init__(self, source, target):
+    def __init__(self, source, target, transform=None):
         _validate_link(source, target)
         self.source, self.target = source, target
+        self._transform, self._transform_inv = (
+            transform if transform else (lambda x: x,) * 2)
+
         self.link()
 
     def link(self):
         try:
             setattr(self.target[0], self.target[1],
-                    getattr(self.source[0], self.source[1]))
+                    self._transform(getattr(self.source[0], self.source[1])))
+
         finally:
             self.source[0].observe(self._update_target, names=self.source[1])
             self.target[0].observe(self._update_source, names=self.target[1])
@@ -292,13 +298,22 @@ class link(object):
         if self.updating:
             return
         with self._busy_updating():
-            setattr(self.target[0], self.target[1], change.new)
+            setattr(self.target[0], self.target[1], self._transform(change.new))
+            if getattr(self.source[0], self.source[1]) != change.new:
+                raise TraitError(
+                    "Broken link {}: the source value changed while updating "
+                    "the target.".format(self))
 
     def _update_source(self, change):
         if self.updating:
             return
         with self._busy_updating():
-            setattr(self.source[0], self.source[1], change.new)
+            setattr(self.source[0], self.source[1],
+                    self._transform_inv(change.new))
+            if getattr(self.target[0], self.target[1]) != change.new:
+                raise TraitError(
+                    "Broken link {}: the target value changed while updating "
+                    "the source.".format(self))
 
     def unlink(self):
         self.source[0].unobserve(self._update_target, names=self.source[1])


Test patch:
diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py
index 3a81e88..9119ca5 100644
--- a/traitlets/tests/test_traitlets.py
+++ b/traitlets/tests/test_traitlets.py
@@ -1886,6 +1886,56 @@ class TestLink(TestCase):
         self.assertEqual(''.join(callback_count), 'ab')
         del callback_count[:]
 
+    def test_tranform(self):
+        """Test transform link."""
+
+        # Create two simple classes with Int traitlets.
+        class A(HasTraits):
+            value = Int()
+        a = A(value=9)
+        b = A(value=8)
+
+        # Conenct the two classes.
+        c = link((a, 'value'), (b, 'value'),
+                 transform=(lambda x: 2 * x, lambda x: int(x / 2.)))
+
+        # Make sure the values are correct at the point of linking.
+        self.assertEqual(b.value, 2 * a.value)
+
+        # Change one the value of the source and check that it modifies the target.
+        a.value = 5
+        self.assertEqual(b.value, 10)
+        # Change one the value of the target and check that it modifies the
+        # source.
+        b.value = 6
+        self.assertEqual(a.value, 3)
+
+    def test_link_broken_at_source(self):
+        class MyClass(HasTraits):
+            i = Int()
+            j = Int()
+
+            @observe("j")
+            def another_update(self, change):
+                self.i = change.new * 2
+
+        mc = MyClass()
+        l = link((mc, "i"), (mc, "j"))
+        self.assertRaises(TraitError, setattr, mc, 'i', 2)
+
+    def test_link_broken_at_target(self):
+        class MyClass(HasTraits):
+            i =Int()
+            j = Int()
+
+            @observe("i")
+            def another_update(self, change):
+                self.j = change.new * 2
+
+        mc = MyClass()
+        l = link((mc, "i"), (mc, "j"))
+        self.assertRaises(TraitError, setattr, mc, 'j', 2)
+
 class TestDirectionalLink(TestCase):
     def test_connect_same(self):
         """Verify two traitlets of the same type can be linked together using directional_link."""