I am loving this library, great work devs! I'm hoping you could help me work out a snag I'm facing.
I would like to create widgets that are compositions of other widgets and that have their own observe method. I'd like to do this in pure python if I can, and I think I am very close. What I have now is:
from ipywidgets import HBox, FloatRangeSlider, Button
class MyWidget(HBox):
def __init__(self):
super(MyWidget, self).__init__()
self.slider = FloatRangeSlider()
self.button = Button(description='next')
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
def move_up(self, change):
value = self.slider.value
self.slider.set_state({'value': (value[0] + 1, value[1] + 1)})
my_widget = MyWidget()
my_widget.observe = my_widget.slider.observe
my_widget.observe(lambda x: print(x))
my_widget
This achieves the desired result, but the problem with my current solution is that I have to add the observe method on an instance of the MyWidget class rather than putting this somehow in the MyWidget class definition. I have tried variants of
class MyWidget(HBox):
def __init__(self):
super(MyWidget, self).__init__()
self.slider = FloatRangeSlider()
self.button = Button(description='next')
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
def move_up(self, change):
value = self.slider.value
self.slider.set_state({'value': (value[0] + 1, value[1] + 1)})
def observe(self, handler, **kwargs):
self.slider.observe(handler, **kwargs)
my_widget = MyWidget()
but I haven't gotten anything to work. The error I get from the above is:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-62-3ea00d238837> in <module>
17 self.slider.observe(handler)
18
---> 19 my_widget = MyWidget()
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in __new__(cls, *args, **kwargs)
956 else:
957 inst = new_meth(cls, *args, **kwargs)
--> 958 inst.setup_instance(*args, **kwargs)
959 return inst
960
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
984 self._trait_notifiers = {}
985 self._trait_validators = {}
--> 986 super(HasTraits, self).setup_instance(*args, **kwargs)
987
988 def __init__(self, *args, **kwargs):
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
975 else:
976 if isinstance(value, BaseDescriptor):
--> 977 value.instance_init(self)
978
979
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in instance_init(self, inst)
922
923 def instance_init(self, inst):
--> 924 inst.observe(self, self.trait_names, type=self.type)
925
926
TypeError: observe() takes 2 positional arguments but 3 were given
Has anyone done something like this? Any tips on how I might get this to work as intended? I think pure python "widget composition" capability could be really useful for lots of users.
This is very similar to #700, in which @jasongrout suggested this would be possible in pure python. @pbugnion this seems up your alley as well as it is similar to #1543.
I know why, I'll tell you the solution first and then the reason why it doesn't work:
Solution:
Change the name of the observe method you defined in your class:
def my_observe(self, handler, **kwargs):
self.slider.observe(handler, **kwargs)
Reason:
If you print out the MRO you can see from which class you are inheriting. In fact you are inheriting from the HasTraits class of the traitlets module in the traitlets package. This is because you are inheriting from HBox which in turn inherits from HasTraits.
This HasTraits class that is you parent, already has an observe method which in turn is used by other private functions
When you define your own observe method you are overriding the observe method in the parent class and everything breaks. It would suffice to follow the traceback to have a clear understanding of when and how it broke.
Thanks for taking the time, @gioxc88. Some functions rely on the name of the method being observe, e.g. interactive_output. If I named my observe method my_observe, interactive_output would not work on my widget. Is there any way to keep the name observe?
I tried using the same function signature as HasTraits:
from ipywidgets import HBox, FloatRangeSlider, Button
from traitlets import All
class MyWidget(HBox):
def __init__(self):
self.slider = FloatRangeSlider()
self.button = Button(description='next')
super(MyWidget, self).__init__()
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
def move_up(self, change):
value = self.slider.value
self.slider.set_state({'value': (value[0] + 1, value[1] + 1)})
def observe(self, handler, names=All, type='change'):
return self.slider.observe(handler, names, type)
my_widget = MyWidget()
but then I get this puzzling error:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-10-1847da5944aa> in <module>
22 self.slider.observe(handler, **kwargs)
23
---> 24 my_widget = MyWidget()
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in __new__(cls, *args, **kwargs)
956 else:
957 inst = new_meth(cls, *args, **kwargs)
--> 958 inst.setup_instance(*args, **kwargs)
959 return inst
960
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
984 self._trait_notifiers = {}
985 self._trait_validators = {}
--> 986 super(HasTraits, self).setup_instance(*args, **kwargs)
987
988 def __init__(self, *args, **kwargs):
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
975 else:
976 if isinstance(value, BaseDescriptor):
--> 977 value.instance_init(self)
978
979
~/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in instance_init(self, inst)
922
923 def instance_init(self, inst):
--> 924 inst.observe(self, self.trait_names, type=self.type)
925
926
<ipython-input-10-1847da5944aa> in observe(self, handler, names, type)
20
21 def observe(self, handler, names=All, type='change'):
---> 22 self.slider.observe(handler, **kwargs)
23
24 my_widget = MyWidget()
AttributeError: 'MyWidget' object has no attribute 'slider'
the error is clear from the first line of the traceback :
/opt/anaconda3/lib/python3.7/site-packages/traitlets/traitlets.py in __new__(cls, *args, **kwargs)
this happens because __new__ is a class method and while you have self.slider which is an instance attribute.
This means that only instances of the MyWidget class have a slider attribute but not the class itself.
Try to do:
class MyWidget(HBox):
slider = FloatRangeSlider()
def __init__(self):
self.button = Button(description='next')
super(MyWidget, self).__init__()
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
def move_up(self, change):
value = self.slider.value
self.slider.set_state({'value': (value[0] + 1, value[1] + 1)})
def observe(self, handler, names=All, type='change'):
return self.slider.observe(handler, names, type)
my_widget = MyWidget()
this will work but still I am not sure want you want to achieve so I don't know if it will work for your purposes
@gioxc88 thanks again for the help. I tried implementing it this way, but when I try to display the resulting widget by running my_widget after, I get this message instead of the widget:
A Jupyter widget could not be displayed because the widget state could not be found. This could happen if the kernel storing the widget is no longer available, or if the widget state was not saved in the notebook. You may be able to create the widget by running the appropriate cells.
Also, with this new approach, if I create multiple instances of MyWidget, would they each have their own slider? I would want them to have distinct components.
yes I see now what you want to achieve but not sure how to go about it.
That being said I am trying to leverage this ipywidgets (which I find very useful and promising) to build a complex app. So I also want to know how to do what you are asking and if I find a solution and nobody answer before I will share it here
OK, sounds good. Thanks for the help anyway, you helped me understand the problem better.
What if you define a 'value' trait on your class, and internally link it to the slider value? You can do this by using multiple inheritance to inherit from ValueWidget. If you want it to work with interact, it will also need a description attribute, so I also inherit from DescriptionWidget below:
from ipywidgets import HBox, FloatRangeSlider, Button, ValueWidget, link, interact
from ipywidgets.widgets.widget_description import DescriptionWidget
class MyWidget(HBox, ValueWidget, DescriptionWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.slider = FloatRangeSlider()
self.button = Button(description='next')
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
link((self.slider, 'value'), (self, 'value'))
link((self.slider, 'description'), (self, 'description'))
def move_up(self, change):
value = self.slider.value
self.slider.set_state({'value': (value[0] + 1, value[1] + 1)})
my_widget = MyWidget()
my_widget.observe(lambda x: print(x))
my_widget
This may not be the cleanest way to do things, but have fun experimenting!
Thanks @jasongrout, this is perfect!
I really want to thank @gioxc88 for digging into this and exposing what the problem was.
yes, thanks to both of you.
Now that we have a value attribute, here's a version that's a bit cleaner:
from ipywidgets import HBox, FloatRangeSlider, Button, ValueWidget, link, interact
from ipywidgets.widgets.widget_description import DescriptionWidget
class MyWidget(HBox, ValueWidget, DescriptionWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.slider = FloatRangeSlider()
self.button = Button(description='next')
self.button.on_click(self.move_up)
self.children = [self.slider, self.button]
link((self.slider, 'value'), (self, 'value'))
link((self.slider, 'description'), (self, 'description'))
def move_up(self, change):
self.value = (self.value[0] + 1, self.value[1] + 1)
my_widget = MyWidget()
my_widget.observe(lambda x: print(x))
my_widget
Note also that the ipywidgets 8 slider will let you grab the bar between the two endpoints of a range slider and move both endpoints in lock-step.
@jasongrout ooooo that would be great! When can we expect that to be released?
Hopefully in the next few months
For posterity, I came up against a similar problem where I couldn't use links and I got it to work by using observers on the inner widgets to control the value of the main widget.
In this case, I also output the start and stop of a window, but my widgets are FloatSlider for the start of the window and BoundedFloatText for the duration of the window. It's a bit funky inside the observer method for the slider, but it works
from ipywidgets import Layout, HBox
class StartAndDurationController(HBox, ValueWidget, DescriptionWidget):
def __init__(self, tmax, tmin=0, start_value=None, duration=1., dtype='float', description='window (s)',
**kwargs):
self.tmin = tmin
self.tmax = tmax
self.start_value = start_value
self.dtype = dtype
self.slider = widgets.FloatSlider(
value=start_value,
min=tmin,
max=tmax,
step=0.01,
description=description,
continuous_update=False,
orientation='horizontal',
readout=True,
readout_format='.2f',
style={'description_width': 'initial'},
layout=Layout(width='100%'))
self.duration = widgets.BoundedFloatText(
value=duration,
min=0,
max=tmax - tmin,
step=0.1,
description='duration (s):',
style={'description_width': 'initial'},
layout=Layout(width='150px')
)
super(StartAndDurationController, self).__init__()
link((self.slider, 'description'), (self, 'description'))
self.value = (self.slider.value, self.slider.value + self.duration.value)
self.forward_button = widgets.Button(description='â–¶', layout=Layout(width='50px'))
self.forward_button.on_click(self.move_up)
self.backwards_button = widgets.Button(description='â—€', layout=Layout(width='50px'))
self.backwards_button.on_click(self.move_down)
self.children = [self.slider, self.duration, self.backwards_button, self.forward_button]
self.slider.observe(self.monitor_slider)
self.duration.observe(self.monitor_duration)
def monitor_slider(self, change):
if 'new' in change:
if isinstance(change['new'], dict):
if 'value' in change['new']:
value = change['new']['value']
else:
return
else:
value = change['new']
if value + self.duration.value > self.tmax:
self.slider.value = self.tmax - self.duration.value
else:
self.value = (value, value + self.duration.value)
def monitor_duration(self, change):
if 'new' in change:
if isinstance(change['new'], dict):
if 'value' in change['new']:
value = change['new']['value']
if self.slider.value + value > self.tmax:
self.slider.value = self.tmax - value
self.value = (self.slider.value, self.slider.value + value)
def move_up(self, change):
if self.slider.value + 2 * self.duration.value < self.tmax:
self.slider.value += self.duration.value
else:
self.slider.value = self.tmax - self.duration.value
def move_down(self, change):
if self.slider.value - self.duration.value > self.tmin:
self.slider.value -= self.duration.value
else:
self.slider.value = self.tmin
start_and_duration_controller = StartAndDurationController(10)
start_and_duration_controller.observe(lambda x: print(x['new']))
start_and_duration_controller