Can you define custom observables?

With default widgets, such as sliders, it is possible to observe the value or other traits so that any changes trigger a given function (see here):

w_slider = widgets.IntSlider()
w_output = widgets.Output()

def on_value_change(change_dict):
    with w_output:
        print(change_dict['new'])

w_slider.observe(on_value_change, names='value')

display(w_slider, w_output)

(I couldn’t find a comprehensive list of available observable traits for each widget type, but it is not relevant for my question I think)

Is it possible to define/trigger custom observables in subclassed widgets? I mean, without having to implement the pattern myself (i.e. having to write my own suscribe_observer and notify_observer methods to keep up with the list of subscribers and notifications). I thought of 2 options of how it might be done, but maybe there is another way.

# Define the component
#----------------------------------------------------------------#
class CustomComponent(widgets.VBox):

    def __init__(self):
        self._internal_value = None
        # initialization of sub-components

    # Option 1: trigger the observe event when making changes
    def change_internal_value(self, newval):
        self._trigger_event("internal_value", {'type': 'change'})
        self._internal_value = newval

    # Option 2: mark a property as an observable and ipywidgets takes care of triggering events
    @widgets.mark_as_observable
    @property
    def internal_value(self):
        return self._internal_value


# Instantiate and observe the component
#----------------------------------------------------------------#
custom_component = CustomComponent()

def dont_touch(change_dict):
    print('Careful, you are changing the value!')

custom_component.observe(dont_touch, "internal_value")

possible to observe the value

looks like that was answered?

define/trigger custom observables in subclassed widgets

The tool you’re looking for is one layer below widgets, in traitlets.observe, which can be used inside a class declaration. This is uses in ipywidgets, but also a lot of IPython, jupyter_server, etc: having a look through those codebases for interesting uses of traitlets might dig up some treasures.

Once you get in the traitlets paradigm… pretty much everything can be a trait but maybe not everything should. You can even make your observers a trait if you want to go full quis-custodiet-ipsos-custodes.

import ipywidgets as W
import traitlets as T

class Foo(W.Widget):
    value = T.Unicode().tag(sync=True)
    extra_observers = T.Tuple()

    @T.observe("value")
    def _on_value_changed(self, change):
        [observer(change) for observer in self.extra_observers]

foo = Foo()
foo.extra_observers += (lambda x: print(x),)
foo.value = "1"

list of available observable traits for each widget type

In a live session, grab a copy of some_widget.traits() and have a look.

>>> foo.traits()

{'_model_module': <traitlets.traitlets.Unicode at 0x1ce4d80>,
 '_model_module_version': <traitlets.traitlets.Unicode at 0x16a76f0>,
 '_model_name': <traitlets.traitlets.Unicode at 0x16a6cf0>,
 '_msg_callbacks': <traitlets.traitlets.Instance at 0x1ccdd20>,
 '_property_lock': <traitlets.traitlets.Dict at 0x21b7a48>,
 '_states_to_send': <traitlets.traitlets.Set at 0x1e2bcc8>,
 '_view_count': <traitlets.traitlets.Int at 0x1ce6ff8>,
 '_view_module': <traitlets.traitlets.Unicode at 0x22cacd0>,
 '_view_module_version': <traitlets.traitlets.Unicode at 0x2323670>,
 '_view_name': <traitlets.traitlets.Unicode at 0x1f560d0>,
 'comm': <traitlets.traitlets.Any at 0x1da0dd0>,
 'extra_observers': <ipywidgets.widgets.trait_types.TypedTuple at 0x2580988>,
 'keys': <traitlets.traitlets.List at 0x2197020>,
 'log': <traitlets.traitlets.Instance at 0x1d8dea8>,
 'value': <traitlets.traitlets.Unicode at 0x28bc790>}

The docs recently got somewhat better, but this descriptor pattern used by traitlets, and to extend widgets, is somewhat complex for the docs to reason about.

Traits can be added dynamically, further compounding the complexity and uniqueness of every setup:

foo.add_traits(bar=T.Unicode())
1 Like