traitlets
traitlets copied to clipboard
Improving link and dlink
link and dlink are good for simple uses, but lack certain features
- They are not compatible with
Outputwidgets, making debugging a lot harder, see https://github.com/jupyter-widgets/ipywidgets/issues/2306 - They contain redundant code
- They are built around the "lock" pattern, but in an ad-hoc way: there's no context manager. Such a thing would be quite useful for a user needing a slightly more sophisticated linking pattern, say, linking a 2-Tuple trait with 2 Tuple traits, etc.
Is there any interest adding some optional arguments to link and dlink, and creating a Lock context manager?
I would be happy to do a PR
Linking with contexts
Consider the following example
from traitlets import HasTraits, Int, observe, link
import ipywidgets as widgets
class Model(HasTraits):
value = Int()
@observe('value')
def _on_value(self, change):
raise RuntimeError
model = Model()
field = widgets.IntText()
link((model, 'value'), (field, 'value'))
When you type into field, a RuntimeError is raised but this is not possible to capture, even using the new optional transform argument of link.
Adding an extra optional context argument to link, the problem could be solved as
import ipywidgets as widgets
from traitlets import HasTraits, Int, observe
class Model(HasTraits):
value = Int()
@observe('value')
def _on_value(self, change):
raise RuntimeError
model = Model()
field = widgets.IntText()
output = widgets.Output()
link((model, 'value'), (field, 'value'), context=lambda: output)
The implementation is just adding a with self.context() block to link._update_source, link._update_target and link.link and a default context manager:
class TrivialContext(object):
def __enter__(self):
pass
def __exit__(self, *args):
pass
Alternatively, this change could be made on the ipywidgets side, instead of just importing link and dlink from traitlets.
Adding a Lock pattern
The lock pattern is used internally in traitlets for cross validation, as well as in linking, as mentioned above. It's also useful in building traitlets models.
Consider the following app that let's you perform a heavy calculation (summing two integers)
import ipywidgets as widgets
import time
from traitlets import HasTraits, Int, link
# defining the model
class Model(HasTraits):
a = Int()
b = Int()
sum = Int()
@observe('a', 'b')
def _on_a_b(self, change):
self._update_sum()
def _update_sum(self):
time.sleep(1)
self.sum = self.a + self.b
model = Model()
model.a = 3
model.b = 4
print(model.sum)
As written, it's inefficient because the model provides no way of setting a and b simultaneously without recomputing the sum twice.
Now consider the same app, enhanced with a lock pattern:
class Model(HasTraits):
a = Int()
b = Int()
sum = Int()
_sum_lock = Instance(Lock, args=())
@observe('a', 'b')
def _on_a_b(self, change):
self._update_sum()
def set_both(self, a, b):
with self._sum_lock.locked():
self.a = a
self.b = b
self._update_sum()
def _update_sum(self):
with self._sum_lock.locked() as grabbed:
Lock.require(grabbed)
time.sleep(1)
self.sum = self.a + self.b
model = Model()
model.set_both(3, 4)
print(model.sum)
And an example implementation of the Lock class
from contextlib import contextmanager
class SkipException(Exception):
pass
class Lock(object):
def __init__(self):
self._locked = False
@contextmanager
def locked(self):
"""
Switch to a locked state
Returns whether lock was grabbed
"""
grabbed = not self._locked
self._locked = True
try:
yield grabbed
except SkipException:
pass
finally:
self._locked = not grabbed
@staticmethod
def require(grabbed):
"""
Escapes context if lock was not grabbed
"""
if not grabbed:
raise SkipException
The pattern could be used to rewrite the traitlets internals mentioned above and is quite useful when developing HasTraits models as well.
In particular, there is no need to have separate code for link and dlink. link can simply create two dlinks using the same lock.
@krey would hold_trait_notifications accomplish this? I know the given example is about validation, but I know that @observe handlers are also put on hold:
https://traitlets.readthedocs.io/en/stable/using_traitlets.html?highlight=hold_trait_notifications#validation
@rmorshea I'm interested in addressing the following properties of link and dlink (copied from the original post):
- They are not compatible with Output widgets, making debugging a lot harder, see jupyter-widgets/ipywidgets#2306
- They contain redundant code
- They are built around the "lock" pattern, but in an ad-hoc way: there's no context manager. Such a thing would be quite useful for a user needing a slightly more sophisticated linking pattern, say, linking a 2-Tuple trait with 2 Tuple traits, etc.
Which one are you referring to when you write "accomplish this"?
@krey those are definitely problems that should be addressed, however I think a past proposal for a mute_trait_notifications context manager might suite you needs. The version from the proposal is global, but it could be made to accepts a list of optional trait names such that only those provided would have notifications muted.
This also seems related to https://github.com/ipython/traitlets/issues/389