This commit is contained in:
Christian Mantha
2026-03-02 19:10:52 -05:00
commit 2ca0b9ef7c
28907 changed files with 5233713 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from .widget import Widget, CallbackDispatcher, register, widget_serialization
from .domwidget import DOMWidget
from .valuewidget import ValueWidget
from .trait_types import Color, Datetime, NumberFormat, TypedTuple
from .widget_core import CoreWidget
from .widget_bool import Checkbox, ToggleButton, Valid
from .widget_button import Button, ButtonStyle
from .widget_box import Box, HBox, VBox, GridBox
from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider, FloatLogSlider
from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider, Play, SliderStyle
from .widget_color import ColorPicker
from .widget_date import DatePicker
from .widget_datetime import DatetimePicker, NaiveDatetimePicker
from .widget_time import TimePicker
from .widget_output import Output
from .widget_selection import RadioButtons, ToggleButtons, ToggleButtonsStyle, Dropdown, Select, SelectionSlider, SelectMultiple, SelectionRangeSlider
from .widget_selectioncontainer import Tab, Accordion, Stack
from .widget_string import HTML, HTMLMath, Label, Text, Textarea, Password, Combobox
from .widget_controller import Controller
from .interaction import interact, interactive, fixed, interact_manual, interactive_output
from .widget_link import jslink, jsdlink
from .widget_layout import Layout
from .widget_media import Image, Video, Audio
from .widget_tagsinput import TagsInput, ColorsInput, FloatsInput, IntsInput
from .widget_style import Style
from .widget_templates import TwoByTwoLayout, AppLayout, GridspecLayout
from .widget_upload import FileUpload

View File

@@ -0,0 +1,15 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
def doc_subst(snippets):
""" Substitute format strings in class or function docstring """
def decorator(cls):
# Running python with -OO will discard docstrings (__doc__ is None).
if cls.__doc__ is not None:
# Strip the snippets to avoid trailing new lines and whitespace
stripped_snippets = {
key: snippet.strip() for (key, snippet) in snippets.items()
}
cls.__doc__ = cls.__doc__.format(**stripped_snippets)
return cls
return decorator

View File

@@ -0,0 +1,72 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Contains the DOMWidget class"""
from traitlets import Bool, Unicode
from .widget import Widget, widget_serialization
from .trait_types import InstanceDict, TypedTuple
from .widget_layout import Layout
from .widget_style import Style
class DOMWidget(Widget):
"""Widget that can be inserted into the DOM
Parameters
----------
tooltip: str
tooltip caption
layout: InstanceDict(Layout)
widget layout
"""
_model_name = Unicode('DOMWidgetModel').tag(sync=True)
_dom_classes = TypedTuple(trait=Unicode(), help="CSS classes applied to widget DOM element").tag(sync=True)
tabbable = Bool(help="Is widget tabbable?", allow_none=True, default_value=None).tag(sync=True)
tooltip = Unicode(None, allow_none=True, help="A tooltip caption.").tag(sync=True)
layout = InstanceDict(Layout).tag(sync=True, **widget_serialization)
def add_class(self, className):
"""
Adds a class to the top level element of the widget.
Doesn't add the class if it already exists.
"""
if className not in self._dom_classes:
self._dom_classes = list(self._dom_classes) + [className]
return self
def remove_class(self, className):
"""
Removes a class from the top level element of the widget.
Doesn't remove the class if it doesn't exist.
"""
if className in self._dom_classes:
self._dom_classes = [c for c in self._dom_classes if c != className]
return self
def focus(self):
"""
Focus on the widget.
"""
self.send({'do':'focus'})
def blur(self):
"""
Blur the widget.
"""
self.send({'do':'blur'})
def _repr_keys(self):
for key in super()._repr_keys():
# Exclude layout if it had the default value
if key == 'layout':
value = getattr(self, key)
if repr(value) == '%s()' % value.__class__.__name__:
continue
yield key
# We also need to include _dom_classes in repr for reproducibility
if self._dom_classes:
yield '_dom_classes'

View File

@@ -0,0 +1,584 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Interact with functions using widgets."""
from collections.abc import Iterable, Mapping
from enum import EnumMeta as EnumType
from inspect import signature, Parameter
from inspect import getcallargs
from inspect import getfullargspec as check_argspec
import sys
from IPython import get_ipython
from . import (Widget, ValueWidget, Text,
FloatSlider, FloatText, IntSlider, IntText, Checkbox,
Dropdown, VBox, Button, DOMWidget, Output)
from IPython.display import display, clear_output
from traitlets import HasTraits, Any, Unicode, observe
from numbers import Real, Integral
from warnings import warn
empty = Parameter.empty
def show_inline_matplotlib_plots():
"""Show matplotlib plots immediately if using the inline backend.
With ipywidgets 6.0, matplotlib plots don't work well with interact when
using the inline backend that comes with ipykernel. Basically, the inline
backend only shows the plot after the entire cell executes, which does not
play well with drawing plots inside of an interact function. See
https://github.com/jupyter-widgets/ipywidgets/issues/1181/ and
https://github.com/ipython/ipython/issues/10376 for more details. This
function displays any matplotlib plots if the backend is the inline backend.
"""
if 'matplotlib' not in sys.modules:
# matplotlib hasn't been imported, nothing to do.
return
try:
import matplotlib as mpl
from matplotlib_inline.backend_inline import flush_figures
except ImportError:
return
if (mpl.get_backend() == 'module://ipykernel.pylab.backend_inline' or
mpl.get_backend() == 'module://matplotlib_inline.backend_inline'):
flush_figures()
def interactive_output(f, controls):
"""Connect widget controls to a function.
This function does not generate a user interface for the widgets (unlike `interact`).
This enables customisation of the widget user interface layout.
The user interface layout must be defined and displayed manually.
"""
out = Output()
def observer(change):
kwargs = {k:v.value for k,v in controls.items()}
show_inline_matplotlib_plots()
with out:
clear_output(wait=True)
f(**kwargs)
show_inline_matplotlib_plots()
for k,w in controls.items():
w.observe(observer, 'value')
show_inline_matplotlib_plots()
observer(None)
return out
def _matches(o, pattern):
"""Match a pattern of types in a sequence."""
if not len(o) == len(pattern):
return False
comps = zip(o,pattern)
return all(isinstance(obj,kind) for obj,kind in comps)
def _get_min_max_value(min, max, value=None, step=None):
"""Return min, max, value given input values with possible None."""
# Either min and max need to be given, or value needs to be given
if value is None:
if min is None or max is None:
raise ValueError('unable to infer range, value from: ({}, {}, {})'.format(min, max, value))
diff = max - min
value = min + (diff / 2)
# Ensure that value has the same type as diff
if not isinstance(value, type(diff)):
value = min + (diff // 2)
else: # value is not None
if not isinstance(value, Real):
raise TypeError('expected a real number, got: %r' % value)
# Infer min/max from value
if value == 0:
# This gives (0, 1) of the correct type
vrange = (value, value + 1)
elif value > 0:
vrange = (-value, 3*value)
else:
vrange = (3*value, -value)
if min is None:
min = vrange[0]
if max is None:
max = vrange[1]
if step is not None:
# ensure value is on a step
tick = int((value - min) / step)
value = min + tick * step
if not min <= value <= max:
raise ValueError('value must be between min and max (min={}, value={}, max={})'.format(min, value, max))
return min, max, value
def _yield_abbreviations_for_parameter(param, kwargs):
"""Get an abbreviation for a function parameter."""
name = param.name
kind = param.kind
default = param.default
not_found = (name, empty, empty)
if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
if name in kwargs:
value = kwargs.pop(name)
elif default is not empty:
value = default
elif param.annotation:
value = param.annotation
else:
yield not_found
yield (name, value, default)
elif kind == Parameter.VAR_KEYWORD:
# In this case name=kwargs and we yield the items in kwargs with their keys.
for k, v in kwargs.copy().items():
kwargs.pop(k)
yield k, v, empty
class interactive(VBox):
"""
A VBox container containing a group of interactive widgets tied to a
function.
Parameters
----------
__interact_f : function
The function to which the interactive widgets are tied. The `**kwargs`
should match the function signature.
__options : dict
A dict of options. Currently, the only supported keys are
``"manual"`` (defaults to ``False``), ``"manual_name"`` (defaults
to ``"Run Interact"``) and ``"auto_display"`` (defaults to ``False``).
**kwargs : various, optional
An interactive widget is created for each keyword argument that is a
valid widget abbreviation.
Note that the first two parameters intentionally start with a double
underscore to avoid being mixed up with keyword arguments passed by
``**kwargs``.
"""
def __init__(self, __interact_f, __options={}, **kwargs):
VBox.__init__(self, _dom_classes=['widget-interact'])
self.result = None
self.args = []
self.kwargs = {}
self.f = f = __interact_f
self.clear_output = kwargs.pop('clear_output', True)
self.manual = __options.get("manual", False)
self.manual_name = __options.get("manual_name", "Run Interact")
self.auto_display = __options.get("auto_display", False)
new_kwargs = self.find_abbreviations(kwargs)
# Before we proceed, let's make sure that the user has passed a set of args+kwargs
# that will lead to a valid call of the function. This protects against unspecified
# and doubly-specified arguments.
try:
check_argspec(f)
except TypeError:
# if we can't inspect, we can't validate
pass
else:
getcallargs(f, **{n:v for n,v,_ in new_kwargs})
# Now build the widgets from the abbreviations.
self.kwargs_widgets = self.widgets_from_abbreviations(new_kwargs)
# This has to be done as an assignment, not using self.children.append,
# so that traitlets notices the update. We skip any objects (such as fixed) that
# are not DOMWidgets.
c = [w for w in self.kwargs_widgets if isinstance(w, DOMWidget)]
# If we are only to run the function on demand, add a button to request this.
if self.manual:
self.manual_button = Button(description=self.manual_name)
c.append(self.manual_button)
self.out = Output()
c.append(self.out)
self.children = c
# Wire up the widgets
# If we are doing manual running, the callback is only triggered by the button
# Otherwise, it is triggered for every trait change received
# On-demand running also suppresses running the function with the initial parameters
if self.manual:
self.manual_button.on_click(self.update)
# Also register input handlers on text areas, so the user can hit return to
# invoke execution.
for w in self.kwargs_widgets:
if isinstance(w, Text):
w.continuous_update = False
w.observe(self.update, names='value')
else:
for widget in self.kwargs_widgets:
widget.observe(self.update, names='value')
self.update()
# Callback function
def update(self, *args):
"""
Call the interact function and update the output widget with
the result of the function call.
Parameters
----------
*args : ignored
Required for this method to be used as traitlets callback.
"""
self.kwargs = {}
if self.manual:
self.manual_button.disabled = True
try:
show_inline_matplotlib_plots()
with self.out:
if self.clear_output:
clear_output(wait=True)
for widget in self.kwargs_widgets:
value = widget.get_interact_value()
self.kwargs[widget._kwarg] = value
self.result = self.f(**self.kwargs)
show_inline_matplotlib_plots()
if self.auto_display and self.result is not None:
display(self.result)
except Exception as e:
ip = get_ipython()
if ip is None:
self.log.warning("Exception in interact callback: %s", e, exc_info=True)
else:
ip.showtraceback()
finally:
if self.manual:
self.manual_button.disabled = False
# Find abbreviations
def signature(self):
return signature(self.f)
def find_abbreviations(self, kwargs):
"""Find the abbreviations for the given function and kwargs.
Return (name, abbrev, default) tuples.
"""
new_kwargs = []
try:
sig = self.signature()
except (ValueError, TypeError):
# can't inspect, no info from function; only use kwargs
return [ (key, value, value) for key, value in kwargs.items() ]
for param in sig.parameters.values():
for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
if value is empty:
raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
new_kwargs.append((name, value, default))
return new_kwargs
# Abbreviations to widgets
def widgets_from_abbreviations(self, seq):
"""Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
result = []
for name, abbrev, default in seq:
if isinstance(abbrev, Widget) and (not isinstance(abbrev, ValueWidget)):
raise TypeError("{!r} is not a ValueWidget".format(abbrev))
widget = self.widget_from_abbrev(abbrev, default)
if widget is None:
raise ValueError("{!r} cannot be transformed to a widget".format(abbrev))
if not hasattr(widget, "description") or not widget.description:
widget.description = name
widget._kwarg = name
result.append(widget)
return result
@classmethod
def widget_from_abbrev(cls, abbrev, default=empty):
"""Build a ValueWidget instance given an abbreviation or Widget."""
if isinstance(abbrev, ValueWidget) or isinstance(abbrev, fixed):
return abbrev
if isinstance(abbrev, tuple):
widget = cls.widget_from_tuple(abbrev)
if default is not empty:
try:
widget.value = default
except Exception:
# ignore failure to set default
pass
return widget
# Try type annotation
if isinstance(abbrev, type):
widget = cls.widget_from_annotation(abbrev)
if widget is not None:
return widget
# Try single value
widget = cls.widget_from_single_value(abbrev)
if widget is not None:
return widget
# Something iterable (list, dict, generator, ...). Note that str and
# tuple should be handled before, that is why we check this case last.
if isinstance(abbrev, Iterable):
widget = cls.widget_from_iterable(abbrev)
if default is not empty:
try:
widget.value = default
except Exception:
# ignore failure to set default
pass
return widget
# No idea...
return None
@staticmethod
def widget_from_single_value(o):
"""Make widgets from single values, which can be used as parameter defaults."""
if isinstance(o, str):
return Text(value=str(o))
elif isinstance(o, bool):
return Checkbox(value=o)
elif isinstance(o, Integral):
min, max, value = _get_min_max_value(None, None, o)
return IntSlider(value=o, min=min, max=max)
elif isinstance(o, Real):
min, max, value = _get_min_max_value(None, None, o)
return FloatSlider(value=o, min=min, max=max)
else:
return None
@staticmethod
def widget_from_annotation(t):
"""Make widgets from type annotation and optional default value."""
if t is str:
return Text()
elif t is bool:
return Checkbox()
elif t in {int, Integral}:
return IntText()
elif t in {float, Real}:
return FloatText()
elif isinstance(t, EnumType):
return Dropdown(options={option.name: option for option in t})
else:
return None
@staticmethod
def widget_from_tuple(o):
"""Make widgets from a tuple abbreviation."""
if _matches(o, (Real, Real)):
min, max, value = _get_min_max_value(o[0], o[1])
if all(isinstance(_, Integral) for _ in o):
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, min=min, max=max)
elif _matches(o, (Real, Real, Real)):
step = o[2]
if step <= 0:
raise ValueError("step must be >= 0, not %r" % step)
min, max, value = _get_min_max_value(o[0], o[1], step=step)
if all(isinstance(_, Integral) for _ in o):
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, min=min, max=max, step=step)
@staticmethod
def widget_from_iterable(o):
"""Make widgets from an iterable. This should not be done for
a string or tuple."""
# Dropdown expects a dict or list, so we convert an arbitrary
# iterable to either of those.
if isinstance(o, (list, dict)):
return Dropdown(options=o)
elif isinstance(o, Mapping):
return Dropdown(options=list(o.items()))
else:
return Dropdown(options=list(o))
# Return a factory for interactive functions
@classmethod
def factory(cls):
options = dict(manual=False, auto_display=True, manual_name="Run Interact")
return _InteractFactory(cls, options)
class _InteractFactory:
"""
Factory for instances of :class:`interactive`.
This class is needed to support options like::
>>> @interact.options(manual=True)
... def greeting(text="World"):
... print("Hello {}".format(text))
Parameters
----------
cls : class
The subclass of :class:`interactive` to construct.
options : dict
A dict of options used to construct the interactive
function. By default, this is returned by
``cls.default_options()``.
kwargs : dict
A dict of **kwargs to use for widgets.
"""
def __init__(self, cls, options, kwargs={}):
self.cls = cls
self.opts = options
self.kwargs = kwargs
def widget(self, f):
"""
Return an interactive function widget for the given function.
The widget is only constructed, not displayed nor attached to
the function.
Returns
-------
An instance of ``self.cls`` (typically :class:`interactive`).
Parameters
----------
f : function
The function to which the interactive widgets are tied.
"""
return self.cls(f, self.opts, **self.kwargs)
def __call__(self, __interact_f=None, **kwargs):
"""
Make the given function interactive by adding and displaying
the corresponding :class:`interactive` widget.
Expects the first argument to be a function. Parameters to this
function are widget abbreviations passed in as keyword arguments
(``**kwargs``). Can be used as a decorator (see examples).
Returns
-------
f : __interact_f with interactive widget attached to it.
Parameters
----------
__interact_f : function
The function to which the interactive widgets are tied. The `**kwargs`
should match the function signature. Passed to :func:`interactive()`
**kwargs : various, optional
An interactive widget is created for each keyword argument that is a
valid widget abbreviation. Passed to :func:`interactive()`
Examples
--------
Render an interactive text field that shows the greeting with the passed in
text::
# 1. Using interact as a function
def greeting(text="World"):
print("Hello {}".format(text))
interact(greeting, text="Jupyter Widgets")
# 2. Using interact as a decorator
@interact
def greeting(text="World"):
print("Hello {}".format(text))
# 3. Using interact as a decorator with named parameters
@interact(text="Jupyter Widgets")
def greeting(text="World"):
print("Hello {}".format(text))
Render an interactive slider widget and prints square of number::
# 1. Using interact as a function
def square(num=1):
print("{} squared is {}".format(num, num*num))
interact(square, num=5)
# 2. Using interact as a decorator
@interact
def square(num=2):
print("{} squared is {}".format(num, num*num))
# 3. Using interact as a decorator with named parameters
@interact(num=5)
def square(num=2):
print("{} squared is {}".format(num, num*num))
"""
# If kwargs are given, replace self by a new
# _InteractFactory with the updated kwargs
if kwargs:
kw = dict(self.kwargs)
kw.update(kwargs)
self = type(self)(self.cls, self.opts, kw)
f = __interact_f
if f is None:
# This branch handles the case 3
# @interact(a=30, b=40)
# def f(*args, **kwargs):
# ...
#
# Simply return the new factory
return self
# positional arg support in: https://gist.github.com/8851331
# Handle the cases 1 and 2
# 1. interact(f, **kwargs)
# 2. @interact
# def f(*args, **kwargs):
# ...
w = self.widget(f)
try:
f.widget = w
except AttributeError:
# some things (instancemethods) can't have attributes attached,
# so wrap in a lambda
f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
f.widget = w
show_inline_matplotlib_plots()
display(w)
return f
def options(self, **kwds):
"""
Change options for interactive functions.
Returns
-------
A new :class:`_InteractFactory` which will apply the
options when called.
"""
opts = dict(self.opts)
for k in kwds:
try:
# Ensure that the key exists because we want to change
# existing options, not add new ones.
_ = opts[k]
except KeyError:
raise ValueError("invalid option {!r}".format(k))
opts[k] = kwds[k]
return type(self)(self.cls, opts, self.kwargs)
interact = interactive.factory()
interact_manual = interact.options(manual=True, manual_name="Run Interact")
class fixed(HasTraits):
"""A pseudo-widget whose value is fixed and never synced to the client."""
value = Any(help="Any Python object")
description = Unicode('', help="Any Python object")
def __init__(self, value, **kwargs):
super().__init__(value=value, **kwargs)
def get_interact_value(self):
"""Return the value for this widget which should be passed to
interactive functions. Custom widgets can change this method
to process the raw value ``self.value``.
"""
return self.value

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,90 @@
# coding: utf-8
# Copyright (c) Vidar Tonaas Fauske.
# Distributed under the terms of the Modified BSD License.
import pytest
import datetime
import pytz
from traitlets import TraitError
from ..trait_types import (
time_to_json,
time_from_json,
datetime_to_json,
datetime_from_json,
)
def test_time_serialize_none():
assert time_to_json(None, None) == None
def test_time_serialize_value():
t = datetime.time(13, 37, 42, 7000)
assert time_to_json(t, None) == dict(
hours=13, minutes=37, seconds=42, milliseconds=7
)
def test_time_deserialize_none():
assert time_from_json(None, None) == None
def test_time_deserialize_value():
v = dict(hours=13, minutes=37, seconds=42, milliseconds=7)
assert time_from_json(v, None) == datetime.time(13, 37, 42, 7000)
def test_datetime_serialize_none():
assert datetime_to_json(None, None) == None
def test_datetime_serialize_value():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7000, pytz.utc)
assert datetime_to_json(t, None) == dict(
year=2002,
month=1, # Months are 0-based indices in JS
date=20,
hours=13,
minutes=37,
seconds=42,
milliseconds=7,
)
def test_datetime_serialize_non_utz():
# Non-existent timezone, so it will never be the local one:
tz = pytz.FixedOffset(42)
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7000, tz)
assert datetime_to_json(t, None) == dict(
year=2002,
month=1, # Months are 0-based indices in JS
date=20,
hours=12,
minutes=55,
seconds=42,
milliseconds=7,
)
def test_datetime_deserialize_none():
assert datetime_from_json(None, None) == None
def test_datetime_deserialize_value():
tz = pytz.FixedOffset(42)
v = dict(
year=2002,
month=1, # Months are 0-based indices in JS
date=20,
hours=13,
minutes=37,
seconds=42,
milliseconds=7,
)
assert datetime_from_json(v, None) == datetime.datetime(
2002, 2, 20, 14, 19, 42, 7000, tz
)

View File

@@ -0,0 +1,27 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from unittest import TestCase
from ipywidgets.widgets.docutils import doc_subst
class TestDocSubst(TestCase):
def test_substitution(self):
snippets = {'key': '62'}
@doc_subst(snippets)
def f():
""" Docstring with value {key} """
assert "Docstring with value 62" in f.__doc__
def test_unused_keys(self):
snippets = {'key': '62', 'other-key': 'unused'}
@doc_subst(snippets)
def f():
""" Docstring with value {key} """
assert "Docstring with value 62" in f.__doc__

View File

@@ -0,0 +1,660 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Test interact and interactive."""
from unittest.mock import patch
import os
from enum import Enum
from collections import OrderedDict
import pytest
import ipywidgets as widgets
from traitlets import TraitError, Float
from ipywidgets import (interact, interact_manual, interactive,
interaction, Output, Widget)
#-----------------------------------------------------------------------------
# Utility stuff
#-----------------------------------------------------------------------------
def f(**kwargs):
pass
class Color(Enum):
red = 0
green = 1
blue = 2
def g(a: str, b: bool, c: int, d: float, e: Color) -> None:
pass
displayed = []
@pytest.fixture()
def clear_display():
global displayed
displayed = []
def record_display(*args):
displayed.extend(args)
#-----------------------------------------------------------------------------
# Actual tests
#-----------------------------------------------------------------------------
def check_widget(w, **d):
"""Check a single widget against a dict"""
for attr, expected in d.items():
if attr == 'cls':
assert w.__class__ is expected
else:
value = getattr(w, attr)
assert value == expected, "{}.{} = {!r} != {!r}".format(w.__class__.__name__, attr, value, expected)
# For numeric values, the types should match too
if isinstance(value, (int, float)):
tv = type(value)
te = type(expected)
assert tv is te, "type({}.{}) = {!r} != {!r}".format(w.__class__.__name__, attr, tv, te)
def check_widget_children(container, **to_check):
"""Check that widgets are created as expected"""
# build a widget dictionary, so it matches
widgets = {}
for w in container.children:
if not isinstance(w, Output):
widgets[w.description] = w
for key, d in to_check.items():
assert key in widgets
check_widget(widgets[key], **d)
def test_single_value_string():
a = 'hello'
c = interactive(f, a=a)
w = c.children[0]
check_widget(w,
cls=widgets.Text,
description='a',
value=a,
)
def test_single_value_bool():
for a in (True, False):
c = interactive(f, a=a)
w = c.children[0]
check_widget(w,
cls=widgets.Checkbox,
description='a',
value=a,
)
def test_single_value_float():
for a in (2.25, 1.0, -3.5, 0.0):
if not a:
expected_min = 0.0
expected_max = 1.0
elif a > 0:
expected_min = -a
expected_max = 3*a
else:
expected_min = 3*a
expected_max = -a
c = interactive(f, a=a)
w = c.children[0]
check_widget(w,
cls=widgets.FloatSlider,
description='a',
value=a,
min=expected_min,
max=expected_max,
step=0.1,
readout=True,
)
def test_single_value_int():
for a in (1, 5, -3, 0):
if not a:
expected_min = 0
expected_max = 1
elif a > 0:
expected_min = -a
expected_max = 3*a
else:
expected_min = 3*a
expected_max = -a
c = interactive(f, a=a)
assert len(c.children) == 2
w = c.children[0]
check_widget(w,
cls=widgets.IntSlider,
description='a',
value=a,
min=expected_min,
max=expected_max,
step=1,
readout=True,
)
def test_list_str():
values = ['hello', 'there', 'guy']
first = values[0]
c = interactive(f, lis=values)
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=tuple(values),
_options_labels=tuple(values),
_options_values=tuple(values),
)
check_widget_children(c, lis=d)
def test_list_int():
values = [3, 1, 2]
first = values[0]
c = interactive(f, lis=values)
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=tuple(values),
_options_labels=tuple(str(v) for v in values),
_options_values=tuple(values),
)
check_widget_children(c, lis=d)
def test_list_tuple():
values = [(3, 300), (1, 100), (2, 200)]
first = values[0][1]
c = interactive(f, lis=values)
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=tuple(values),
_options_labels=("3", "1", "2"),
_options_values=(300, 100, 200),
)
check_widget_children(c, lis=d)
def test_list_tuple_invalid():
for bad in [
(),
]:
with pytest.raises(ValueError):
print(bad) # because there is no custom message in assert_raises
c = interactive(f, tup=bad)
def test_dict():
for d in [
dict(a=5),
dict(a=5, b='b', c=dict),
]:
c = interactive(f, d=d)
w = c.children[0]
check = dict(
cls=widgets.Dropdown,
description='d',
value=next(iter(d.values())),
options=d,
_options_labels=tuple(d.keys()),
_options_values=tuple(d.values()),
)
check_widget(w, **check)
def test_ordereddict():
from collections import OrderedDict
items = [(3, 300), (1, 100), (2, 200)]
first = items[0][1]
values = OrderedDict(items)
c = interactive(f, lis=values)
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=values,
_options_labels=("3", "1", "2"),
_options_values=(300, 100, 200),
)
check_widget_children(c, lis=d)
def test_iterable():
def yield_values():
yield 3
yield 1
yield 2
first = next(yield_values())
c = interactive(f, lis=yield_values())
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=(3, 1, 2),
_options_labels=("3", "1", "2"),
_options_values=(3, 1, 2),
)
check_widget_children(c, lis=d)
def test_iterable_tuple():
values = [(3, 300), (1, 100), (2, 200)]
first = values[0][1]
c = interactive(f, lis=iter(values))
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=tuple(values),
_options_labels=("3", "1", "2"),
_options_values=(300, 100, 200),
)
check_widget_children(c, lis=d)
def test_mapping():
from collections.abc import Mapping
from collections import OrderedDict
class TestMapping(Mapping):
def __init__(self, values):
self.values = values
def __getitem__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
def __iter__(self):
raise NotImplementedError
def items(self):
return self.values
items = [(3, 300), (1, 100), (2, 200)]
first = items[0][1]
values = TestMapping(items)
c = interactive(f, lis=values)
assert len(c.children) == 2
d = dict(
cls=widgets.Dropdown,
value=first,
options=tuple(items),
_options_labels=("3", "1", "2"),
_options_values=(300, 100, 200),
)
check_widget_children(c, lis=d)
def test_decorator_kwarg(clear_display):
with patch.object(interaction, 'display', record_display):
@interact(a=5)
def foo(a):
pass
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.IntSlider,
value=5,
)
def test_interact_instancemethod(clear_display):
class Foo:
def show(self, x):
print(x)
f = Foo()
with patch.object(interaction, 'display', record_display):
g = interact(f.show, x=(1,10))
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.IntSlider,
value=5,
)
def test_decorator_no_call(clear_display):
with patch.object(interaction, 'display', record_display):
@interact
def foo(a='default'):
pass
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.Text,
value='default',
)
def test_call_interact(clear_display):
def foo(a='default'):
pass
with patch.object(interaction, 'display', record_display):
ifoo = interact(foo)
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.Text,
value='default',
)
def test_call_interact_on_trait_changed_none_return(clear_display):
def foo(a='default'):
pass
with patch.object(interaction, 'display', record_display):
ifoo = interact(foo)
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.Text,
value='default',
)
with patch.object(interaction, 'display', record_display):
w.value = 'called'
assert len(displayed) == 1
def test_call_interact_kwargs(clear_display):
def foo(a='default'):
pass
with patch.object(interaction, 'display', record_display):
ifoo = interact(foo, a=10)
assert len(displayed) == 1
w = displayed[0].children[0]
check_widget(w,
cls=widgets.IntSlider,
value=10,
)
def test_call_decorated_on_trait_change(clear_display):
"""test calling @interact decorated functions"""
d = {}
with patch.object(interaction, 'display', record_display):
@interact
def foo(a='default'):
d['a'] = a
return a
assert len(displayed) == 2 # display the result and the interact
w = displayed[1].children[0]
check_widget(w,
cls=widgets.Text,
value='default',
)
# test calling the function directly
a = foo('hello')
assert a == 'hello'
assert d['a'] == 'hello'
# test that setting trait values calls the function
with patch.object(interaction, 'display', record_display):
w.value = 'called'
assert d['a'] == 'called'
assert len(displayed) == 3
assert w.value == displayed[-1]
def test_call_decorated_kwargs_on_trait_change(clear_display):
"""test calling @interact(foo=bar) decorated functions"""
d = {}
with patch.object(interaction, 'display', record_display):
@interact(a='kwarg')
def foo(a='default'):
d['a'] = a
return a
assert len(displayed) == 2 # display the result and the interact
w = displayed[1].children[0]
check_widget(w,
cls=widgets.Text,
value='kwarg',
)
# test calling the function directly
a = foo('hello')
assert a == 'hello'
assert d['a'] == 'hello'
# test that setting trait values calls the function
with patch.object(interaction, 'display', record_display):
w.value = 'called'
assert d['a'] == 'called'
assert len(displayed) == 3
assert w.value == displayed[-1]
def test_fixed():
c = interactive(f, a=widgets.fixed(5), b='text')
assert len(c.children) == 2
w = c.children[0]
check_widget(w,
cls=widgets.Text,
value='text',
description='b',
)
def test_default_description():
c = interactive(f, b='text')
w = c.children[0]
check_widget(w,
cls=widgets.Text,
value='text',
description='b',
)
def test_custom_description():
d = {}
def record_kwargs(**kwargs):
d.clear()
d.update(kwargs)
c = interactive(record_kwargs, b=widgets.Text(value='text', description='foo'))
w = c.children[0]
check_widget(w,
cls=widgets.Text,
value='text',
description='foo',
)
w.value = 'different text'
assert d == {'b': 'different text'}
def test_raises_on_non_value_widget():
""" Test that passing in a non-value widget raises an error """
class BadWidget(Widget):
""" A widget that contains a `value` traitlet """
value = Float()
with pytest.raises(TypeError, match=".* not a ValueWidget.*"):
interactive(f, b=BadWidget())
def test_interact_manual_button():
c = interact.options(manual=True).widget(f)
w = c.children[0]
check_widget(w, cls=widgets.Button)
def test_interact_manual_nocall():
callcount = 0
def calltest(testarg):
callcount += 1
c = interact.options(manual=True)(calltest, testarg=5).widget
c.children[0].value = 10
assert callcount == 0
def test_interact_call():
w = interact.widget(f)
w.update()
w = interact_manual.widget(f)
w.update()
def test_interact_options():
def f(x):
return x
w = interact.options(manual=False).options(manual=True)(f, x=21).widget
assert w.manual == True
w = interact_manual.options(manual=False).options()(x=21).widget(f)
assert w.manual == False
w = interact(x=21)().options(manual=True)(f).widget
assert w.manual == True
def test_interact_options_bad():
with pytest.raises(ValueError):
interact.options(bad="foo")
def test_int_range_logic():
irsw = widgets.IntRangeSlider
w = irsw(value=(2, 4), min=0, max=6)
check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
w.upper = 3
w.max = 3
check_widget(w, cls=irsw, value=(2, 3), min=0, max=3)
w.min = 0
w.max = 6
w.lower = 2
w.upper = 4
check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
w.value = (0, 1) #lower non-overlapping range
check_widget(w, cls=irsw, value=(0, 1), min=0, max=6)
w.value = (5, 6) #upper non-overlapping range
check_widget(w, cls=irsw, value=(5, 6), min=0, max=6)
w.lower = 2
check_widget(w, cls=irsw, value=(2, 6), min=0, max=6)
with pytest.raises(TraitError):
w.min = 7
with pytest.raises(TraitError):
w.max = -1
w = irsw(min=2, max=3, value=(2, 3))
check_widget(w, min=2, max=3, value=(2, 3))
w = irsw(min=100, max=200, value=(125, 175))
check_widget(w, value=(125, 175))
with pytest.raises(TraitError):
irsw(min=2, max=1)
def test_float_range_logic():
frsw = widgets.FloatRangeSlider
w = frsw(value=(.2, .4), min=0., max=.6)
check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
w.min = 0.
w.max = .6
w.lower = .2
w.upper = .4
check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
w.value = (0., .1) #lower non-overlapping range
check_widget(w, cls=frsw, value=(0., .1), min=0., max=.6)
w.value = (.5, .6) #upper non-overlapping range
check_widget(w, cls=frsw, value=(.5, .6), min=0., max=.6)
w.lower = .2
check_widget(w, cls=frsw, value=(.2, .6), min=0., max=.6)
with pytest.raises(TraitError):
w.min = .7
with pytest.raises(TraitError):
w.max = -.1
w = frsw(min=2, max=3, value=(2.2, 2.5))
check_widget(w, min=2., max=3.)
with pytest.raises(TraitError):
frsw(min=.2, max=.1)
def test_multiple_selection():
smw = widgets.SelectMultiple
# degenerate multiple select
w = smw()
check_widget(w, value=tuple())
# don't accept random other value when no options
with pytest.raises(TraitError):
w.value = (2,)
check_widget(w, value=tuple())
# basic multiple select
w = smw(options=[(1, 1)], value=[1])
check_widget(w, cls=smw, value=(1,), options=((1, 1),))
# don't accept random other value
with pytest.raises(TraitError):
w.value = w.value + (2,)
check_widget(w, value=(1,))
# change options, which resets value
w.options = w.options + ((2, 2),)
check_widget(w, options=((1, 1), (2,2)), value=())
# change value
w.value = (1,2)
check_widget(w, value=(1, 2))
# dict style
w.options = {1: 1}
check_widget(w, options={1:1})
# updating
w.options = (1,)
with pytest.raises(TraitError):
w.value = (2,)
check_widget(w, options=(1,) )
def test_interact_noinspect():
a = 'hello'
c = interactive(dict, a=a)
w = c.children[0]
check_widget(w,
cls=widgets.Text,
description='a',
value=a,
)
def test_get_interact_value():
from ipywidgets.widgets import ValueWidget
from traitlets import Unicode
class TheAnswer(ValueWidget):
_model_name = Unicode('TheAnswer')
description = Unicode()
def get_interact_value(self):
return 42
w = TheAnswer()
c = interactive(lambda v: v, v=w)
c.update()
assert c.result == 42
def test_state_schema():
from ipywidgets.widgets import IntSlider, Widget
import json
import jsonschema
s = IntSlider()
state = Widget.get_manager_state(drop_defaults=True)
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../', 'state.schema.json')) as f:
schema = json.load(f)
jsonschema.validate(state, schema)
def test_type_hints():
c = interactive(g)
assert len(c.children) == 6
check_widget_children(
c,
a={'cls': widgets.Text},
b={'cls': widgets.Checkbox},
c={'cls': widgets.IntText},
d={'cls': widgets.FloatText},
e={
'cls': widgets.Dropdown,
'options': {
'red': Color.red,
'green': Color.green,
'blue': Color.blue,
},
'_options_labels': ("red", "green", "blue"),
'_options_values': (Color.red, Color.green, Color.blue),
},
)

View File

@@ -0,0 +1,38 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import pytest
from .. import jslink, jsdlink, ToggleButton
def test_jslink_args():
with pytest.raises(TypeError):
jslink()
w1 = ToggleButton()
with pytest.raises(TypeError):
jslink((w1, 'value'))
w2 = ToggleButton()
jslink((w1, 'value'), (w2, 'value'))
with pytest.raises(TypeError):
jslink((w1, 'value'), (w2, 'nosuchtrait'))
with pytest.raises(TypeError):
jslink((w1, 'value'), (w2, 'traits'))
def test_jsdlink_args():
with pytest.raises(TypeError):
jsdlink()
w1 = ToggleButton()
with pytest.raises(TypeError):
jsdlink((w1, 'value'))
w2 = ToggleButton()
jsdlink((w1, 'value'), (w2, 'value'))
with pytest.raises(TypeError):
jsdlink((w1, 'value'), (w2, 'nosuchtrait'))
with pytest.raises(TypeError):
jsdlink((w1, 'value'), (w2, 'traits'))

View File

@@ -0,0 +1,168 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from unittest import TestCase
from traitlets import TraitError
from ipywidgets.widgets import Accordion, Tab, Stack, HTML
class TestTab(TestCase):
def setUp(self):
self.children = [HTML('0'), HTML('1')]
self.widget = Tab
def test_selected_index_none(self):
widget = self.widget(self.children, selected_index=None)
state = widget.get_state()
assert state['selected_index'] == 0
def test_selected_index_default(self):
widget = self.widget(self.children)
state = widget.get_state()
assert state['selected_index'] == 0
def test_selected_index(self):
widget = self.widget(self.children, selected_index=1)
state = widget.get_state()
assert state['selected_index'] == 1
def test_selected_index_out_of_bounds(self):
with self.assertRaises(TraitError):
self.widget(self.children, selected_index=-1)
def test_children_position_argument(self):
self.widget(self.children)
def test_titles(self):
widget = self.widget(self.children, selected_index=None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles == ('', '')
widget.set_title(1, 'Title 1')
assert widget.get_state()['titles'] == ('', 'Title 1')
assert widget.titles[1] == 'Title 1'
assert widget.get_title(1) == 'Title 1'
# Backwards compatible with 7.x api
widget.set_title(1, None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles[1] == ''
assert widget.get_title(1) == ''
with self.assertRaises(IndexError):
widget.set_title(2, 'out of bounds')
with self.assertRaises(IndexError):
widget.get_title(2)
widget.children = tuple(widget.children[:1])
assert len(widget.children) == 1
assert widget.titles == ('',)
class TestAccordion(TestCase):
def setUp(self):
self.children = [HTML('0'), HTML('1')]
self.widget = Accordion
def test_selected_index_none(self):
widget = self.widget(self.children, selected_index=None)
state = widget.get_state()
assert state['selected_index'] is None
def test_selected_index_default(self):
widget = self.widget(self.children)
state = widget.get_state()
assert state['selected_index'] is None
def test_selected_index(self):
widget = self.widget(self.children, selected_index=1)
state = widget.get_state()
assert state['selected_index'] == 1
def test_selected_index_out_of_bounds(self):
with self.assertRaises(TraitError):
self.widget(self.children, selected_index=-1)
def test_children_position_argument(self):
self.widget(self.children)
def test_titles(self):
widget = self.widget(self.children, selected_index=None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles == ('', '')
widget.set_title(1, 'Title 1')
assert widget.get_state()['titles'] == ('', 'Title 1')
assert widget.titles[1] == 'Title 1'
assert widget.get_title(1) == 'Title 1'
# Backwards compatible with 7.x api
widget.set_title(1, None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles[1] == ''
assert widget.get_title(1) == ''
with self.assertRaises(IndexError):
widget.set_title(2, 'out of bounds')
with self.assertRaises(IndexError):
widget.get_title(2)
widget.children = tuple(widget.children[:1])
assert len(widget.children) == 1
assert widget.titles == ('',)
class TestStack(TestCase):
def setUp(self):
self.children = [HTML('0'), HTML('1')]
self.widget = Stack
def test_selected_index_none(self):
widget = self.widget(self.children, selected_index=None)
state = widget.get_state()
assert state['selected_index'] is None
def test_selected_index_default(self):
widget = self.widget(self.children)
state = widget.get_state()
assert state['selected_index'] is None
def test_selected_index(self):
widget = self.widget(self.children, selected_index=1)
state = widget.get_state()
assert state['selected_index'] == 1
def test_selected_index_out_of_bounds(self):
with self.assertRaises(TraitError):
self.widget(self.children, selected_index=-1)
def test_children_position_argument(self):
self.widget(self.children)
def test_titles(self):
widget = self.widget(self.children, selected_index=None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles == ('', '')
widget.set_title(1, 'Title 1')
assert widget.get_state()['titles'] == ('', 'Title 1')
assert widget.titles[1] == 'Title 1'
assert widget.get_title(1) == 'Title 1'
# Backwards compatible with 7.x api
widget.set_title(1, None)
assert widget.get_state()['titles'] == ('', '')
assert widget.titles[1] == ''
assert widget.get_title(1) == ''
with self.assertRaises(IndexError):
widget.set_title(2, 'out of bounds')
with self.assertRaises(IndexError):
widget.get_title(2)
widget.children = tuple(widget.children[:1])
assert len(widget.children) == 1
assert widget.titles == ('',)

View File

@@ -0,0 +1,31 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from traitlets import Bool, Tuple, List
from .utils import setup
from ..widget import Widget
from ..._version import __control_protocol_version__
# A widget with simple traits
class SimpleWidget(Widget):
a = Bool().tag(sync=True)
b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag(
sync=True
)
c = List(Bool()).tag(sync=True)
def test_empty_send_state():
w = SimpleWidget()
w.send_state([])
assert w.comm.messages == []
def test_empty_hold_sync():
w = SimpleWidget()
with w.hold_sync():
pass
assert w.comm.messages == []

View File

@@ -0,0 +1,389 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import pytest
from unittest import mock
from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int, TraitError, observe
from .utils import setup
import ipywidgets
from ipywidgets import Widget
@pytest.fixture(params=[True, False])
def echo(request):
oldvalue = ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO
ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO = request.param
yield request.param
ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO = oldvalue
#
# First some widgets to test on:
#
# A widget with simple traits (list + tuple to ensure both are handled)
class SimpleWidget(Widget):
a = Bool().tag(sync=True)
b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag(sync=True)
c = List(Bool()).tag(sync=True)
# A widget with various kinds of number traits
class NumberWidget(Widget):
f = Float().tag(sync=True)
cf = CFloat().tag(sync=True)
i = Int().tag(sync=True)
ci = CInt().tag(sync=True)
# A widget where the data might be changed on reception:
def transform_fromjson(data, widget):
# Switch the two last elements when setting from json, if the first element is True
# and always set first element to False
if not data[0]:
return data
return [False] + data[1:-2] + [data[-1], data[-2]]
class TransformerWidget(Widget):
d = List(Bool()).tag(sync=True, from_json=transform_fromjson)
# A widget that has a buffer:
class DataInstance():
def __init__(self, data=None):
self.data = data
def mview_serializer(instance, widget):
return { 'data': memoryview(instance.data) if instance.data else None }
def bytes_serializer(instance, widget):
return { 'data': bytearray(memoryview(instance.data).tobytes()) if instance.data else None }
def deserializer(json_data, widget):
return DataInstance( memoryview(json_data['data']).tobytes() if json_data else None )
class DataWidget(SimpleWidget):
d = Instance(DataInstance, args=()).tag(sync=True, to_json=mview_serializer, from_json=deserializer)
# A widget that has a buffer that might be changed on reception:
def truncate_deserializer(json_data, widget):
return DataInstance( json_data['data'][:20].tobytes() if json_data else None )
class TruncateDataWidget(SimpleWidget):
d = Instance(DataInstance, args=()).tag(sync=True, to_json=bytes_serializer, from_json=truncate_deserializer)
#
# Actual tests:
#
def test_set_state_simple(echo):
w = SimpleWidget()
w.set_state(dict(
a=True,
b=[True, False, True],
c=[False, True, False],
))
assert len(w.comm.messages) == (1 if echo else 0)
def test_set_state_transformer(echo):
w = TransformerWidget()
w.set_state(dict(
d=[True, False, True]
))
# Since the deserialize step changes the state, this should send an update
expected = []
if echo:
expected.append(
((), dict(
buffers=[],
data=dict(
buffer_paths=[],
method='echo_update',
state=dict(d=[True, False, True]),
))))
expected.append(
((), dict(
buffers=[],
data=dict(
buffer_paths=[],
method='update',
state=dict(d=[False, True, False]),
))))
assert w.comm.messages == expected
def test_set_state_data(echo):
w = DataWidget()
data = memoryview(b'x'*30)
w.set_state(dict(
a=True,
d={'data': data},
))
assert len(w.comm.messages) == (1 if echo else 0)
def test_set_state_data_truncate(echo):
w = TruncateDataWidget()
data = memoryview(b'x'*30)
w.set_state(dict(
a=True,
d={'data': data},
))
# Get message for checking
assert len(w.comm.messages) == 2 if echo else 1 # ensure we didn't get more than expected
msg = w.comm.messages[-1]
# Assert that the data update (truncation) sends an update
buffers = msg[1].pop('buffers')
assert msg == ((), dict(
data=dict(
method='update',
state=dict(d={}),
buffer_paths=[['d', 'data']]
)))
# Sanity:
assert len(buffers) == 1
assert buffers[0] == data[:20].tobytes()
def test_set_state_numbers_int(echo):
# JS does not differentiate between float/int.
# Instead, it formats exact floats as ints in JSON (1.0 -> '1').
w = NumberWidget()
# Set everything with ints
w.set_state(dict(
f = 1,
cf = 2,
i = 3,
ci = 4,
))
# Ensure one update message gets produced
assert len(w.comm.messages) == (1 if echo else 0)
def test_set_state_numbers_float(echo):
w = NumberWidget()
# Set floats to int-like floats
w.set_state(dict(
f = 1.0,
cf = 2.0,
ci = 4.0
))
# Ensure one update message gets produced
assert len(w.comm.messages) == (1 if echo else 0)
def test_set_state_float_to_float(echo):
w = NumberWidget()
# Set floats to float
w.set_state(dict(
f = 1.2,
cf = 2.6,
))
# Ensure one message gets produced
assert len(w.comm.messages) == (1 if echo else 0)
def test_set_state_cint_to_float(echo):
w = NumberWidget()
# Set CInt to float
w.set_state(dict(
ci = 5.6
))
# Ensure an update message gets produced
assert len(w.comm.messages) == (2 if echo else 1)
msg = w.comm.messages[-1]
data = msg[1]['data']
assert data['method'] == 'update'
assert data['state'] == {'ci': 5}
# This test is disabled, meaning ipywidgets REQUIRES
# any JSON received to format int-like numbers as ints
def _x_test_set_state_int_to_int_like():
# Note: Setting i to an int-like float will produce an
# error, so if JSON producer were to always create
# float formatted numbers, this would fail!
w = NumberWidget()
# Set floats to int-like floats
w.set_state(dict(
i = 3.0
))
# Ensure no update message gets produced
assert len(w.comm.messages) == 0
def test_set_state_int_to_float(echo):
w = NumberWidget()
# Set Int to float
with pytest.raises(TraitError):
w.set_state(dict(
i = 3.5
))
def test_property_lock(echo):
# when this widget's value is set to 42, it sets itself to 2, and then back to 42 again (and then stops)
class AnnoyingWidget(Widget):
value = Float().tag(sync=True)
stop = Bool(False)
@observe('value')
def _propagate_value(self, change):
print('_propagate_value', change.new)
if self.stop:
return
if change.new == 42:
self.value = 2
if change.new == 2:
self.stop = True
self.value = 42
widget = AnnoyingWidget(value=1)
assert widget.value == 1
widget._send = mock.MagicMock()
# this mimics a value coming from the front end
widget.set_state({'value': 42})
assert widget.value == 42
assert widget.stop is True
# we expect no new state to be sent
calls = []
widget._send.assert_has_calls(calls)
def test_hold_sync(echo):
# when this widget's value is set to 42, it sets the value to 2, and also sets a different trait value
class AnnoyingWidget(Widget):
value = Float().tag(sync=True)
other = Float().tag(sync=True)
@observe('value')
def _propagate_value(self, change):
print('_propagate_value', change.new)
if change.new == 42:
self.value = 2
self.other = 11
widget = AnnoyingWidget(value=1)
assert widget.value == 1
widget._send = mock.MagicMock()
# this mimics a value coming from the front end
widget.set_state({'value': 42})
assert widget.value == 2
assert widget.other == 11
msg = {'method': 'echo_update', 'state': {'value': 42.0}, 'buffer_paths': []}
call42 = mock.call(msg, buffers=[])
msg = {'method': 'update', 'state': {'value': 2.0}, 'buffer_paths': []}
call2 = mock.call(msg, buffers=[])
msg = {'method': 'update', 'state': {'other': 11.0}, 'buffer_paths': []}
call11 = mock.call(msg, buffers=[])
calls = [call42, call2, call11] if echo else [call2, call11]
widget._send.assert_has_calls(calls)
def test_echo():
# we always echo values back to the frontend
class ValueWidget(Widget):
value = Float().tag(sync=True)
widget = ValueWidget(value=1)
assert widget.value == 1
widget._send = mock.MagicMock()
# this mimics a state coming from the front end
widget.set_state({'value': 42, 'unexpected_field': 43})
assert widget.value == 42
# we expect this to be echoed
msg = {'method': 'echo_update', 'state': {'value': 42.0}, 'buffer_paths': []}
call42 = mock.call(msg, buffers=[])
calls = [call42]
widget._send.assert_has_calls(calls)
def test_echo_single():
# we always echo multiple changes back in 1 update
class ValueWidget(Widget):
value = Float().tag(sync=True)
square = Float().tag(sync=True)
@observe('value')
def _square(self, change):
self.square = self.value**2
widget = ValueWidget(value=1)
assert widget.value == 1
widget._send = mock.MagicMock()
# this mimics a value coming from the front end
widget._handle_msg({
'content': {
'data': {
'method': 'update',
'state': {
'value': 8,
}
}
}
})
assert widget.value == 8
assert widget.square == 64
# we expect this to be echoed
# note that only value is echoed, not square
msg = {'method': 'echo_update', 'state': {'value': 8.0}, 'buffer_paths': []}
call = mock.call(msg, buffers=[])
msg = {'method': 'update', 'state': {'square': 64}, 'buffer_paths': []}
call2 = mock.call(msg, buffers=[])
calls = [call, call2]
widget._send.assert_has_calls(calls)
def test_no_echo(echo):
# in cases where values coming from the frontend are 'heavy', we might want to opt out
class ValueWidget(Widget):
value = Float().tag(sync=True, echo_update=False)
widget = ValueWidget(value=1)
assert widget.value == 1
widget._send = mock.MagicMock()
# this mimics a value coming from the front end
widget._handle_msg({
'content': {
'data': {
'method': 'update',
'state': {
'value': 42,
}
}
}
})
assert widget.value == 42
# widget._send.assert_not_called(calls)
widget._send.assert_not_called()
# a regular set should sync to the frontend
widget.value = 43
widget._send.assert_has_calls([mock.call({'method': 'update', 'state': {'value': 43.0}, 'buffer_paths': []}, buffers=[])])

View File

@@ -0,0 +1,262 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Test trait types of the widget packages."""
import array
import datetime as dt
import pytest
from unittest import TestCase
from traitlets import HasTraits, Int, TraitError
from traitlets.tests.test_traitlets import TraitTestBase
from ipywidgets import Color, NumberFormat
from ipywidgets.widgets.widget import _remove_buffers, _put_buffers
from ipywidgets.widgets.trait_types import date_serialization, TypedTuple
class NumberFormatTrait(HasTraits):
value = NumberFormat(".3f")
class TestNumberFormat(TraitTestBase):
obj = NumberFormatTrait()
_good_values = [
'.2f', '.0%', '($.2f', '+20', '.^20', '.2s', '#x', ',.2r',
' .2f', '.2', ''
]
_bad_values = [52, False, 'broken', '..2f', '.2a']
class ColorTrait(HasTraits):
value = Color("black")
class TestColor(TraitTestBase):
obj = ColorTrait()
_good_values = [
"blue", # valid color name
"#AA0", # single digit hex
"#FFFFFF", # double digit hex
"transparent", # special color name
'#aaaa', # single digit hex with alpha
'#ffffffff', # double digit hex with alpha
'rgb(0, 0, 0)', # rgb
'rgb( 20,70,50 )', # rgb with spaces
'rgba(10,10,10, 0.5)', # rgba with float alpha
'rgba(255, 255, 255, 255)', # out of bounds alpha (spec says clamp to 1)
'hsl(0.0, .0, 0)', # hsl
'hsl( 0.5,0.3,0 )', # hsl with spaces
'hsla(10,10,10, 0.5)', # rgba with float alpha
'var(--my-color)', # CSS variable without fallback
'var(--my-color-with_separators)', # CSS variable without fallback
'var(--my-color,)', # CSS variable with empty fallback
'var(--my-color-æ)', # CSS variable with non-ascii characters
'var(--my-color-\u1234)', # CSS variable with unicode characters
r'var(--my-color-\\1234)', # CSS variable escaped hex character
'var(--my-color-\.)', # CSS variable with escaped characters
'var(--my-color,black)', # CSS variable with named color fallback
'var(--my-color, black)', # CSS variable with named color fallback
'var(--my-color, rgb(20, 70, 50))', # CSS variable with rgb color fallback
'var(--my-color, #fff)', # CSS variable with rgb color fallback
]
_bad_values = [
"vanilla", "blues", # Invalid color names
1.2, 0.0, # Should fail with float input
0, 1, 2, # Should fail with int input
'rgb(0.4, 512, -40)',
'hsl(0.4, 512, -40)',
'rgba(0, 0, 0)',
'hsla(0, 0, 0)',
'var(-my-color)', # wrong identifier
'var(--my-color-\u2041)', # invalid unicode codepoint
'var(my-color, black)', # wrong identifier
'var(my-color-., black)', # invalid character in identifier
'var(--my-color, vanilla)', # wrong fallback
'var(--my-color, rgba(0,0,0))', # wrong fallback
None,
]
class ColorTraitWithNone(HasTraits):
value = Color("black", allow_none=True)
class TestColorWithNone(TraitTestBase):
obj = ColorTraitWithNone()
_good_values = TestColor._good_values + [None]
_bad_values = list(filter(lambda v: v is not None, TestColor._bad_values))
class TestDateSerialization(TestCase):
def setUp(self):
self.to_json = date_serialization['to_json']
self.dummy_manager = None
def test_serialize_none(self):
self.assertIs(self.to_json(None, self.dummy_manager), None)
def test_serialize_date(self):
date = dt.date(1900, 2, 18)
expected = {
'year': 1900,
'month': 1,
'date': 18
}
self.assertEqual(self.to_json(date, self.dummy_manager), expected)
class TestDateDeserialization(TestCase):
def setUp(self):
self.from_json = date_serialization['from_json']
self.dummy_manager = None
def test_deserialize_none(self):
self.assertIs(self.from_json(None, self.dummy_manager), None)
def test_deserialize_date(self):
serialized_date = {
'year': 1900,
'month': 1,
'date': 18
}
expected = dt.date(1900, 2, 18)
self.assertEqual(
self.from_json(serialized_date, self.dummy_manager),
expected
)
class TestBuffers(TestCase):
def test_remove_and_put_buffers(self):
mv1 = memoryview(b'test1')
mv2 = memoryview(b'test2')
state = {'plain': [0, 'text'], # should not get removed
'x': {'ar': mv1}, # should result in an empty dict
'y': {'shape': (10, 10), 'data': mv1},
'z': (mv1, mv2), # tests tuple assignment
'top': mv1, # test a top level removal
'deep': {'a': 1, 'b':[0,{'deeper':mv2}]}} # deeply nested
plain = state['plain']
x = state['x']
y = state['y']
y_shape = y['shape']
state_before = state
state, buffer_paths, buffers = _remove_buffers(state)
# check if buffers are removed
self.assertIn('plain', state)
self.assertIn('shape', state['y'])
self.assertNotIn('ar', state['x'])
self.assertEqual(state['x'], {})
self.assertNotIn('data', state['y'])
self.assertNotIn(mv1, state['z'])
self.assertNotIn(mv1, state['z'])
self.assertNotIn('top', state)
self.assertIn('deep', state)
self.assertIn('b', state['deep'])
self.assertNotIn('deeper', state['deep']['b'][1])
# check that items that didn't need change aren't touched
self.assertIsNot(state, state_before)
self.assertIs(state['plain'], plain)
self.assertIsNot(state['x'], x)
self.assertIsNot(state['y'], y)
self.assertIs(state['y']['shape'], y_shape)
# check that the buffer paths really point to the right buffer
for path, buffer in [(['x', 'ar'], mv1), (['y', 'data'], mv1), (['z', 0], mv1), (['z', 1], mv2),\
(['top'], mv1), (['deep', 'b', 1, 'deeper'], mv2)]:
self.assertIn(path, buffer_paths, "%r not in path" % path)
index = buffer_paths.index(path)
self.assertEqual(buffer, buffers[index])
# and check that we can put it back together again
_put_buffers(state, buffer_paths, buffers)
# we know that tuples get converted to list, so help the comparison by changing the tuple to a list
state_before['z'] = list(state_before['z'])
self.assertEqual(state_before, state)
def test_typed_tuple_uninitialized_ints():
class TestCase(HasTraits):
value = TypedTuple(trait=Int())
obj = TestCase()
assert obj.value == ()
def test_typed_tuple_init_ints():
class TestCase(HasTraits):
value = TypedTuple(trait=Int())
obj = TestCase(value=(1, 2, 3))
assert obj.value == (1, 2, 3)
def test_typed_tuple_set_ints():
class TestCase(HasTraits):
value = TypedTuple(trait=Int())
obj = TestCase()
obj.value = (1, 2, 3)
assert obj.value == (1, 2, 3)
def test_typed_tuple_default():
class TestCase(HasTraits):
value = TypedTuple(default_value=(1, 2, 3))
obj = TestCase()
assert obj.value == (1, 2, 3)
def test_typed_tuple_mixed_default():
class TestCase(HasTraits):
value = TypedTuple(default_value=(1, 2, 'foobar'))
obj = TestCase()
assert obj.value == (1, 2, 'foobar')
def test_typed_tuple_bad_default():
class TestCase(HasTraits):
value = TypedTuple(trait=Int(), default_value=(1, 2, 'foobar'))
with pytest.raises(TraitError):
obj = TestCase()
a = obj.value # a read might be needed to trigger default validation
def test_typed_tuple_bad_set():
class TestCase(HasTraits):
value = TypedTuple(trait=Int())
obj = TestCase()
with pytest.raises(TraitError):
obj.value = (1, 2, 'foobar')
def test_typed_tuple_positional_trait():
class TestCase(HasTraits):
value = TypedTuple(Int())
obj = TestCase(value=(1, 2, 3))
assert obj.value == (1, 2, 3)
def test_typed_tuple_positional_default():
class TestCase(HasTraits):
value = TypedTuple((1, 2, 3))
obj = TestCase()
assert obj.value == (1, 2, 3)

View File

@@ -0,0 +1,72 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import inspect
import pytest
from ..utils import deprecation
from .utils import call_method
CALL_PATH = inspect.getfile(call_method)
def test_deprecation():
caller_path = inspect.stack(context=0)[1].filename
with pytest.deprecated_call() as record:
deprecation('Deprecated call')
# Make sure the deprecation pointed to the external function calling this test function
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
deprecation('Deprecated call', ['ipywidgets/widgets/tests'])
# Make sure the deprecation pointed to the external function calling this test function
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
deprecation('Deprecated call', 'ipywidgets/widgets/tests')
# Make sure the deprecation pointed to the external function calling this test function
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
deprecation('Deprecated call', [])
# Make sure the deprecation pointed to *this* file
assert len(record) == 1
assert record[0].filename == __file__
def test_deprecation_indirect():
# If the line that calls "deprecation" is not internal, it is considered the source:
with pytest.warns(DeprecationWarning) as record:
call_method(deprecation, "test message", [])
assert len(record) == 1
assert record[0].filename == CALL_PATH
def test_deprecation_indirect_internal():
# If the line that calls "deprecation" is internal, it is not considered the source:
with pytest.warns(DeprecationWarning) as record:
call_method(deprecation, "test message", [CALL_PATH])
assert len(record) == 1
assert record[0].filename == __file__
def test_deprecation_nested1():
def level1():
deprecation("test message", [])
with pytest.warns(DeprecationWarning) as record:
call_method(level1)
assert len(record) == 1
assert record[0].filename == __file__
def test_deprecation_nested2():
def level2():
deprecation("test message", [])
def level1():
level2()
with pytest.warns(DeprecationWarning) as record:
call_method(level1)
assert len(record) == 1
assert record[0].filename == __file__

View File

@@ -0,0 +1,91 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Test Widget."""
import inspect
import pytest
from IPython.core.interactiveshell import InteractiveShell
from IPython.display import display
from IPython.utils.capture import capture_output
from .. import widget
from ..widget import Widget
from ..widget_button import Button
import copy
def test_no_widget_view():
# ensure IPython shell is instantiated
# otherwise display() just calls print
shell = InteractiveShell.instance()
with capture_output() as cap:
w = Widget()
display(w)
assert len(cap.outputs) == 1, "expect 1 output"
mime_bundle = cap.outputs[0].data
assert mime_bundle["text/plain"] == repr(w), "expected plain text output"
assert (
"application/vnd.jupyter.widget-view+json" not in mime_bundle
), "widget has no view"
assert cap.stdout == "", repr(cap.stdout)
assert cap.stderr == "", repr(cap.stderr)
def test_widget_view():
# ensure IPython shell is instantiated
# otherwise display() just calls print
shell = InteractiveShell.instance()
with capture_output() as cap:
w = Button()
display(w)
assert len(cap.outputs) == 1, "expect 1 output"
mime_bundle = cap.outputs[0].data
assert mime_bundle["text/plain"] == repr(w), "expected plain text output"
assert (
"application/vnd.jupyter.widget-view+json" in mime_bundle
), "widget should have have a view"
assert cap.stdout == "", repr(cap.stdout)
assert cap.stderr == "", repr(cap.stderr)
def test_close_all():
# create a couple of widgets
widgets = [Button() for i in range(10)]
assert len(widget._instances) > 0, "expect active widgets"
assert widget._instances[widgets[0].model_id] is widgets[0]
# close all the widgets
Widget.close_all()
assert len(widget._instances) == 0, "active widgets should be cleared"
def test_compatibility():
button = Button()
assert widget._instances[button.model_id] is button
with pytest.deprecated_call() as record:
assert widget._instances is widget.Widget.widgets
assert widget._instances is widget.Widget._active_widgets
assert widget._registry is widget.Widget.widget_types
assert widget._registry is widget.Widget._widget_types
Widget.close_all()
assert not widget.Widget.widgets
assert not widget.Widget._active_widgets
caller_path = inspect.stack(context=0)[1].filename
assert all(x.filename == caller_path for x in record)
assert len(record) == 6
def test_widget_copy():
button = Button()
with pytest.raises(NotImplementedError):
copy.copy(button)
with pytest.raises(NotImplementedError):
copy.deepcopy(button)

View File

@@ -0,0 +1,33 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from unittest import TestCase
from traitlets import TraitError
import ipywidgets as widgets
class TestBox(TestCase):
def test_construction(self):
box = widgets.Box()
assert box.get_state()['children'] == []
def test_construction_with_children(self):
html = widgets.HTML('some html')
slider = widgets.IntSlider()
box = widgets.Box([html, slider])
children_state = box.get_state()['children']
assert children_state == [
widgets.widget._widget_to_json(html, None),
widgets.widget._widget_to_json(slider, None),
]
def test_construction_style(self):
box = widgets.Box(box_style='warning')
assert box.get_state()['box_style'] == 'warning'
def test_construction_invalid_style(self):
with self.assertRaises(TraitError):
widgets.Box(box_style='invalid')

View File

@@ -0,0 +1,12 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import inspect
import pytest
from ipywidgets import Button
def test_deprecation_fa_icons():
with pytest.deprecated_call() as record:
Button(icon='fa-home')
assert len(record) == 1
assert record[0].filename == inspect.stack(context=0)[1].filename

View File

@@ -0,0 +1,156 @@
# coding: utf-8
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import pytest
from contextlib import nullcontext
import datetime
import itertools
import pytz
from traitlets import TraitError
from ..widget_datetime import DatetimePicker
dt_1442 = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
dt_1664 = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
dt_1994 = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
dt_2002 = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
dt_2056 = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
def test_time_creation_blank():
w = DatetimePicker()
assert w.value is None
def test_time_creation_value():
dt = datetime.datetime.now(pytz.utc)
w = DatetimePicker(value=dt)
assert w.value is dt
def test_datetime_validate_value_none():
dt = dt_2002
dt_min = dt_1442
dt_max = dt_2056
w = DatetimePicker(value=dt, min=dt_min, max=dt_max)
w.value = None
assert w.value is None
def _permuted_dts():
ret = []
combos = list(itertools.product([None, dt_1442, dt_2002, dt_2056], repeat=3))
for vals in combos:
expected = vals[0]
if vals[1] and vals[2] and vals[1] > vals[2]:
expected = TraitError
elif vals[0] is None:
pass
elif vals[1] and vals[1] > vals[0]:
expected = vals[1]
elif vals[2] and vals[2] < vals[0]:
expected = vals[2]
ret.append(vals + (expected,))
return ret
@pytest.mark.parametrize(
"input_value,input_min,input_max,expected",
_permuted_dts()
)
def test_datetime_cross_validate_value_min_max(
input_value,
input_min,
input_max,
expected,
):
w = DatetimePicker(value=dt_2002, min=dt_2002, max=dt_2002)
should_raise = expected is TraitError
with pytest.raises(expected) if should_raise else nullcontext():
with w.hold_trait_notifications():
w.value = input_value
w.min = input_min
w.max = input_max
if not should_raise:
assert w.value is expected
def test_datetime_validate_value_vs_min():
dt = dt_2002
dt_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
dt_max = dt_2056
w = DatetimePicker(min=dt_min, max=dt_max)
w.value = dt
assert w.value.year == 2019
def test_datetime_validate_value_vs_max():
dt = dt_2002
dt_min = dt_1664
dt_max = dt_1994
w = DatetimePicker(min=dt_min, max=dt_max)
w.value = dt
assert w.value.year == 1994
def test_datetime_validate_min_vs_value():
dt = dt_2002
dt_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
dt_max = dt_2056
w = DatetimePicker(value=dt, max=dt_max)
w.min = dt_min
assert w.value.year == 2019
def test_datetime_validate_min_vs_max():
dt = dt_2002
dt_min = datetime.datetime(2112, 1, 1, tzinfo=pytz.utc)
dt_max = dt_2056
w = DatetimePicker(value=dt, max=dt_max)
with pytest.raises(TraitError):
w.min = dt_min
def test_datetime_validate_max_vs_value():
dt = dt_2002
dt_min = dt_1664
dt_max = dt_1994
w = DatetimePicker(value=dt, min=dt_min)
w.max = dt_max
assert w.value.year == 1994
def test_datetime_validate_max_vs_min():
dt = dt_2002
dt_min = dt_1664
dt_max = datetime.datetime(1337, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=dt, min=dt_min)
with pytest.raises(TraitError):
w.max = dt_max
def test_datetime_validate_naive():
dt = dt_2002
dt_min = dt_1442
dt_max = dt_2056
w = DatetimePicker(value=dt, min=dt_min, max=dt_max)
with pytest.raises(TraitError):
w.max = dt_max.replace(tzinfo=None)
with pytest.raises(TraitError):
w.min = dt_min.replace(tzinfo=None)
with pytest.raises(TraitError):
w.value = dt.replace(tzinfo=None)
def test_datetime_tzinfo():
tz = pytz.timezone('Australia/Sydney')
dt = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz)
w = DatetimePicker(value=dt)
assert w.value == dt
# tzinfo only changes upon input from user
assert w.value.tzinfo == tz

View File

@@ -0,0 +1,22 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from unittest import TestCase
from traitlets import TraitError
from ipywidgets import FloatSlider
class TestFloatSlider(TestCase):
def test_construction(self):
FloatSlider()
def test_construction_readout_format(self):
slider = FloatSlider(readout_format='$.1f')
assert slider.get_state()['readout_format'] == '$.1f'
def test_construction_invalid_readout_format(self):
with self.assertRaises(TraitError):
FloatSlider(readout_format='broken')

View File

@@ -0,0 +1,173 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Test Image widget"""
import io
import os
from ipywidgets import Image
import hashlib
import pkgutil
import tempfile
from contextlib import contextmanager
# Data
@contextmanager
def get_logo_png():
# Once the tests are not in the package, this context manager can be
# replaced with the location of the actual file
LOGO_DATA = pkgutil.get_data('ipywidgets.widgets.tests',
'data/jupyter-logo-transparent.png')
handle, fname = tempfile.mkstemp()
os.close(handle)
with open(fname, 'wb') as f:
f.write(LOGO_DATA)
yield fname
os.remove(fname)
LOGO_PNG_DIGEST = '3ff9eafd7197083153e83339a72e7a335539bae189c33554c680e4382c98af02'
def test_empty_image():
# Empty images shouldn't raise any errors
Image()
def test_image_value():
random_bytes = b'\x0ee\xca\x80\xcd\x9ak#\x7f\x07\x03\xa7'
Image(value=random_bytes)
def test_image_format():
# Test that these format names don't throw an error
Image(format='png')
Image(format='jpeg')
Image(format='url')
def test_from_filename():
with get_logo_png() as LOGO_PNG:
img = Image.from_file(LOGO_PNG)
assert_equal_hash(img.value, LOGO_PNG_DIGEST)
def test_set_from_filename():
img = Image()
with get_logo_png() as LOGO_PNG:
img.set_value_from_file(LOGO_PNG)
assert_equal_hash(img.value, LOGO_PNG_DIGEST)
def test_from_file():
with get_logo_png() as LOGO_PNG:
with open(LOGO_PNG, 'rb') as f:
img = Image.from_file(f)
assert_equal_hash(img.value, LOGO_PNG_DIGEST)
def test_set_value_from_file():
img = Image()
with get_logo_png() as LOGO_PNG:
with open(LOGO_PNG, 'rb') as f:
img.set_value_from_file(f)
assert_equal_hash(img.value, LOGO_PNG_DIGEST)
def test_from_url_unicode():
img = Image.from_url('https://jupyter.org/assets/main-logo.svg')
assert img.value == b'https://jupyter.org/assets/main-logo.svg'
def test_from_url_bytes():
img = Image.from_url(b'https://jupyter.org/assets/main-logo.svg')
assert img.value == b'https://jupyter.org/assets/main-logo.svg'
def test_format_inference_filename():
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
name = f.name
f.close() # Allow tests to run on Windows
img = Image.from_file(name)
assert img.format == 'svg+xml'
def test_format_inference_file():
with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as f:
img = Image.from_file(f)
assert img.format == 'gif'
def test_format_inference_stream():
# There's no way to infer the format, so it should default to png
fstream = io.BytesIO(b'')
img = Image.from_file(fstream)
assert img.format == 'png'
def test_serialize():
fstream = io.BytesIO(b'123')
img = Image.from_file(fstream)
img_state = img.get_state()
# for python27 it is a memoryview
assert isinstance(img_state['value'], (bytes, memoryview))
# make sure it is (for python 3), since that is what it will be once it comes off the wire
img_state['value'] = memoryview(img_state['value'])
# check that we can deserialize it and get back the original value
img_copy = Image()
img_copy.set_state(img_state)
assert img.value == img_copy.value
def test_format_inference_overridable():
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
name = f.name
f.close() # Allow tests to run on Windows
img = Image.from_file(name, format='gif')
assert img.format == 'gif'
def test_value_repr_length():
with get_logo_png() as LOGO_PNG:
with open(LOGO_PNG, 'rb') as f:
img = Image.from_file(f)
assert len(img.__repr__()) < 140
assert img.__repr__().endswith(")")
assert img.__repr__()[-5:-2] == '...'
def test_value_repr_url():
img = Image.from_url(b'https://jupyter.org/assets/main-logo.svg')
assert 'https://jupyter.org/assets/main-logo.svg' in img.__repr__()
# Helper functions
def get_hash_hex(byte_str):
m = hashlib.new('sha256')
m.update(byte_str)
return m.hexdigest()
def assert_equal_hash(byte_str, digest):
assert get_hash_hex(byte_str) == digest

View File

@@ -0,0 +1,94 @@
# coding: utf-8
# Copyright (c) Vidar Tonaas Fauske.
# Distributed under the terms of the Modified BSD License.
import pytest
import datetime
import pytz
from traitlets import TraitError
from ..widget_datetime import NaiveDatetimePicker
def test_time_creation_blank():
w = NaiveDatetimePicker()
assert w.value is None
def test_time_creation_value():
t = datetime.datetime.today()
w = NaiveDatetimePicker(value=t)
assert w.value is t
def test_time_validate_value_none():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(1442, 1, 1)
t_max = datetime.datetime(2056, 1, 1)
w = NaiveDatetimePicker(value=t, min=t_min, max=t_max)
w.value = None
assert w.value is None
def test_time_validate_value_vs_min():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(2019, 1, 1)
t_max = datetime.datetime(2056, 1, 1)
w = NaiveDatetimePicker(min=t_min, max=t_max)
w.value = t
assert w.value.year == 2019
def test_time_validate_value_vs_max():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(1664, 1, 1)
t_max = datetime.datetime(1994, 1, 1)
w = NaiveDatetimePicker(min=t_min, max=t_max)
w.value = t
assert w.value.year == 1994
def test_time_validate_min_vs_value():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(2019, 1, 1)
t_max = datetime.datetime(2056, 1, 1)
w = NaiveDatetimePicker(value=t, max=t_max)
w.min = t_min
assert w.value.year == 2019
def test_time_validate_min_vs_max():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(2112, 1, 1)
t_max = datetime.datetime(2056, 1, 1)
w = NaiveDatetimePicker(value=t, max=t_max)
with pytest.raises(TraitError):
w.min = t_min
def test_time_validate_max_vs_value():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(1664, 1, 1)
t_max = datetime.datetime(1994, 1, 1)
w = NaiveDatetimePicker(value=t, min=t_min)
w.max = t_max
assert w.value.year == 1994
def test_time_validate_max_vs_min():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7)
t_min = datetime.datetime(1664, 1, 1)
t_max = datetime.datetime(1337, 1, 1)
w = NaiveDatetimePicker(value=t, min=t_min)
with pytest.raises(TraitError):
w.max = t_max
def test_datetime_tzinfo():
tz = pytz.timezone('Australia/Sydney')
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz)
with pytest.raises(TraitError):
w = NaiveDatetimePicker(value=t)

View File

@@ -0,0 +1,239 @@
import sys
from unittest import TestCase
from contextlib import contextmanager
from IPython.display import Markdown, Image
from ipywidgets import widget_output
class TestOutputWidget(TestCase):
@contextmanager
def _mocked_ipython(self, get_ipython, clear_output):
""" Context manager that monkeypatches get_ipython and clear_output """
original_clear_output = widget_output.clear_output
original_get_ipython = widget_output.get_ipython
widget_output.get_ipython = get_ipython
widget_output.clear_output = clear_output
try:
yield
finally:
widget_output.clear_output = original_clear_output
widget_output.get_ipython = original_get_ipython
def _mock_get_ipython(self, msg_id):
""" Returns a mock IPython application with a mocked kernel """
kernel = type(
'mock_kernel',
(object, ),
{'_parent_header': {'header': {'msg_id': msg_id}}}
)
# Specifically override this so the traceback
# is still printed to screen
def showtraceback(self_, exc_tuple, *args, **kwargs):
etype, evalue, tb = exc_tuple
raise etype(evalue)
ipython = type(
'mock_ipython',
(object, ),
{'kernel': kernel, 'showtraceback': showtraceback}
)
return ipython
def _mock_clear_output(self):
""" Mock function that records calls to it """
calls = []
def clear_output(*args, **kwargs):
calls.append((args, kwargs))
clear_output.calls = calls
return clear_output
def test_set_msg_id_when_capturing(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
with self._mocked_ipython(get_ipython, clear_output):
widget = widget_output.Output()
assert widget.msg_id == ''
with widget:
assert widget.msg_id == msg_id
assert widget.msg_id == ''
def test_clear_output(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
with self._mocked_ipython(get_ipython, clear_output):
widget = widget_output.Output()
widget.clear_output(wait=True)
assert len(clear_output.calls) == 1
assert clear_output.calls[0] == ((), {'wait': True})
def test_capture_decorator(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
expected_argument = 'arg'
expected_keyword_argument = True
captee_calls = []
with self._mocked_ipython(get_ipython, clear_output):
widget = widget_output.Output()
assert widget.msg_id == ''
@widget.capture()
def captee(*args, **kwargs):
# Check that we are capturing output
assert widget.msg_id == msg_id
# Check that arguments are passed correctly
captee_calls.append((args, kwargs))
captee(
expected_argument, keyword_argument=expected_keyword_argument)
assert widget.msg_id == ''
captee()
assert len(captee_calls) == 2
assert captee_calls[0] == (
(expected_argument, ),
{'keyword_argument': expected_keyword_argument}
)
assert captee_calls[1] == ((), {})
def test_capture_decorator_clear_output(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
with self._mocked_ipython(get_ipython, clear_output):
widget = widget_output.Output()
@widget.capture(clear_output=True, wait=True)
def captee(*args, **kwargs):
# Check that we are capturing output
assert widget.msg_id == msg_id
captee()
captee()
assert len(clear_output.calls) == 2
assert clear_output.calls[0] == clear_output.calls[1] == \
((), {'wait': True})
def test_capture_decorator_no_clear_output(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
with self._mocked_ipython(get_ipython, clear_output):
widget = widget_output.Output()
@widget.capture(clear_output=False)
def captee(*args, **kwargs):
# Check that we are capturing output
assert widget.msg_id == msg_id
captee()
captee()
assert len(clear_output.calls) == 0
def _make_stream_output(text, name):
return {
'output_type': 'stream',
'name': name,
'text': text
}
def test_append_stdout():
widget = widget_output.Output()
# Try appending a message to stdout.
widget.append_stdout("snakes!")
expected = (_make_stream_output("snakes!", "stdout"),)
assert widget.outputs == expected, repr(widget.outputs)
# Try appending a second message.
widget.append_stdout("more snakes!")
expected += (_make_stream_output("more snakes!", "stdout"),)
assert widget.outputs == expected, repr(widget.outputs)
def test_append_stderr():
widget = widget_output.Output()
# Try appending a message to stderr.
widget.append_stderr("snakes!")
expected = (_make_stream_output("snakes!", "stderr"),)
assert widget.outputs == expected, repr(widget.outputs)
# Try appending a second message.
widget.append_stderr("more snakes!")
expected += (_make_stream_output("more snakes!", "stderr"),)
assert widget.outputs == expected, repr(widget.outputs)
def test_append_display_data():
widget = widget_output.Output()
# Try appending a Markdown object.
widget.append_display_data(Markdown("# snakes!"))
expected = (
{
'output_type': 'display_data',
'data': {
'text/plain': '<IPython.core.display.Markdown object>',
'text/markdown': '# snakes!'
},
'metadata': {}
},
)
assert widget.outputs == expected, repr(widget.outputs)
# Now try appending an Image.
image_data = b"foobar"
widget.append_display_data(Image(image_data, width=123, height=456))
# Old ipykernel/IPython
expected1 = expected + (
{
'output_type': 'display_data',
'data': {
'image/png': 'Zm9vYmFy\n',
'text/plain': '<IPython.core.display.Image object>'
},
'metadata': {
'image/png': {
'width': 123,
'height': 456
}
}
},
)
# Latest ipykernel/IPython
expected2 = expected + (
{
'output_type': 'display_data',
'data': {
'image/png': 'Zm9vYmFy',
'text/plain': '<IPython.core.display.Image object>'
},
'metadata': {
'image/png': {
'width': 123,
'height': 456
}
}
},
)
assert widget.outputs == expected1 or widget.outputs == expected2

View File

@@ -0,0 +1,107 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import inspect
from unittest import TestCase
from traitlets import TraitError
from ipywidgets import Dropdown, SelectionSlider, Select
class TestDropdown(TestCase):
def test_construction(self):
Dropdown()
def test_dict_mapping_options(self):
d = Dropdown(options={'One': 1, 'Two': 2, 'Three': 3})
assert d.get_state('_options_labels') == {'_options_labels': ('One', 'Two', 'Three')}
def test_setting_options_from_list(self):
d = Dropdown()
assert d.options == ()
d.options = ['One', 'Two', 'Three']
assert d.get_state('_options_labels') == {'_options_labels': ('One', 'Two', 'Three')}
def test_setting_options_from_list_tuples(self):
d = Dropdown()
assert d.options == ()
d.options = [('One', 1), ('Two', 2), ('Three', 3)]
assert d.get_state('_options_labels') == {'_options_labels': ('One', 'Two', 'Three')}
d.value = 2
assert d.get_state('index') == {'index': 1}
def test_setting_options_from_dict(self):
d = Dropdown()
assert d.options == ()
d.options = {'One': 1, 'Two': 2, 'Three': 3}
assert d.get_state('_options_labels') == {'_options_labels': ('One', 'Two', 'Three')}
class TestSelectionSlider(TestCase):
def test_construction(self):
SelectionSlider(options=['a', 'b', 'c'])
def test_index_trigger(self):
slider = SelectionSlider(options=['a', 'b', 'c'])
observations = []
def f(change):
observations.append(change.new)
slider.observe(f, 'index')
assert slider.index == 0
slider.options = [4, 5, 6]
assert slider.index == 0
assert slider.value == 4
assert slider.label == '4'
assert observations == [0]
class TestSelection(TestCase):
def test_construction(self):
select = Select(options=['a', 'b', 'c'])
def test_index_trigger(self):
select = Select(options=[1, 2, 3])
observations = []
def f(change):
observations.append(change.new)
select.observe(f, 'index')
assert select.index == 0
select.options = [4, 5, 6]
assert select.index == 0
assert select.value == 4
assert select.label == '4'
assert observations == [0]
def test_duplicate(self):
select = Select(options=['first', 1, 'dup', 'dup'])
observations = []
def f(change):
observations.append(change.new)
select.observe(f, 'index')
select.index = 3
assert select.index == 3
assert select.value == 'dup'
assert select.label == 'dup'
assert observations == [3]
select.index = 2
assert select.index == 2
assert select.value == 'dup'
assert select.label == 'dup'
assert observations == [3, 2]
select.index = 0
assert select.index == 0
assert select.value == 'first'
assert select.label == 'first'
assert observations == [3, 2, 0]
# picks the first matching value
select.value = 'dup'
assert select.index == 2
assert select.value == 'dup'
assert select.label == 'dup'
assert observations == [3, 2, 0, 2]

View File

@@ -0,0 +1,65 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import inspect
import pytest
from ..widget_string import Combobox, Text
def test_combobox_creation_blank():
w = Combobox()
assert w.value == ''
assert w.options == ()
assert w.ensure_option == False
def test_combobox_creation_kwargs():
w = Combobox(
value='Chocolate',
options=[
"Chocolate",
"Coconut",
"Mint",
"Strawberry",
"Vanilla",
],
ensure_option=True
)
assert w.value == 'Chocolate'
assert w.options == (
"Chocolate",
"Coconut",
"Mint",
"Strawberry",
"Vanilla",
)
assert w.ensure_option == True
def test_tooltip_deprecation():
caller_path = inspect.stack(context=0)[1].filename
with pytest.deprecated_call() as record:
w = Text(description_tooltip="testing")
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
w.description_tooltip
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
w.description_tooltip == "testing"
assert len(record) == 1
assert record[0].filename == caller_path
with pytest.deprecated_call() as record:
w.description_tooltip = "second value"
assert len(record) == 1
assert record[0].filename == caller_path
assert w.tooltip == "second value"
def test_on_submit_deprecation():
with pytest.deprecated_call() as record:
Text().on_submit(lambda *args: ...)
assert len(record) == 1
assert record[0].filename == inspect.stack(context=0)[1].filename

View File

@@ -0,0 +1,714 @@
"Testing widget layout templates"
from unittest import TestCase
from unittest import mock
import pytest
import traitlets
import ipywidgets as widgets
from ipywidgets.widgets.widget_templates import LayoutProperties
class TestTwoByTwoLayout(TestCase):
"""test layout templates"""
def test_merge_cells(self): #pylint: disable=no-self-use
"""test merging cells with missing widgets"""
button1 = widgets.Button()
button2 = widgets.Button()
button3 = widgets.Button()
button4 = widgets.Button()
box = widgets.TwoByTwoLayout(top_left=button1,
top_right=button2,
bottom_left=button3,
bottom_right=button4)
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
assert box.top_left.layout.grid_area == 'top-left'
assert box.top_right.layout.grid_area == 'top-right'
assert box.bottom_left.layout.grid_area == 'bottom-left'
assert box.bottom_right.layout.grid_area == 'bottom-right'
assert len(box.get_state()['children']) == 4
box = widgets.TwoByTwoLayout(top_left=button1,
top_right=button2,
bottom_left=None,
bottom_right=button4)
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"top-left bottom-right"')
assert box.top_left.layout.grid_area == 'top-left'
assert box.top_right.layout.grid_area == 'top-right'
assert box.bottom_left is None
assert box.bottom_right.layout.grid_area == 'bottom-right'
assert len(box.get_state()['children']) == 3
box = widgets.TwoByTwoLayout(top_left=None,
top_right=button2,
bottom_left=button3,
bottom_right=button4)
assert box.layout.grid_template_areas == ('"bottom-left top-right"\n' +
'"bottom-left bottom-right"')
assert box.top_left is None
assert box.top_right.layout.grid_area == 'top-right'
assert box.bottom_left.layout.grid_area == 'bottom-left'
assert box.bottom_right.layout.grid_area == 'bottom-right'
assert len(box.get_state()['children']) == 3
box = widgets.TwoByTwoLayout(top_left=None,
top_right=button2,
bottom_left=None,
bottom_right=button4)
assert box.layout.grid_template_areas == ('"top-right top-right"\n' +
'"bottom-right bottom-right"')
assert box.top_left is None
assert box.top_right.layout.grid_area == 'top-right'
assert box.bottom_left is None
assert box.bottom_right.layout.grid_area == 'bottom-right'
assert len(box.get_state()['children']) == 2
box = widgets.TwoByTwoLayout(top_left=button1,
top_right=None,
bottom_left=button3,
bottom_right=button4)
assert box.layout.grid_template_areas == ('"top-left bottom-right"\n' +
'"bottom-left bottom-right"')
assert box.top_left.layout.grid_area == 'top-left'
assert box.top_right is None
assert box.bottom_left.layout.grid_area == 'bottom-left'
assert box.bottom_right.layout.grid_area == 'bottom-right'
assert len(box.get_state()['children']) == 3
box = widgets.TwoByTwoLayout(top_left=button1,
top_right=None,
bottom_left=None,
bottom_right=None)
assert box.layout.grid_template_areas == ('"top-left top-left"\n' +
'"top-left top-left"')
assert box.top_left is button1
assert box.top_left.layout.grid_area == 'top-left'
assert box.top_right is None
assert box.bottom_left is None
assert box.bottom_right is None
assert len(box.get_state()['children']) == 1
box = widgets.TwoByTwoLayout(top_left=None,
top_right=button1,
bottom_left=None,
bottom_right=None)
assert box.layout.grid_template_areas == ('"top-right top-right"\n' +
'"top-right top-right"')
assert box.top_right is button1
assert box.top_right.layout.grid_area == 'top-right'
assert box.top_left is None
assert box.bottom_left is None
assert box.bottom_right is None
assert len(box.get_state()['children']) == 1
box = widgets.TwoByTwoLayout(top_left=None,
top_right=None,
bottom_left=None,
bottom_right=None)
assert box.layout.grid_template_areas is None
assert box.top_left is None
assert box.top_right is None
assert box.bottom_left is None
assert box.bottom_right is None
assert not box.get_state()['children']
box = widgets.TwoByTwoLayout(top_left=None,
top_right=button1,
bottom_left=None,
bottom_right=None,
merge=False)
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
assert box.top_right is button1
assert box.top_right.layout.grid_area == 'top-right'
assert box.top_left is None
assert box.bottom_left is None
assert box.bottom_right is None
assert len(box.get_state()['children']) == 1
def test_keep_layout_options(self): #pylint: disable=no-self-use
"""test whether layout options are passed down to GridBox"""
layout = widgets.Layout(align_items="center")
button1 = widgets.Button()
button2 = widgets.Button()
button3 = widgets.Button()
button4 = widgets.Button()
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button2,
bottom_left=button3, bottom_right=button4,
layout=layout)
assert box.layout.align_items == 'center'
def test_pass_layout_options(self): #pylint: disable=no-self-use
"""test whether the extra layout options of the template class are
passed down to Layout object"""
button1 = widgets.Button()
button2 = widgets.Button()
button3 = widgets.Button()
button4 = widgets.Button()
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button2,
bottom_left=button3, bottom_right=button4,
grid_gap="10px", justify_content="center",
align_items="center")
assert box.layout.grid_gap == "10px"
assert box.layout.justify_content == "center"
assert box.layout.align_items == "center"
# we still should be able to pass them through layout
layout = widgets.Layout(grid_gap="10px", justify_content="center",
align_items="center")
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button2,
bottom_left=button3, bottom_right=button4,
layout=layout
)
assert box.layout.grid_gap == "10px"
assert box.layout.justify_content == "center"
assert box.layout.align_items == "center"
# values passed directly in the constructor should overwrite layout options
layout = widgets.Layout(grid_gap="10px", justify_content="center",
align_items="center")
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button2,
bottom_left=button3, bottom_right=button4,
layout=layout, grid_gap="30px"
)
assert box.layout.grid_gap == "30px"
assert box.layout.justify_content == "center"
assert box.layout.align_items == "center"
@mock.patch("ipywidgets.Layout.send_state")
def test_update_dynamically(self, send_state): #pylint: disable=no-self-use
"""test whether it's possible to add widget outside __init__"""
button1 = widgets.Button()
button2 = widgets.Button()
button3 = widgets.Button()
button4 = widgets.Button()
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button3,
bottom_left=None, bottom_right=button4)
from ipykernel.kernelbase import Kernel
state = box.get_state()
assert len(state['children']) == 3
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"top-left bottom-right"')
box.layout.comm.kernel = mock.MagicMock(spec=Kernel) #for mocking purposes
send_state.reset_mock()
box.bottom_left = button2
state = box.get_state()
assert len(state['children']) == 4
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
# check whether frontend was informed
send_state.assert_called_with(key="grid_template_areas")
box = widgets.TwoByTwoLayout(top_left=button1, top_right=button3,
bottom_left=None, bottom_right=button4)
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"top-left bottom-right"')
box.layout.comm.kernel = mock.MagicMock(spec=Kernel) #for mocking purposes
send_state.reset_mock()
box.merge = False
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
send_state.assert_called_with(key="grid_template_areas")
class TestAppLayout(TestCase):
"""test layout templates"""
def test_create_with_defaults(self):
"test creating with default values"
footer = widgets.Button()
header = widgets.Button()
center = widgets.Button()
left_sidebar = widgets.Button()
right_sidebar = widgets.Button()
box = widgets.AppLayout(
footer=footer,
header=header,
center=center,
left_sidebar=left_sidebar,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"left-sidebar center right-sidebar"\n' +
'"footer footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert len(box.get_state()['children']) == 5
# empty layout should produce no effects
box = widgets.AppLayout()
assert box.layout.grid_template_areas is None
assert box.layout.grid_template_columns is None
assert box.layout.grid_template_rows is None
assert len(box.get_state()['children']) == 0
def test_merge_empty_cells(self):
"test if cells are correctly merged"
footer = widgets.Button()
header = widgets.Button()
center = widgets.Button()
left_sidebar = widgets.Button()
right_sidebar = widgets.Button()
# merge all if only one widget
box = widgets.AppLayout(
center=center
)
assert box.layout.grid_template_areas == ('"center center center"\n' +
'"center center center"\n' +
'"center center center"')
assert box.center.layout.grid_area == 'center'
assert len(box.get_state()['children']) == 1
box = widgets.AppLayout(
left_sidebar=left_sidebar
)
assert box.layout.grid_template_areas == ('"left-sidebar left-sidebar left-sidebar"\n' +
'"left-sidebar left-sidebar left-sidebar"\n' +
'"left-sidebar left-sidebar left-sidebar"')
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert len(box.get_state()['children']) == 1
# merge left and right sidebars with center
box = widgets.AppLayout(
header=header,
footer=footer,
left_sidebar=left_sidebar,
center=center
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"left-sidebar center center"\n' +
'"footer footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert len(box.get_state()['children']) == 4
box = widgets.AppLayout(
header=header,
footer=footer,
right_sidebar=right_sidebar,
center=center
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"center center right-sidebar"\n' +
'"footer footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center.layout.grid_area == 'center'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert len(box.get_state()['children']) == 4
box = widgets.AppLayout(
header=header,
footer=footer,
center=center
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"center center center"\n' +
'"footer footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center.layout.grid_area == 'center'
assert len(box.get_state()['children']) == 3
# if only center missing, remove it from view
box = widgets.AppLayout(
header=header,
footer=footer,
center=None,
left_sidebar=left_sidebar,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == ('"header header"\n' +
'"left-sidebar right-sidebar"\n' +
'"footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert box.center is None
assert len(box.get_state()['children']) == 4
# center and one sidebar missing -> 3 row arrangement
box = widgets.AppLayout(
header=header,
footer=footer,
center=None,
left_sidebar=None,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == ('"header header"\n' +
'"right-sidebar right-sidebar"\n' +
'"footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.left_sidebar is None
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert box.center is None
assert len(box.get_state()['children']) == 3
# remove middle row is both sidebars and center missing
box = widgets.AppLayout(
header=header,
footer=footer,
center=None,
left_sidebar=None,
right_sidebar=None
)
assert box.layout.grid_template_areas == ('"header"\n' +
'"footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center is None
assert box.left_sidebar is None
assert box.right_sidebar is None
assert len(box.get_state()['children']) == 2
# do not merge if merge=False
box = widgets.AppLayout(
header=header,
footer=footer,
center=center,
merge=False
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"left-sidebar center right-sidebar"\n' +
'"footer footer footer"')
assert box.footer.layout.grid_area == 'footer'
assert box.header.layout.grid_area == 'header'
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar is None
assert box.right_sidebar is None
assert len(box.get_state()['children']) == 3
# merge header and footer simply removes it from view
box = widgets.AppLayout(
footer=footer,
center=center,
left_sidebar=left_sidebar,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == ('"left-sidebar center right-sidebar"\n' +
'"footer footer footer"')
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert box.footer.layout.grid_area == 'footer'
assert box.header is None
assert len(box.get_state()['children']) == 4
box = widgets.AppLayout(
header=header,
center=center,
left_sidebar=left_sidebar,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == ('"header header header"\n' +
'"left-sidebar center right-sidebar"')
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert box.header.layout.grid_area == 'header'
assert box.footer is None
assert len(box.get_state()['children']) == 4
box = widgets.AppLayout(
center=center,
left_sidebar=left_sidebar,
right_sidebar=right_sidebar
)
assert box.layout.grid_template_areas == '"left-sidebar center right-sidebar"'
assert box.center.layout.grid_area == 'center'
assert box.left_sidebar.layout.grid_area == 'left-sidebar'
assert box.right_sidebar.layout.grid_area == 'right-sidebar'
assert box.footer is None
assert box.header is None
assert len(box.get_state()['children']) == 3
# merge all if only one widget
box = widgets.AppLayout(
center=center
)
assert box.layout.grid_template_areas == ('"center center center"\n' +
'"center center center"\n' +
'"center center center"')
assert box.center.layout.grid_area == 'center'
assert len(box.get_state()['children']) == 1
def test_size_to_css(self):
box = widgets.AppLayout()
assert box._size_to_css("100px") == '100px'
assert box._size_to_css("1fr") == '1fr'
assert box._size_to_css("2.5fr") == '2.5fr'
assert box._size_to_css('2.5') == '2.5fr'
assert box._size_to_css('25%') == '25%'
with pytest.raises(TypeError):
box._size_to_css('this is not correct size')
def test_set_pane_widths_heights(self):
footer = widgets.Button()
header = widgets.Button()
center = widgets.Button()
left_sidebar = widgets.Button()
right_sidebar = widgets.Button()
box = widgets.AppLayout(
header=header,
footer=footer,
left_sidebar=left_sidebar,
right_sidebar=left_sidebar,
center=center
)
with pytest.raises(traitlets.TraitError):
box.pane_widths = ['1fx', '1fx', '1fx', '1fx']
with pytest.raises(traitlets.TraitError):
box.pane_widths = ['1fx', '1fx']
with pytest.raises(traitlets.TraitError):
box.pane_heights = ['1fx', '1fx', '1fx', '1fx']
with pytest.raises(traitlets.TraitError):
box.pane_heights = ['1fx', '1fx']
assert box.layout.grid_template_rows == "1fr 3fr 1fr"
assert box.layout.grid_template_columns == "1fr 2fr 1fr"
box.pane_heights = ['3fr', '100px', 20]
assert box.layout.grid_template_rows == "3fr 100px 20fr"
assert box.layout.grid_template_columns == "1fr 2fr 1fr"
box.pane_widths = [3, 3, 1]
assert box.layout.grid_template_rows == "3fr 100px 20fr"
assert box.layout.grid_template_columns == "3fr 3fr 1fr"
class TestGridspecLayout(TestCase):
"test GridspecLayout"
def test_init(self):
with pytest.raises(traitlets.TraitError):
box = widgets.GridspecLayout()
with pytest.raises(traitlets.TraitError):
box = widgets.GridspecLayout(n_rows=-1, n_columns=1)
box = widgets.GridspecLayout(n_rows=5, n_columns=3)
assert box.n_rows == 5
assert box.n_columns == 3
assert len(box._grid_template_areas) == 5
assert len(box._grid_template_areas[0]) == 3
box = widgets.GridspecLayout(1, 2)
assert box.n_rows == 1
assert box.n_columns == 2
with pytest.raises(traitlets.TraitError):
box = widgets.GridspecLayout(0, 0)
def test_setitem_index(self):
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
button2 = widgets.Button()
button3 = widgets.Button()
button4 = widgets.Button()
box[0, 0] = button1
button1_label = button1.layout.grid_area
assert button1 in box.children
assert box.layout.grid_template_areas == '''"{} . ."\n". . ."'''.format(button1_label)
box[-1, -1] = button2
button2_label = button2.layout.grid_area
assert button1_label != button2_label
assert button2 in box.children
assert box.layout.grid_template_areas == '''"{} . ."\n". . {}"'''.format(button1_label,
button2_label)
box[1, 0] = button3
button3_label = button3.layout.grid_area
assert button1_label != button3_label
assert button2_label != button3_label
assert button3 in box.children
assert box.layout.grid_template_areas == '''"{b1} . ."\n"{b3} . {b2}"'''.format(b1=button1_label,
b2=button2_label,
b3=button3_label)
#replace widget
box[1, 0] = button4
button4_label = button4.layout.grid_area
assert button1_label != button4_label
assert button2_label != button4_label
assert button4 in box.children
assert button3 not in box.children
assert box.layout.grid_template_areas == '''"{b1} . ."\n"{b4} . {b2}"'''.format(b1=button1_label,
b2=button2_label,
b4=button4_label)
def test_setitem_slices(self):
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
box[:2, 0] = button1
assert len(box.children) == 1
assert button1 in box.children
button1_label = button1.layout.grid_area
assert box.layout.grid_template_areas == '''"{b1} . ."\n"{b1} . ."'''.format(b1=button1_label)
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
button2 = widgets.Button()
box[:2, 1:] = button1
assert len(box.children) == 1
assert button1 in box.children
button1_label = button1.layout.grid_area
assert box.layout.grid_template_areas == '''". {b1} {b1}"\n". {b1} {b1}"'''.format(b1=button1_label)
# replace button
box[:2, 1:] = button2
assert len(box.children) == 1
assert button2 in box.children
button2_label = button2.layout.grid_area
assert box.layout.grid_template_areas == '''". {b1} {b1}"\n". {b1} {b1}"'''.format(b1=button2_label)
def test_getitem_index(self):
"test retrieving widget"
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
box[0, 0] = button1
assert box[0, 0] is button1
def test_getitem_slices(self):
"test retrieving widgets with slices"
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
box[:2, 0] = button1
assert box[:2, 0] is button1
box = widgets.GridspecLayout(2, 3)
button1 = widgets.Button()
button2 = widgets.Button()
box[0, 0] = button1
box[1, 0] = button2
assert box[0, 0] is button1
assert box[1, 0] is button2
with pytest.raises(TypeError, match="The slice spans"):
button = box[:2, 0]
class TestLayoutProperties(TestCase):
"""test mixin with layout properties"""
class DummyTemplate(widgets.GridBox, LayoutProperties):
location = traitlets.Instance(widgets.Widget, allow_none=True)
def test_layout_updated_on_trait_change(self):
"test whether respective layout traits are updated when traits change"
template = self.DummyTemplate(width="100%")
assert template.width == '100%'
assert template.layout.width == '100%'
template.width = 'auto'
assert template.width == 'auto'
assert template.layout.width == 'auto'
def test_align_items_extra_options(self):
template = self.DummyTemplate(align_items='top')
assert template.align_items == 'top'
assert template.layout.align_items == 'flex-start'
template.align_items = 'bottom'
assert template.align_items == 'bottom'
assert template.layout.align_items == 'flex-end'
def test_validate_properties(self):
prop_obj = self.DummyTemplate()
for prop in LayoutProperties.align_items.values:
prop_obj.align_items = prop
assert prop_obj.align_items == prop
with pytest.raises(traitlets.TraitError):
prop_obj.align_items = 'any default position'

View File

@@ -0,0 +1,100 @@
# coding: utf-8
# Copyright (c) Vidar Tonaas Fauske.
# Distributed under the terms of the Modified BSD License.
import pytest
import datetime
from traitlets import TraitError
from ..widget_time import TimePicker
def test_time_creation_blank():
w = TimePicker()
assert w.value is None
def test_time_creation_value():
t = datetime.time()
w = TimePicker(value=t)
assert w.value is t
def test_time_cross_validate_value_min_max():
w = TimePicker(value=datetime.time(2), min=datetime.time(2), max=datetime.time(2))
with w.hold_trait_notifications():
w.value = None
w.min = datetime.time(4)
w.max = datetime.time(6)
assert w.value is None
with w.hold_trait_notifications():
w.value = datetime.time(4)
w.min = None
w.max = None
assert w.value == datetime.time(4)
def test_time_validate_value_none():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(2)
t_max = datetime.time(22)
w = TimePicker(value=t, min=t_min, max=t_max)
w.value = None
assert w.value is None
def test_time_validate_value_vs_min():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(14)
t_max = datetime.time(22)
w = TimePicker(min=t_min, max=t_max)
w.value = t
assert w.value.hour == 14
def test_time_validate_value_vs_max():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(2)
t_max = datetime.time(12)
w = TimePicker(min=t_min, max=t_max)
w.value = t
assert w.value.hour == 12
def test_time_validate_min_vs_value():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(14)
t_max = datetime.time(22)
w = TimePicker(value=t, max=t_max)
w.min = t_min
assert w.value.hour == 14
def test_time_validate_min_vs_max():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(14)
t_max = datetime.time(12)
w = TimePicker(value=t, max=t_max)
with pytest.raises(TraitError):
w.min = t_min
def test_time_validate_max_vs_value():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(2)
t_max = datetime.time(12)
w = TimePicker(value=t, min=t_min)
w.max = t_max
assert w.value.hour == 12
def test_time_validate_max_vs_min():
t = datetime.time(13, 37, 42, 7)
t_min = datetime.time(2)
t_max = datetime.time(1)
w = TimePicker(value=t, min=t_min)
with pytest.raises(TraitError):
w.max = t_max

View File

@@ -0,0 +1,118 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import datetime as dt
from unittest import TestCase
from unittest.mock import MagicMock
from traitlets import TraitError
from ipywidgets import FileUpload
FILE_UPLOAD_FRONTEND_CONTENT = {
'name': 'file-name.txt',
'type': 'text/plain',
'size': 20760,
'last_modified': 1578578296434,
'content': memoryview(b'file content'),
}
class TestFileUpload(TestCase):
def test_construction(self):
uploader = FileUpload()
# Default
assert uploader.accept == ''
assert not uploader.multiple
assert not uploader.disabled
def test_construction_with_params(self):
uploader = FileUpload(
accept='.txt', multiple=True, disabled=True)
assert uploader.accept == '.txt'
assert uploader.multiple
assert uploader.disabled
def test_empty_initial_value(self):
uploader = FileUpload()
assert uploader.value == ()
def test_receive_single_file(self):
uploader = FileUpload()
message = {'value': [FILE_UPLOAD_FRONTEND_CONTENT]}
uploader.set_state(message)
assert len(uploader.value) == 1
(uploaded_file,) = uploader.value
assert uploaded_file.name == 'file-name.txt'
assert uploaded_file.type == 'text/plain'
assert uploaded_file.size == 20760
assert uploaded_file.content.tobytes() == b'file content'
assert (
uploaded_file.last_modified ==
dt.datetime(2020, 1, 9, 13, 58, 16, 434000, tzinfo=dt.timezone.utc)
)
def test_receive_multiple_files(self):
uploader = FileUpload(multiple=True)
message = {
'value': [
FILE_UPLOAD_FRONTEND_CONTENT,
{**FILE_UPLOAD_FRONTEND_CONTENT, **{'name': 'other-file-name.txt'}}
]
}
uploader.set_state(message)
assert len(uploader.value) == 2
assert uploader.value[0].name == 'file-name.txt'
assert uploader.value[1].name == 'other-file-name.txt'
def test_serialization_deserialization_integrity(self):
# The value traitlet needs to remain unchanged following
# a serialization / deserialization roundtrip, otherwise
# the kernel dispatches it back to the frontend following
# a state change, because it doesn't recognize that the
# property_lock entry is the same as the new value.
from ipykernel.comm import Comm
uploader = FileUpload()
mock_comm = MagicMock(spec=Comm)
mock_comm.send = MagicMock()
mock_comm.kernel = 'does not matter'
uploader.comm = mock_comm
message = {'value': [FILE_UPLOAD_FRONTEND_CONTENT]}
uploader.set_state(message)
# Check that no message is sent back to the frontend
# as a result of setting the state.
mock_comm.send.assert_not_called()
def test_resetting_value(self):
# Simulate an upload, then resetting the value from the
# kernel.
uploader = FileUpload()
message = {'value': [FILE_UPLOAD_FRONTEND_CONTENT]}
uploader.set_state(message)
uploader.value = [] # reset value to an empty file list
assert uploader.get_state(key='value') == {'value': []}
def test_setting_non_empty_value(self):
# Simulate user setting a value for the upload from the kernel.
uploader = FileUpload()
content = memoryview(b'some content')
uploader.value = [{
'name': 'some-name.txt',
'type': 'text/plain',
'size': 561,
'last_modified': dt.datetime(2020, 1, 9, 13, 58, 16, 434000, tzinfo=dt.timezone.utc),
'content': content
}]
state = uploader.get_state(key='value')
assert len(state['value']) == 1
[entry] = state['value']
assert entry['name'] == 'some-name.txt'
assert entry['type'] == 'text/plain'
assert entry['size'] == 561
assert entry['last_modified'] == 1578578296434
assert entry['content'] == content

View File

@@ -0,0 +1,97 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from ipywidgets import Widget
import ipywidgets.widgets.widget
# The new comm package is not available in our Python 3.7 CI (older ipykernel version)
try:
import comm
NEW_COMM_PACKAGE = True
except ImportError:
NEW_COMM_PACKAGE = False
import ipykernel.comm
import pytest
class DummyComm():
comm_id = 'a-b-c-d'
kernel = 'Truthy'
def __init__(self, *args, **kwargs):
super().__init__()
self.messages = []
def open(self, *args, **kwargs):
pass
def on_msg(self, *args, **kwargs):
pass
def send(self, *args, **kwargs):
self.messages.append((args, kwargs))
def close(self, *args, **kwargs):
pass
def dummy_create_comm(**kwargs):
return DummyComm()
def dummy_get_comm_manager(**kwargs):
return {}
_widget_attrs = {}
undefined = object()
if NEW_COMM_PACKAGE:
orig_comm = ipykernel.comm.comm.BaseComm
else:
orig_comm = ipykernel.comm.Comm
orig_create_comm = None
orig_get_comm_manager = None
if NEW_COMM_PACKAGE:
orig_create_comm = comm.create_comm
orig_get_comm_manager = comm.get_comm_manager
def setup_test_comm():
if NEW_COMM_PACKAGE:
comm.create_comm = dummy_create_comm
comm.get_comm_manager = dummy_get_comm_manager
ipykernel.comm.comm.BaseComm = DummyComm
else:
ipykernel.comm.Comm = DummyComm
Widget.comm.klass = DummyComm
ipywidgets.widgets.widget.Comm = DummyComm
_widget_attrs['_repr_mimebundle_'] = Widget._repr_mimebundle_
def raise_not_implemented(*args, **kwargs):
raise NotImplementedError()
Widget._repr_mimebundle_ = raise_not_implemented
def teardown_test_comm():
if NEW_COMM_PACKAGE:
comm.create_comm = orig_create_comm
comm.get_comm_manager = orig_get_comm_manager
ipykernel.comm.comm.BaseComm = orig_comm
else:
ipykernel.comm.Comm = orig_comm
Widget.comm.klass = orig_comm
ipywidgets.widgets.widget.Comm = orig_comm
for attr, value in _widget_attrs.items():
if value is undefined:
delattr(Widget, attr)
else:
setattr(Widget, attr, value)
_widget_attrs.clear()
@pytest.fixture(autouse=True)
def setup():
setup_test_comm()
yield
teardown_test_comm()
def call_method(method, *args, **kwargs):
method(*args, **kwargs)

View File

@@ -0,0 +1,465 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""
Trait types for html widgets.
"""
import re
import traitlets
import datetime as dt
_color_names = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beiae', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred ', 'indigo ', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']
# Regex colors #fff and #ffffff
_color_hex = r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?'
_color_hex_re = re.compile(fr'^{_color_hex}$')
# Regex colors #ffff and #ffffffff (includes alpha value)
_color_hexa = r'#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?'
_color_hexa_re = re.compile(fr'^{_color_hexa}$')
# Helpers (float percent, int percent with optional surrounding whitespace)
_color_frac_percent = r'\s*(\d+(\.\d*)?|\.\d+)?%?\s*'
_color_int_percent = r'\s*\d+%?\s*'
# rgb(), rgba(), hsl() and hsla() format strings
_color_rgb = r'rgb\({ip},{ip},{ip}\)'
_color_rgba = r'rgba\({ip},{ip},{ip},{fp}\)'
_color_hsl = r'hsl\({fp},{fp},{fp}\)'
_color_hsla = r'hsla\({fp},{fp},{fp},{fp}\)'
# Regex colors rgb/rgba/hsl/hsla
_color_rgbhsl = '({})|({})|({})|({})'.format(
_color_rgb, _color_rgba, _color_hsl, _color_hsla
).format(ip=_color_int_percent, fp=_color_frac_percent)
_color_rgbhsl_re = re.compile(_color_rgbhsl)
# Support for CSS variables.
# For production rules, see: https://drafts.csswg.org/css-syntax-3/#tokenization
_escape = r'\\([0-9a-fA-F]{1-6}\s?|[^0-9a-fA-F\s])'
_non_ascii = r''.join(
(
r'\u00B7',
r'\u00C0-\u00D6',
r'\u00C0-\u00D6',
r'\u00D8-\u00F6',
r'\u00F8-\u037D',
r'\u037F-\u1FFF',
r'\u200C',
r'\u200D',
r'\u203F',
r'\u2040',
r'\u2070-\u218F',
r'\u2C00-\u2FEF',
r'\u3001-\uD7FF',
r'\uF900-\uFDCF',
r'\uFDF0-\uFFFD',
r'\u10000'
)
)
# Custom CSS identifier
_custom_ident = fr'--([a-zA-Z0-9_\-{_non_ascii}]|{_escape})+'
# Matching for CSS variables with valid color fallback declaration values.
#
# A CSS variable consists of a custom identifier starting with '--'.
# The 'var()' function can be used for substituting the custom property into
# the value of another property.
#
# Here we further restrict the fallback values to be valid colors.
_css_color = fr'({"|".join(_color_names)}|({_color_rgbhsl})|({_color_hex})|({_color_hexa}))'
_css_var_fallback_color = fr'var\({_custom_ident}(,\s*({_css_color}\s*)?)?\)'
_color_var_re = re.compile(_css_var_fallback_color)
class Color(traitlets.Unicode):
"""A string holding a valid HTML color such as 'blue', '#060482', '#A80'"""
info_text = 'a valid HTML color'
default_value = traitlets.Undefined
def validate(self, obj, value):
if value is None and self.allow_none:
return value
if isinstance(value, str):
if (value.lower() in _color_names or _color_hex_re.match(value) or
_color_hexa_re.match(value) or _color_rgbhsl_re.match(value) or
_color_var_re.match(value)):
return value
self.error(obj, value)
class Datetime(traitlets.TraitType):
"""A trait type holding a Python datetime object"""
klass = dt.datetime
default_value = dt.datetime(1900, 1, 1)
class Date(traitlets.TraitType):
"""A trait type holding a Python date object"""
klass = dt.date
default_value = dt.date(1900, 1, 1)
class Time(traitlets.TraitType):
"""A trait type holding a Python time object"""
klass = dt.date
default_value = dt.time()
def datetime_to_json(pydt, manager):
"""Serialize a Python datetime object to json.
Instantiating a JavaScript Date object with a string assumes that the
string is a UTC string, while instantiating it with constructor arguments
assumes that it's in local time:
>>> cdate = new Date('2015-05-12')
Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time)
>>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS
Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time)
Attributes of this dictionary are to be passed to the JavaScript Date
constructor.
"""
if pydt is None:
return None
else:
try:
utcdt = pydt.astimezone(dt.timezone.utc)
except (ValueError, OSError):
# If year is outside valid range for conversion,
# use it as-is
utcdt = pydt
return dict(
year=utcdt.year,
month=utcdt.month - 1, # Months are 0-based indices in JS
date=utcdt.day,
hours=utcdt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=utcdt.minute, # are plural in JS
seconds=utcdt.second,
milliseconds=utcdt.microsecond / 1000,
)
def datetime_from_json(js, manager):
"""Deserialize a Python datetime object from json."""
if js is None:
return None
else:
try:
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
).astimezone()
except (ValueError, OSError):
# If year is outside valid range for conversion,
# return UTC datetime
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
dt.timezone.utc,
)
datetime_serialization = {
'from_json': datetime_from_json,
'to_json': datetime_to_json
}
def naive_to_json(pydt, manager):
"""Serialize a naive Python datetime object to json.
Instantiating a JavaScript Date object with a string assumes that the
string is a UTC string, while instantiating it with constructor arguments
assumes that it's in local time:
>>> cdate = new Date('2015-05-12')
Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time)
>>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS
Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time)
Attributes of this dictionary are to be passed to the JavaScript Date
constructor.
"""
if pydt is None:
return None
else:
naivedt = pydt.replace(tzinfo=None)
return dict(
year=naivedt.year,
month=naivedt.month - 1, # Months are 0-based indices in JS
date=naivedt.day,
hours=naivedt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=naivedt.minute, # are plural in JS
seconds=naivedt.second,
milliseconds=naivedt.microsecond / 1000,
)
def naive_from_json(js, manager):
"""Deserialize a naive Python datetime object from json."""
if js is None:
return None
else:
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
)
naive_serialization = {"from_json": naive_from_json, "to_json": naive_to_json}
def date_to_json(pydate, manager):
"""Serialize a Python date object.
Attributes of this dictionary are to be passed to the JavaScript Date
constructor.
"""
if pydate is None:
return None
else:
return dict(
year=pydate.year,
month=pydate.month - 1, # Months are 0-based indices in JS
date=pydate.day
)
def date_from_json(js, manager):
"""Deserialize a Javascript date."""
if js is None:
return None
else:
return dt.date(
js['year'],
js['month'] + 1, # Months are 1-based in Python
js['date'],
)
date_serialization = {
'from_json': date_from_json,
'to_json': date_to_json
}
class ByteMemoryView(traitlets.TraitType):
"""A trait for memory views of bytes."""
default_value = memoryview(b'')
info_text = 'a memory view object'
def validate(self, obj, value):
if isinstance(value, memoryview) and value.format == 'B':
return value
self.error(obj, value)
def default_value_repr(self):
return repr(self.default_value.tobytes())
class CByteMemoryView(ByteMemoryView):
"""A casting version of the byte memory view trait."""
def validate(self, obj, value):
if isinstance(value, memoryview) and value.format == 'B':
return value
try:
mv = memoryview(value)
if mv.format != 'B':
mv = mv.cast('B')
return mv
except Exception:
self.error(obj, value)
def time_to_json(pyt, manager):
"""Serialize a Python time object to json."""
if pyt is None:
return None
else:
return dict(
hours=pyt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=pyt.minute, # are plural in JS
seconds=pyt.second,
milliseconds=pyt.microsecond / 1000,
)
def time_from_json(js, manager):
"""Deserialize a Python time object from json."""
if js is None:
return None
else:
return dt.time(
js["hours"], js["minutes"], js["seconds"], js["milliseconds"] * 1000
)
time_serialization = {"from_json": time_from_json, "to_json": time_to_json}
def datetime_to_json(pydt, manager):
"""Serialize a Python datetime object to json.
Instantiating a JavaScript Date object with a string assumes that the
string is a UTC string, while instantiating it with constructor arguments
assumes that it's in local time:
>>> cdate = new Date('2015-05-12')
Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time)
>>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS
Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time)
Attributes of this dictionary are to be passed to the JavaScript Date
constructor.
"""
if pydt is None:
return None
else:
try:
utcdt = pydt.astimezone(dt.timezone.utc)
except (ValueError, OSError):
# If year is outside valid range for conversion,
# use it as-is
utcdt = pydt
return dict(
year=utcdt.year,
month=utcdt.month - 1, # Months are 0-based indices in JS
date=utcdt.day,
hours=utcdt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=utcdt.minute, # are plural in JS
seconds=utcdt.second,
milliseconds=utcdt.microsecond / 1000,
)
def datetime_from_json(js, manager):
"""Deserialize a Python datetime object from json."""
if js is None:
return None
else:
try:
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
dt.timezone.utc,
).astimezone()
except (ValueError, OSError):
# If year is outside valid range for conversion,
# return naive datetime
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
dt.timezone.utc,
)
datetime_serialization = {"from_json": datetime_from_json, "to_json": datetime_to_json}
class InstanceDict(traitlets.Instance):
"""An instance trait which coerces a dict to an instance.
This lets the instance be specified as a dict, which is used
to initialize the instance.
Also, we default to a trivial instance, even if args and kwargs
is not specified."""
def validate(self, obj, value):
if isinstance(value, dict):
return super().validate(obj, self.klass(**value))
else:
return super().validate(obj, value)
def make_dynamic_default(self):
return self.klass(*(self.default_args or ()),
**(self.default_kwargs or {}))
# The regexp is taken
# from https://github.com/d3/d3-format/blob/main/src/formatSpecifier.js
_number_format_re = re.compile(r'^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$', re.I)
# The valid types are taken from
# https://github.com/d3/d3-format/blob/main/src/formatTypes.js
_number_format_types = {
'e', 'f', 'g', 'r', 's', '%', 'p', 'b', 'o', 'd', 'x',
'X', 'c', ''
}
class NumberFormat(traitlets.Unicode):
"""A string holding a number format specifier, e.g. '.3f'
This traitlet holds a string that can be passed to the
`d3-format <https://github.com/d3/d3-format>`_ JavaScript library.
The format allowed is similar to the Python format specifier (PEP 3101).
"""
info_text = 'a valid number format'
default_value = traitlets.Undefined
def validate(self, obj, value):
value = super().validate(obj, value)
re_match = _number_format_re.match(value)
if re_match is None:
self.error(obj, value)
else:
format_type = re_match.group(9)
if format_type is None:
return value
elif format_type in _number_format_types:
return value
else:
raise traitlets.TraitError(
'The type specifier of a NumberFormat trait must '
'be one of {}, but a value of \'{}\' was '
'specified.'.format(
list(_number_format_types), format_type)
)
class TypedTuple(traitlets.Container):
"""A trait for a tuple of any length with type-checked elements."""
klass = tuple
_cast_types = (list,)
def bytes_from_json(js, obj):
return None if js is None else js.tobytes()
bytes_serialization = {
'from_json': bytes_from_json,
}

View File

@@ -0,0 +1,64 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from pathlib import Path
import sys
import inspect
import warnings
def _get_frame(level):
"""Get the frame at the given stack level."""
# sys._getframe is much faster than inspect.stack, but isn't guaranteed to
# exist in all python implementations, so we fall back to inspect.stack()
# We need to add one to level to account for this get_frame call.
if hasattr(sys, '_getframe'):
frame = sys._getframe(level+1)
else:
frame = inspect.stack(context=0)[level+1].frame
return frame
# This function is from https://github.com/python/cpython/issues/67998
# (https://bugs.python.org/file39550/deprecated_module_stacklevel.diff) and
# calculates the appropriate stacklevel for deprecations to target the
# deprecation for the caller, no matter how many internal stack frames we have
# added in the process. For example, with the deprecation warning in the
# __init__ below, the appropriate stacklevel will change depending on how deep
# the inheritance hierarchy is.
def _external_stacklevel(internal):
"""Find the stacklevel of the first frame that doesn't contain any of the given internal strings
The depth will be 1 at minimum in order to start checking at the caller of
the function that called this utility method.
"""
# Get the level of my caller's caller
level = 2
frame = _get_frame(level)
# Normalize the path separators:
normalized_internal = [str(Path(s)) for s in internal]
# climb the stack frames while we see internal frames
while frame and any(s in str(Path(frame.f_code.co_filename)) for s in normalized_internal):
level +=1
frame = frame.f_back
# Return the stack level from the perspective of whoever called us (i.e., one level up)
return level-1
def deprecation(message, internal='ipywidgets/widgets/'):
"""Generate a deprecation warning targeting the first frame that is not 'internal'
internal is a string or list of strings, which if they appear in filenames in the
frames, the frames will be considered internal. Changing this can be useful if, for examnple,
we know that ipywidgets is calling out to traitlets internally.
"""
if isinstance(internal, str):
internal = [internal]
# stack level of the first external frame from here
stacklevel = _external_stacklevel(internal)
# The call to .warn adds one frame, so bump the stacklevel up by one
warnings.warn(message, DeprecationWarning, stacklevel=stacklevel+1)

View File

@@ -0,0 +1,27 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Contains the ValueWidget class"""
from .widget import Widget
from traitlets import Any
class ValueWidget(Widget):
"""Widget that can be used for the input of an interactive function"""
value = Any(help="The value of the widget.")
def get_interact_value(self):
"""Return the value for this widget which should be passed to
interactive functions. Custom widgets can change this method
to process the raw value ``self.value``.
"""
return self.value
def _repr_keys(self):
# Ensure value key comes first, and is always present
yield 'value'
for key in super()._repr_keys():
if key != 'value':
yield key

View File

@@ -0,0 +1,851 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Base Widget class. Allows user to create widgets in the back-end that render
in the Jupyter notebook front-end.
"""
import os
import sys
import typing
from contextlib import contextmanager
from collections.abc import Iterable
from IPython import get_ipython
from traitlets import (
Any, HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container,
Undefined)
from json import loads as jsonloads, dumps as jsondumps
from .. import comm
from base64 import standard_b64encode
from .utils import deprecation, _get_frame
from .._version import __protocol_version__, __control_protocol_version__, __jupyter_widgets_base_version__
import inspect
TRAITLETS_FILE = inspect.getfile(HasTraits)
# Based on jupyter_core.paths.envset
def envset(name, default):
"""Return True if the given environment variable is turned on, otherwise False
If the environment variable is set, True will be returned if it is assigned to a value
other than 'no', 'n', 'false', 'off', '0', or '0.0' (case insensitive).
If the environment variable is not set, the default value is returned.
"""
if name in os.environ:
return os.environ[name].lower() not in ['no', 'n', 'false', 'off', '0', '0.0']
else:
return bool(default)
PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0]
CONTROL_PROTOCOL_VERSION_MAJOR = __control_protocol_version__.split('.')[0]
JUPYTER_WIDGETS_ECHO = envset('JUPYTER_WIDGETS_ECHO', default=True)
# we keep a strong reference for every widget created, for a discussion on using weak references see:
# https://github.com/jupyter-widgets/ipywidgets/issues/1345
_instances : typing.MutableMapping[str, "Widget"] = {}
def _widget_to_json(x, obj):
if isinstance(x, dict):
return {k: _widget_to_json(v, obj) for k, v in x.items()}
elif isinstance(x, (list, tuple)):
return [_widget_to_json(v, obj) for v in x]
elif isinstance(x, Widget):
return "IPY_MODEL_" + x.model_id
else:
return x
def _json_to_widget(x, obj):
if isinstance(x, dict):
return {k: _json_to_widget(v, obj) for k, v in x.items()}
elif isinstance(x, (list, tuple)):
return [_json_to_widget(v, obj) for v in x]
elif isinstance(x, str) and x.startswith('IPY_MODEL_') and x[10:] in _instances:
return _instances[x[10:]]
else:
return x
widget_serialization = {
'from_json': _json_to_widget,
'to_json': _widget_to_json
}
_binary_types = (memoryview, bytearray, bytes)
def _put_buffers(state, buffer_paths, buffers):
"""The inverse of _remove_buffers, except here we modify the existing dict/lists.
Modifying should be fine, since this is used when state comes from the wire.
"""
for buffer_path, buffer in zip(buffer_paths, buffers):
# we'd like to set say sync_data['x'][0]['y'] = buffer
# where buffer_path in this example would be ['x', 0, 'y']
obj = state
for key in buffer_path[:-1]:
obj = obj[key]
obj[buffer_path[-1]] = buffer
def _separate_buffers(substate, path, buffer_paths, buffers):
"""For internal, see _remove_buffers"""
# remove binary types from dicts and lists, but keep track of their paths
# any part of the dict/list that needs modification will be cloned, so the original stays untouched
# e.g. {'x': {'ar': ar}, 'y': [ar2, ar3]}, where ar/ar2/ar3 are binary types
# will result in {'x': {}, 'y': [None, None]}, [ar, ar2, ar3], [['x', 'ar'], ['y', 0], ['y', 1]]
# instead of removing elements from the list, this will make replacing the buffers on the js side much easier
if isinstance(substate, (list, tuple)):
is_cloned = False
for i, v in enumerate(substate):
if isinstance(v, _binary_types):
if not is_cloned:
substate = list(substate) # shallow clone list/tuple
is_cloned = True
substate[i] = None
buffers.append(v)
buffer_paths.append(path + [i])
elif isinstance(v, (dict, list, tuple)):
vnew = _separate_buffers(v, path + [i], buffer_paths, buffers)
if v is not vnew: # only assign when value changed
if not is_cloned:
substate = list(substate) # clone list/tuple
is_cloned = True
substate[i] = vnew
elif isinstance(substate, dict):
is_cloned = False
for k, v in substate.items():
if isinstance(v, _binary_types):
if not is_cloned:
substate = dict(substate) # shallow clone dict
is_cloned = True
del substate[k]
buffers.append(v)
buffer_paths.append(path + [k])
elif isinstance(v, (dict, list, tuple)):
vnew = _separate_buffers(v, path + [k], buffer_paths, buffers)
if v is not vnew: # only assign when value changed
if not is_cloned:
substate = dict(substate) # clone list/tuple
is_cloned = True
substate[k] = vnew
else:
raise ValueError("expected state to be a list or dict, not %r" % substate)
return substate
def _remove_buffers(state):
"""Return (state_without_buffers, buffer_paths, buffers) for binary message parts
A binary message part is a memoryview, bytearray, or python 3 bytes object.
As an example:
>>> state = {'plain': [0, 'text'], 'x': {'ar': memoryview(ar1)}, 'y': {'shape': (10,10), 'data': memoryview(ar2)}}
>>> _remove_buffers(state)
({'plain': [0, 'text']}, {'x': {}, 'y': {'shape': (10, 10)}}, [['x', 'ar'], ['y', 'data']],
[<memory at 0x107ffec48>, <memory at 0x107ffed08>])
"""
buffer_paths, buffers = [], []
state = _separate_buffers(state, [], buffer_paths, buffers)
return state, buffer_paths, buffers
def _buffer_list_equal(a, b):
"""Compare two lists of buffers for equality.
Used to decide whether two sequences of buffers (memoryviews,
bytearrays, or python 3 bytes) differ, such that a sync is needed.
Returns True if equal, False if unequal
"""
if len(a) != len(b):
return False
if a == b:
return True
for ia, ib in zip(a, b):
# Check byte equality, since bytes are what is actually synced
# NOTE: Simple ia != ib does not always work as intended, as
# e.g. memoryview(np.frombuffer(ia, dtype='float32')) !=
# memoryview(np.frombuffer(b)), since the format info differs.
# Compare without copying.
if memoryview(ia).cast('B') != memoryview(ib).cast('B'):
return False
return True
class LoggingHasTraits(HasTraits):
"""A parent class for HasTraits that log.
Subclasses have a log trait, and the default behavior
is to get the logger from the currently running Application.
"""
log = Instance('logging.Logger')
@default('log')
def _log_default(self):
from traitlets import log
return log.get_logger()
class CallbackDispatcher(LoggingHasTraits):
"""A structure for registering and running callbacks"""
callbacks = List()
def __call__(self, *args, **kwargs):
"""Call all of the registered callbacks."""
value = None
for callback in self.callbacks:
try:
local_value = callback(*args, **kwargs)
except Exception as e:
ip = get_ipython()
if ip is None:
self.log.warning("Exception in callback %s: %s", callback, e, exc_info=True)
else:
ip.showtraceback()
else:
value = local_value if local_value is not None else value
return value
def register_callback(self, callback, remove=False):
"""(Un)Register a callback
Parameters
----------
callback: method handle
Method to be registered or unregistered.
remove=False: bool
Whether to unregister the callback."""
# (Un)Register the callback.
if remove and callback in self.callbacks:
self.callbacks.remove(callback)
elif not remove and callback not in self.callbacks:
self.callbacks.append(callback)
def _show_traceback(method):
"""decorator for showing tracebacks"""
def m(self, *args, **kwargs):
try:
return(method(self, *args, **kwargs))
except Exception as e:
ip = get_ipython()
if ip is None:
self.log.warning("Exception in widget method %s: %s", method, e, exc_info=True)
else:
ip.showtraceback()
return m
class WidgetRegistry:
def __init__(self):
self._registry = {}
def register(self, model_module, model_module_version_range, model_name, view_module, view_module_version_range, view_name, klass):
"""Register a value"""
model_module = self._registry.setdefault(model_module, {})
model_version = model_module.setdefault(model_module_version_range, {})
model_name = model_version.setdefault(model_name, {})
view_module = model_name.setdefault(view_module, {})
view_version = view_module.setdefault(view_module_version_range, {})
view_version[view_name] = klass
def get(self, model_module, model_module_version, model_name, view_module, view_module_version, view_name):
"""Get a value"""
module_versions = self._registry[model_module]
# The python semver module doesn't work well, for example, it can't do match('3', '*')
# so we just take the first model module version.
#model_names = next(v for k, v in module_versions.items()
# if semver.match(model_module_version, k))
model_names = list(module_versions.values())[0]
view_modules = model_names[model_name]
view_versions = view_modules[view_module]
# The python semver module doesn't work well, so we just take the first view module version
#view_names = next(v for k, v in view_versions.items()
# if semver.match(view_module_version, k))
view_names = list(view_versions.values())[0]
widget_class = view_names[view_name]
return widget_class
def items(self):
for model_module, mm in sorted(self._registry.items()):
for model_version, mv in sorted(mm.items()):
for model_name, vm in sorted(mv.items()):
for view_module, vv in sorted(vm.items()):
for view_version, vn in sorted(vv.items()):
for view_name, widget in sorted(vn.items()):
yield (model_module, model_version, model_name, view_module, view_version, view_name), widget
# a registry of widgets by module, version, and name so we can create a Python model from widgets
# that are constructed from the frontend.
_registry = WidgetRegistry()
def register(widget):
"""A decorator registering a widget class in the widget registry."""
w = widget.class_traits()
_registry.register(w['_model_module'].default_value,
w['_model_module_version'].default_value,
w['_model_name'].default_value,
w['_view_module'].default_value,
w['_view_module_version'].default_value,
w['_view_name'].default_value,
widget)
return widget
class _staticproperty(object):
def __init__(self, fget):
self.fget = fget
def __get__(self, owner_self, owner_cls):
assert owner_self is None
return self.fget()
class Widget(LoggingHasTraits):
#-------------------------------------------------------------------------
# Class attributes
#-------------------------------------------------------------------------
_widget_construction_callback = None
_control_comm = None
@_staticproperty
def widgets():
# Because this is a static attribute, it will be accessed when initializing this class. In that case, since a user
# did not explicitly try to use this attribute, we do not want to throw a deprecation warning.
# So we check if the thing calling this static property is one of the known initialization functions in traitlets.
frame = _get_frame(2)
if not (frame.f_code.co_filename == TRAITLETS_FILE and (frame.f_code.co_name in ('getmembers', 'setup_instance', 'setup_class'))):
deprecation("Widget.widgets is deprecated.")
return _instances
@_staticproperty
def _active_widgets():
# Because this is a static attribute, it will be accessed when initializing this class. In that case, since a user
# did not explicitly try to use this attribute, we do not want to throw a deprecation warning.
# So we check if the thing calling this static property is one of the known initialization functions in traitlets.
frame = _get_frame(2)
if not (frame.f_code.co_filename == TRAITLETS_FILE and (frame.f_code.co_name in ('getmembers', 'setup_instance', 'setup_class'))):
deprecation("Widget._active_widgets is deprecated.")
return _instances
@_staticproperty
def _widget_types():
# Because this is a static attribute, it will be accessed when initializing this class. In that case, since a user
# did not explicitly try to use this attribute, we do not want to throw a deprecation warning.
# So we check if the thing calling this static property is one of the known initialization functions in traitlets.
frame = _get_frame(2)
if not (frame.f_code.co_filename == TRAITLETS_FILE and (frame.f_code.co_name in ('getmembers', 'setup_instance', 'setup_class'))):
deprecation("Widget._widget_types is deprecated.")
return _registry
@_staticproperty
def widget_types():
# Because this is a static attribute, it will be accessed when initializing this class. In that case, since a user
# did not explicitly try to use this attribute, we do not want to throw a deprecation warning.
# So we check if the thing calling this static property is one of the known initialization functions in traitlets.
frame = _get_frame(2)
if not (frame.f_code.co_filename == TRAITLETS_FILE and (frame.f_code.co_name in ('getmembers', 'setup_instance', 'setup_class'))):
deprecation("Widget.widget_types is deprecated.")
return _registry
@classmethod
def close_all(cls):
for widget in list(_instances.values()):
widget.close()
@staticmethod
def on_widget_constructed(callback):
"""Registers a callback to be called when a widget is constructed.
The callback must have the following signature:
callback(widget)"""
Widget._widget_construction_callback = callback
@staticmethod
def _call_widget_constructed(widget):
"""Static method, called when a widget is constructed."""
if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
Widget._widget_construction_callback(widget)
@classmethod
def handle_control_comm_opened(cls, comm, msg):
"""
Class method, called when the comm-open message on the
"jupyter.widget.control" comm channel is received
"""
version = msg.get('metadata', {}).get('version', '')
if version.split('.')[0] != CONTROL_PROTOCOL_VERSION_MAJOR:
raise ValueError("Incompatible widget control protocol versions: received version %r, expected version %r"%(version, __control_protocol_version__))
cls._control_comm = comm
cls._control_comm.on_msg(cls._handle_control_comm_msg)
@classmethod
def _handle_control_comm_msg(cls, msg):
# This shouldn't happen unless someone calls this method manually
if cls._control_comm is None:
raise RuntimeError('Control comm has not been properly opened')
data = msg['content']['data']
method = data['method']
if method == 'request_states':
# Send back the full widgets state
cls.get_manager_state()
widgets = _instances.values()
full_state = {}
drop_defaults = False
for widget in widgets:
full_state[widget.model_id] = {
'model_name': widget._model_name,
'model_module': widget._model_module,
'model_module_version': widget._model_module_version,
'state': widget.get_state(drop_defaults=drop_defaults),
}
full_state, buffer_paths, buffers = _remove_buffers(full_state)
cls._control_comm.send(dict(
method='update_states',
states=full_state,
buffer_paths=buffer_paths
), buffers=buffers)
else:
raise RuntimeError('Unknown front-end to back-end widget control msg with method "%s"' % method)
@staticmethod
def handle_comm_opened(comm, msg):
"""Static method, called when a widget is constructed."""
version = msg.get('metadata', {}).get('version', '')
if version.split('.')[0] != PROTOCOL_VERSION_MAJOR:
raise ValueError("Incompatible widget protocol versions: received version %r, expected version %r"%(version, __protocol_version__))
data = msg['content']['data']
state = data['state']
# Find the widget class to instantiate in the registered widgets
widget_class = _registry.get(state['_model_module'],
state['_model_module_version'],
state['_model_name'],
state['_view_module'],
state['_view_module_version'],
state['_view_name'])
widget = widget_class(comm=comm)
if 'buffer_paths' in data:
_put_buffers(state, data['buffer_paths'], msg['buffers'])
widget.set_state(state)
@staticmethod
def get_manager_state(drop_defaults=False, widgets=None):
"""Returns the full state for a widget manager for embedding
:param drop_defaults: when True, it will not include default value
:param widgets: list with widgets to include in the state (or all widgets when None)
:return:
"""
state = {}
if widgets is None:
widgets = _instances.values()
for widget in widgets:
state[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults)
return {'version_major': 2, 'version_minor': 0, 'state': state}
def _get_embed_state(self, drop_defaults=False):
state = {
'model_name': self._model_name,
'model_module': self._model_module,
'model_module_version': self._model_module_version
}
model_state, buffer_paths, buffers = _remove_buffers(self.get_state(drop_defaults=drop_defaults))
state['state'] = model_state
if len(buffers) > 0:
state['buffers'] = [{'encoding': 'base64',
'path': p,
'data': standard_b64encode(d).decode('ascii')}
for p, d in zip(buffer_paths, buffers)]
return state
def get_view_spec(self):
return dict(version_major=2, version_minor=0, model_id=self._model_id)
#-------------------------------------------------------------------------
# Traits
#-------------------------------------------------------------------------
_model_name = Unicode('WidgetModel',
help="Name of the model.", read_only=True).tag(sync=True)
_model_module = Unicode('@jupyter-widgets/base',
help="The namespace for the model.", read_only=True).tag(sync=True)
_model_module_version = Unicode(__jupyter_widgets_base_version__,
help="A semver requirement for namespace version containing the model.", read_only=True).tag(sync=True)
_view_name = Unicode(None, allow_none=True,
help="Name of the view.").tag(sync=True)
_view_module = Unicode(None, allow_none=True,
help="The namespace for the view.").tag(sync=True)
_view_module_version = Unicode('',
help="A semver requirement for the namespace version containing the view.").tag(sync=True)
_view_count = Int(None, allow_none=True,
help="EXPERIMENTAL: The number of views of the model displayed in the frontend. This attribute is experimental and may change or be removed in the future. None signifies that views will not be tracked. Set this to 0 to start tracking view creation/deletion.").tag(sync=True)
comm = Any(allow_none=True)
keys = List(help="The traits which are synced.")
@default('keys')
def _default_keys(self):
return [name for name in self.traits(sync=True)]
_property_lock = Dict()
_holding_sync = False
_states_to_send = Set()
_msg_callbacks = Instance(CallbackDispatcher, ())
#-------------------------------------------------------------------------
# (Con/de)structor
#-------------------------------------------------------------------------
def __init__(self, **kwargs):
"""Public constructor"""
self._model_id = kwargs.pop('model_id', None)
super().__init__(**kwargs)
Widget._call_widget_constructed(self)
self.open()
def __copy__(self):
raise NotImplementedError("Widgets cannot be copied; custom implementation required")
def __deepcopy__(self, memo):
raise NotImplementedError("Widgets cannot be copied; custom implementation required")
def __del__(self):
"""Object disposal"""
self.close()
#-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
def open(self):
"""Open a comm to the frontend if one isn't already open."""
if self.comm is None:
state, buffer_paths, buffers = _remove_buffers(self.get_state())
args = dict(target_name='jupyter.widget',
data={'state': state, 'buffer_paths': buffer_paths},
buffers=buffers,
metadata={'version': __protocol_version__}
)
if self._model_id is not None:
args['comm_id'] = self._model_id
self.comm = comm.create_comm(**args)
@observe('comm')
def _comm_changed(self, change):
"""Called when the comm is changed."""
if change['new'] is None:
return
self._model_id = self.model_id
self.comm.on_msg(self._handle_msg)
_instances[self.model_id] = self
@property
def model_id(self):
"""Gets the model id of this widget.
If a Comm doesn't exist yet, a Comm will be created automagically."""
return self.comm.comm_id
#-------------------------------------------------------------------------
# Methods
#-------------------------------------------------------------------------
def close(self):
"""Close method.
Closes the underlying comm.
When the comm is closed, all of the widget views are automatically
removed from the front-end."""
if self.comm is not None:
_instances.pop(self.model_id, None)
self.comm.close()
self.comm = None
self._repr_mimebundle_ = None
def send_state(self, key=None):
"""Sends the widget state, or a piece of it, to the front-end, if it exists.
Parameters
----------
key : unicode, or iterable (optional)
A single property's name or iterable of property names to sync with the front-end.
"""
state = self.get_state(key=key)
if len(state) > 0:
if self._property_lock: # we need to keep this dict up to date with the front-end values
for name, value in state.items():
if name in self._property_lock:
self._property_lock[name] = value
state, buffer_paths, buffers = _remove_buffers(state)
msg = {'method': 'update', 'state': state, 'buffer_paths': buffer_paths}
self._send(msg, buffers=buffers)
def get_state(self, key=None, drop_defaults=False):
"""Gets the widget state, or a piece of it.
Parameters
----------
key : unicode or iterable (optional)
A single property's name or iterable of property names to get.
Returns
-------
state : dict of states
metadata : dict
metadata for each field: {key: metadata}
"""
if key is None:
keys = self.keys
elif isinstance(key, str):
keys = [key]
elif isinstance(key, Iterable):
keys = key
else:
raise ValueError("key must be a string, an iterable of keys, or None")
state = {}
traits = self.traits()
for k in keys:
to_json = self.trait_metadata(k, 'to_json', self._trait_to_json)
value = to_json(getattr(self, k), self)
if not drop_defaults or not self._compare(value, traits[k].default_value):
state[k] = value
return state
def _is_numpy(self, x):
return x.__class__.__name__ == 'ndarray' and x.__class__.__module__ == 'numpy'
def _compare(self, a, b):
if self._is_numpy(a) or self._is_numpy(b):
import numpy as np
return np.array_equal(a, b)
else:
return a == b
def set_state(self, sync_data):
"""Called when a state is received from the front-end."""
# Send an echo update message immediately
if JUPYTER_WIDGETS_ECHO:
echo_state = {}
for attr, value in sync_data.items():
if attr in self.keys and self.trait_metadata(attr, 'echo_update', default=True):
echo_state[attr] = value
if echo_state:
echo_state, echo_buffer_paths, echo_buffers = _remove_buffers(echo_state)
msg = {
'method': 'echo_update',
'state': echo_state,
'buffer_paths': echo_buffer_paths,
}
self._send(msg, buffers=echo_buffers)
# The order of these context managers is important. Properties must
# be locked when the hold_trait_notification context manager is
# released and notifications are fired.
with self._lock_property(**sync_data), self.hold_trait_notifications():
for name in sync_data:
if name in self.keys:
from_json = self.trait_metadata(name, 'from_json',
self._trait_from_json)
self.set_trait(name, from_json(sync_data[name], self))
def send(self, content, buffers=None):
"""Sends a custom msg to the widget model in the front-end.
Parameters
----------
content : dict
Content of the message to send.
buffers : list of binary buffers
Binary buffers to send with message
"""
self._send({"method": "custom", "content": content}, buffers=buffers)
def on_msg(self, callback, remove=False):
"""(Un)Register a custom msg receive callback.
Parameters
----------
callback: callable
callback will be passed three arguments when a message arrives::
callback(widget, content, buffers)
remove: bool
True if the callback should be unregistered."""
self._msg_callbacks.register_callback(callback, remove=remove)
def add_traits(self, **traits):
"""Dynamically add trait attributes to the Widget."""
super().add_traits(**traits)
for name, trait in traits.items():
if 'sync' in trait.metadata:
self.keys.append(name)
self.send_state(name)
def notify_change(self, change):
"""Called when a property has changed."""
# Send the state to the frontend before the user-registered callbacks
# are called.
name = change['name']
if self.comm is not None and getattr(self.comm, 'kernel', True) is not None:
# Make sure this isn't information that the front-end just sent us.
if name in self.keys and self._should_send_property(name, getattr(self, name)):
# Send new state to front-end
self.send_state(key=name)
super().notify_change(change)
def __repr__(self):
return self._gen_repr_from_keys(self._repr_keys())
#-------------------------------------------------------------------------
# Support methods
#-------------------------------------------------------------------------
@contextmanager
def _lock_property(self, **properties):
"""Lock a property-value pair.
The value should be the JSON state of the property.
NOTE: This, in addition to the single lock for all state changes, is
flawed. In the future we may want to look into buffering state changes
back to the front-end."""
self._property_lock = properties
try:
yield
finally:
self._property_lock = {}
@contextmanager
def hold_sync(self):
"""Hold syncing any state until the outermost context manager exits"""
if self._holding_sync is True:
yield
else:
try:
self._holding_sync = True
yield
finally:
self._holding_sync = False
self.send_state(self._states_to_send)
self._states_to_send.clear()
def _should_send_property(self, key, value):
"""Check the property lock (property_lock)"""
to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
if key in self._property_lock:
# model_state, buffer_paths, buffers
split_value = _remove_buffers({ key: to_json(value, self)})
split_lock = _remove_buffers({ key: self._property_lock[key]})
# A roundtrip conversion through json in the comparison takes care of
# idiosyncracies of how python data structures map to json, for example
# tuples get converted to lists.
if (jsonloads(jsondumps(split_value[0])) == split_lock[0]
and split_value[1] == split_lock[1]
and _buffer_list_equal(split_value[2], split_lock[2])):
if self._holding_sync:
self._states_to_send.discard(key)
return False
if self._holding_sync:
self._states_to_send.add(key)
return False
else:
return True
# Event handlers
@_show_traceback
def _handle_msg(self, msg):
"""Called when a msg is received from the front-end"""
data = msg['content']['data']
method = data['method']
if method == 'update':
if 'state' in data:
state = data['state']
if 'buffer_paths' in data:
_put_buffers(state, data['buffer_paths'], msg['buffers'])
self.set_state(state)
# Handle a state request.
elif method == 'request_state':
self.send_state()
# Handle a custom msg from the front-end.
elif method == 'custom':
if 'content' in data:
self._handle_custom_msg(data['content'], msg['buffers'])
# Catch remainder.
else:
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
def _handle_custom_msg(self, content, buffers):
"""Called when a custom msg is received."""
self._msg_callbacks(self, content, buffers)
@staticmethod
def _trait_to_json(x, self):
"""Convert a trait value to json."""
return x
@staticmethod
def _trait_from_json(x, self):
"""Convert json values to objects."""
return x
def _repr_mimebundle_(self, **kwargs):
plaintext = repr(self)
if len(plaintext) > 110:
plaintext = plaintext[:110] + ''
data = {
'text/plain': plaintext,
}
if self._view_name is not None:
# The 'application/vnd.jupyter.widget-view+json' mimetype has not been registered yet.
# See the registration process and naming convention at
# http://tools.ietf.org/html/rfc6838
# and the currently registered mimetypes at
# http://www.iana.org/assignments/media-types/media-types.xhtml.
data['application/vnd.jupyter.widget-view+json'] = {
'version_major': 2,
'version_minor': 0,
'model_id': self._model_id
}
return data
def _send(self, msg, buffers=None):
"""Sends a message to the model in the front-end."""
if self.comm is not None and (self.comm.kernel is not None if hasattr(self.comm, "kernel") else True):
self.comm.send(data=msg, buffers=buffers)
def _repr_keys(self):
traits = self.traits()
for key in sorted(self.keys):
# Exclude traits that start with an underscore
if key[0] == '_':
continue
# Exclude traits who are equal to their default value
value = getattr(self, key)
trait = traits[key]
if self._compare(value, trait.default_value):
continue
elif (isinstance(trait, (Container, Dict)) and
trait.default_value == Undefined and
(value is None or len(value) == 0)):
# Empty container, and dynamic default will be empty
continue
yield key
def _gen_repr_from_keys(self, keys):
class_name = self.__class__.__name__
signature = ', '.join(
'{}={!r}'.format(key, getattr(self, key))
for key in keys
)
return '{}({})'.format(class_name, signature)

View File

@@ -0,0 +1,110 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Bool class.
Represents a boolean using a widget.
"""
from .widget_description import DescriptionStyle, DescriptionWidget
from .widget_core import CoreWidget
from .valuewidget import ValueWidget
from .widget import register, widget_serialization
from .trait_types import Color, InstanceDict
from traitlets import Unicode, Bool, CaselessStrEnum
@register
class CheckboxStyle(DescriptionStyle, CoreWidget):
"""Checkbox widget style."""
_model_name = Unicode('CheckboxStyleModel').tag(sync=True)
background = Unicode(None, allow_none=True, help="Background specifications.").tag(sync=True)
@register
class ToggleButtonStyle(DescriptionStyle, CoreWidget):
"""ToggleButton widget style."""
_model_name = Unicode('ToggleButtonStyleModel').tag(sync=True)
font_family = Unicode(None, allow_none=True, help="Toggle button text font family.").tag(sync=True)
font_size = Unicode(None, allow_none=True, help="Toggle button text font size.").tag(sync=True)
font_style = Unicode(None, allow_none=True, help="Toggle button text font style.").tag(sync=True)
font_variant = Unicode(None, allow_none=True, help="Toggle button text font variant.").tag(sync=True)
font_weight = Unicode(None, allow_none=True, help="Toggle button text font weight.").tag(sync=True)
text_color = Color(None, allow_none=True, help="Toggle button text color").tag(sync=True)
text_decoration = Unicode(None, allow_none=True, help="Toggle button text decoration.").tag(sync=True)
class _Bool(DescriptionWidget, ValueWidget, CoreWidget):
"""A base class for creating widgets that represent booleans."""
value = Bool(False, help="Bool value").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
def __init__(self, value=None, **kwargs):
if value is not None:
kwargs['value'] = value
super().__init__(**kwargs)
_model_name = Unicode('BoolModel').tag(sync=True)
@register
class Checkbox(_Bool):
"""Displays a boolean `value` in the form of a checkbox.
Parameters
----------
value : {True,False}
value of the checkbox: True-checked, False-unchecked
description : str
description displayed next to the checkbox
indent : {True,False}
indent the control to align with other controls with a description. The style.description_width attribute controls this width for consistence with other controls.
"""
_view_name = Unicode('CheckboxView').tag(sync=True)
_model_name = Unicode('CheckboxModel').tag(sync=True)
indent = Bool(True, help="Indent the control to align with other controls with a description.").tag(sync=True)
style = InstanceDict(CheckboxStyle, help="Styling customizations").tag(sync=True, **widget_serialization)
@register
class ToggleButton(_Bool):
"""Displays a boolean `value` in the form of a toggle button.
Parameters
----------
value : {True,False}
value of the toggle button: True-pressed, False-unpressed
description : str
description displayed on the button
icon: str
font-awesome icon name
style: instance of DescriptionStyle
styling customizations
button_style: enum
button predefined styling
"""
_view_name = Unicode('ToggleButtonView').tag(sync=True)
_model_name = Unicode('ToggleButtonModel').tag(sync=True)
icon = Unicode('', help= "Font-awesome icon.").tag(sync=True)
button_style = CaselessStrEnum(
values=['primary', 'success', 'info', 'warning', 'danger', ''], default_value='',
help="""Use a predefined styling for the button.""").tag(sync=True)
style = InstanceDict(ToggleButtonStyle, help="Styling customizations").tag(sync=True, **widget_serialization)
@register
class Valid(_Bool):
"""Displays a boolean `value` in the form of a green check (True / valid)
or a red cross (False / invalid).
Parameters
----------
value: {True,False}
value of the Valid widget
"""
readout = Unicode('Invalid', help="Message displayed when the value is False").tag(sync=True)
_view_name = Unicode('ValidView').tag(sync=True)
_model_name = Unicode('ValidModel').tag(sync=True)

View File

@@ -0,0 +1,126 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Box widgets.
These widgets are containers that can be used to
group other widgets together and control their
relative layouts.
"""
from .widget import register, widget_serialization, Widget
from .domwidget import DOMWidget
from .widget_core import CoreWidget
from .docutils import doc_subst
from .trait_types import TypedTuple
from traitlets import Unicode, CaselessStrEnum, Instance
_doc_snippets = {}
_doc_snippets['box_params'] = """
children: iterable of Widget instances
list of widgets to display
box_style: str
one of 'success', 'info', 'warning' or 'danger', or ''.
Applies a predefined style to the box. Defaults to '',
which applies no pre-defined style.
"""
@register
@doc_subst(_doc_snippets)
class Box(DOMWidget, CoreWidget):
""" Displays multiple widgets in a group.
The widgets are laid out horizontally.
Parameters
----------
{box_params}
Examples
--------
>>> import ipywidgets as widgets
>>> title_widget = widgets.HTML('<em>Box Example</em>')
>>> slider = widgets.IntSlider()
>>> widgets.Box([title_widget, slider])
"""
_model_name = Unicode('BoxModel').tag(sync=True)
_view_name = Unicode('BoxView').tag(sync=True)
# Child widgets in the container.
# Using a tuple here to force reassignment to update the list.
# When a proper notifying-list trait exists, use that instead.
children = TypedTuple(trait=Instance(Widget), help="List of widget children").tag(
sync=True, **widget_serialization)
box_style = CaselessStrEnum(
values=['success', 'info', 'warning', 'danger', ''], default_value='',
help="""Use a predefined styling for the box.""").tag(sync=True)
def __init__(self, children=(), **kwargs):
kwargs['children'] = children
super().__init__(**kwargs)
@register
@doc_subst(_doc_snippets)
class VBox(Box):
""" Displays multiple widgets vertically using the flexible box model.
Parameters
----------
{box_params}
Examples
--------
>>> import ipywidgets as widgets
>>> title_widget = widgets.HTML('<em>Vertical Box Example</em>')
>>> slider = widgets.IntSlider()
>>> widgets.VBox([title_widget, slider])
"""
_model_name = Unicode('VBoxModel').tag(sync=True)
_view_name = Unicode('VBoxView').tag(sync=True)
@register
@doc_subst(_doc_snippets)
class HBox(Box):
""" Displays multiple widgets horizontally using the flexible box model.
Parameters
----------
{box_params}
Examples
--------
>>> import ipywidgets as widgets
>>> title_widget = widgets.HTML('<em>Horizontal Box Example</em>')
>>> slider = widgets.IntSlider()
>>> widgets.HBox([title_widget, slider])
"""
_model_name = Unicode('HBoxModel').tag(sync=True)
_view_name = Unicode('HBoxView').tag(sync=True)
@register
class GridBox(Box):
""" Displays multiple widgets in rows and columns using the grid box model.
Parameters
----------
{box_params}
Examples
--------
>>> import ipywidgets as widgets
>>> title_widget = widgets.HTML('<em>Grid Box Example</em>')
>>> slider = widgets.IntSlider()
>>> button1 = widgets.Button(description='1')
>>> button2 = widgets.Button(description='2')
>>> # Create a grid with two columns, splitting space equally
>>> layout = widgets.Layout(grid_template_columns='1fr 1fr')
>>> widgets.GridBox([title_widget, slider, button1, button2], layout=layout)
"""
_model_name = Unicode('GridBoxModel').tag(sync=True)
_view_name = Unicode('GridBoxView').tag(sync=True)

View File

@@ -0,0 +1,109 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Button class.
Represents a button in the frontend using a widget. Allows user to listen for
click events on the button and trigger backend code when the clicks are fired.
"""
from .utils import deprecation
from .domwidget import DOMWidget
from .widget import CallbackDispatcher, register, widget_serialization
from .widget_core import CoreWidget
from .widget_style import Style
from .trait_types import Color, InstanceDict
from traitlets import Unicode, Bool, CaselessStrEnum, Instance, validate, default
@register
class ButtonStyle(Style, CoreWidget):
"""Button style widget."""
_model_name = Unicode('ButtonStyleModel').tag(sync=True)
button_color = Color(None, allow_none=True, help="Color of the button").tag(sync=True)
font_family = Unicode(None, allow_none=True, help="Button text font family.").tag(sync=True)
font_size = Unicode(None, allow_none=True, help="Button text font size.").tag(sync=True)
font_style = Unicode(None, allow_none=True, help="Button text font style.").tag(sync=True)
font_variant = Unicode(None, allow_none=True, help="Button text font variant.").tag(sync=True)
font_weight = Unicode(None, allow_none=True, help="Button text font weight.").tag(sync=True)
text_color = Unicode(None, allow_none=True, help="Button text color.").tag(sync=True)
text_decoration = Unicode(None, allow_none=True, help="Button text decoration.").tag(sync=True)
@register
class Button(DOMWidget, CoreWidget):
"""Button widget.
This widget has an `on_click` method that allows you to listen for the
user clicking on the button. The click event itself is stateless.
Parameters
----------
description: str
description displayed on the button
icon: str
font-awesome icon names, without the 'fa-' prefix
disabled: bool
whether user interaction is enabled
"""
_view_name = Unicode('ButtonView').tag(sync=True)
_model_name = Unicode('ButtonModel').tag(sync=True)
description = Unicode(help="Button label.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
icon = Unicode('', help="Font-awesome icon names, without the 'fa-' prefix.").tag(sync=True)
button_style = CaselessStrEnum(
values=['primary', 'success', 'info', 'warning', 'danger', ''], default_value='',
help="""Use a predefined styling for the button.""").tag(sync=True)
style = InstanceDict(ButtonStyle).tag(sync=True, **widget_serialization)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._click_handlers = CallbackDispatcher()
self.on_msg(self._handle_button_msg)
@validate('icon')
def _validate_icon(self, proposal):
"""Strip 'fa-' if necessary'"""
value = proposal['value']
if 'fa-' in value:
deprecation("icons names no longer need 'fa-', "
"just use the class names themselves (for example, 'gear spin' instead of 'fa-gear fa-spin')",
internal=['ipywidgets/widgets/', 'traitlets/traitlets.py', '/contextlib.py'])
value = value.replace('fa-', '')
return value
def on_click(self, callback, remove=False):
"""Register a callback to execute when the button is clicked.
The callback will be called with one argument, the clicked button
widget instance.
Parameters
----------
remove: bool (optional)
Set to true to remove the callback from the list of callbacks.
"""
self._click_handlers.register_callback(callback, remove=remove)
def click(self):
"""Programmatically trigger a click event.
This will call the callbacks registered to the clicked button
widget instance.
"""
self._click_handlers(self)
def _handle_button_msg(self, _, content, buffers):
"""Handle a msg from the front-end.
Parameters
----------
content: dict
Content of the msg.
"""
if content.get('event', '') == 'click':
self.click()

View File

@@ -0,0 +1,24 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Color class.
Represents an HTML Color .
"""
from .widget_description import DescriptionWidget
from .valuewidget import ValueWidget
from .widget import register
from .widget_core import CoreWidget
from .trait_types import Color
from traitlets import Unicode, Bool
@register
class ColorPicker(DescriptionWidget, ValueWidget, CoreWidget):
value = Color('black', help="The color value.").tag(sync=True)
concise = Bool(help="Display short version with just a color selector.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
_view_name = Unicode('ColorPickerView').tag(sync=True)
_model_name = Unicode('ColorPickerModel').tag(sync=True)

View File

@@ -0,0 +1,53 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Controller class.
Represents a Gamepad or Joystick controller.
"""
from .valuewidget import ValueWidget
from .widget import register, widget_serialization
from .domwidget import DOMWidget
from .widget_core import CoreWidget
from .trait_types import TypedTuple
from traitlets import Bool, Int, Float, Unicode, Instance
@register
class Button(DOMWidget, ValueWidget, CoreWidget):
"""Represents a gamepad or joystick button."""
value = Float(min=0.0, max=1.0, read_only=True, help="The value of the button.").tag(sync=True)
pressed = Bool(read_only=True, help="Whether the button is pressed.").tag(sync=True)
_view_name = Unicode('ControllerButtonView').tag(sync=True)
_model_name = Unicode('ControllerButtonModel').tag(sync=True)
@register
class Axis(DOMWidget, ValueWidget, CoreWidget):
"""Represents a gamepad or joystick axis."""
value = Float(min=-1.0, max=1.0, read_only=True, help="The value of the axis.").tag(sync=True)
_view_name = Unicode('ControllerAxisView').tag(sync=True)
_model_name = Unicode('ControllerAxisModel').tag(sync=True)
@register
class Controller(DOMWidget, CoreWidget):
"""Represents a game controller."""
index = Int(help="The id number of the controller.").tag(sync=True)
# General information about the gamepad, button and axes mapping, name.
# These values are all read-only and set by the JavaScript side.
name = Unicode(read_only=True, help="The name of the controller.").tag(sync=True)
mapping = Unicode(read_only=True, help="The name of the control mapping.").tag(sync=True)
connected = Bool(read_only=True, help="Whether the gamepad is connected.").tag(sync=True)
timestamp = Float(read_only=True, help="The last time the data from this gamepad was updated.").tag(sync=True)
# Buttons and axes - read-only
buttons = TypedTuple(trait=Instance(Button), read_only=True, help="The buttons on the gamepad.").tag(sync=True, **widget_serialization)
axes = TypedTuple(trait=Instance(Axis), read_only=True, help="The axes on the gamepad.").tag(sync=True, **widget_serialization)
_view_name = Unicode('ControllerView').tag(sync=True)
_model_name = Unicode('ControllerModel').tag(sync=True)

View File

@@ -0,0 +1,16 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Base widget class for widgets provided in Core"""
from .widget import Widget
from .._version import __jupyter_widgets_controls_version__
from traitlets import Unicode
class CoreWidget(Widget):
_model_module = Unicode('@jupyter-widgets/controls').tag(sync=True)
_model_module_version = Unicode(__jupyter_widgets_controls_version__).tag(sync=True)
_view_module = Unicode('@jupyter-widgets/controls').tag(sync=True)
_view_module_version = Unicode(__jupyter_widgets_controls_version__).tag(sync=True)

View File

@@ -0,0 +1,87 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Color class.
Represents an HTML Color .
"""
from .widget_description import DescriptionWidget
from .valuewidget import ValueWidget
from .widget import register
from .widget_core import CoreWidget
from .trait_types import Date, date_serialization
from traitlets import Unicode, Bool, Union, CInt, CaselessStrEnum, TraitError, validate
@register
class DatePicker(DescriptionWidget, ValueWidget, CoreWidget):
"""
Display a widget for picking dates.
Parameters
----------
value: datetime.date
The current value of the widget.
disabled: bool
Whether to disable user changes.
Examples
--------
>>> import datetime
>>> import ipywidgets as widgets
>>> date_pick = widgets.DatePicker()
>>> date_pick.value = datetime.date(2019, 7, 9)
"""
_view_name = Unicode('DatePickerView').tag(sync=True)
_model_name = Unicode('DatePickerModel').tag(sync=True)
value = Date(None, allow_none=True).tag(sync=True, **date_serialization)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
min = Date(None, allow_none=True).tag(sync=True, **date_serialization)
max = Date(None, allow_none=True).tag(sync=True, **date_serialization)
step = Union(
(CInt(1), CaselessStrEnum(["any"])),
help='The date step to use for the picker, in days, or "any".',
).tag(sync=True)
@validate("value")
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal["value"]
if value is None:
return value
if self.min and self.min > value:
value = max(value, self.min)
if self.max and self.max < value:
value = min(value, self.max)
return value
@validate("min")
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal["value"]
if min is None:
return min
if self.max and min > self.max:
raise TraitError("Setting min > max")
if self.value and min > self.value:
self.value = min
return min
@validate("max")
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal["value"]
if max is None:
return max
if self.min and max < self.min:
raise TraitError("setting max < min")
if self.value and max < self.value:
self.value = max
return max

View File

@@ -0,0 +1,141 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""
Time and datetime picker widgets
"""
from traitlets import Unicode, Bool, validate, TraitError
from .trait_types import datetime_serialization, Datetime, naive_serialization
from .valuewidget import ValueWidget
from .widget import register
from .widget_core import CoreWidget
from .widget_description import DescriptionWidget
@register
class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget):
"""
Display a widget for picking datetimes.
Parameters
----------
value: datetime.datetime
The current value of the widget.
disabled: bool
Whether to disable user changes.
min: datetime.datetime
The lower allowed datetime bound
max: datetime.datetime
The upper allowed datetime bound
Examples
--------
>>> import datetime
>>> import ipydatetime
>>> datetime_pick = ipydatetime.DatetimePicker()
>>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3)
"""
_view_name = Unicode("DatetimeView").tag(sync=True)
_model_name = Unicode("DatetimeModel").tag(sync=True)
value = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
min = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization)
max = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization)
def _validate_tz(self, value):
if value.tzinfo is None:
raise TraitError('%s values needs to be timezone aware' % (self.__class__.__name__,))
return value
@validate("value")
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal["value"]
if value is None:
return value
value = self._validate_tz(value)
if self.min and self.min > value:
value = max(value, self.min)
if self.max and self.max < value:
value = min(value, self.max)
return value
@validate("min")
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal["value"]
if min is None:
return min
min = self._validate_tz(min)
if self.max and min > self.max:
raise TraitError("Setting min > max")
if self.value and min > self.value:
self.value = min
return min
@validate("max")
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal["value"]
if max is None:
return max
max = self._validate_tz(max)
if self.min and max < self.min:
raise TraitError("setting max < min")
if self.value and max < self.value:
self.value = max
return max
@register
class NaiveDatetimePicker(DatetimePicker):
"""
Display a widget for picking naive datetimes (i.e. timezone unaware).
Parameters
----------
value: datetime.datetime
The current value of the widget.
disabled: bool
Whether to disable user changes.
min: datetime.datetime
The lower allowed datetime bound
max: datetime.datetime
The upper allowed datetime bound
Examples
--------
>>> import datetime
>>> import ipydatetime
>>> datetime_pick = ipydatetime.NaiveDatetimePicker()
>>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3)
"""
# Replace the serializers and model names:
_model_name = Unicode("NaiveDatetimeModel").tag(sync=True)
value = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)
min = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)
max = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)
def _validate_tz(self, value):
if value.tzinfo is not None:
raise TraitError('%s values needs to be timezone unaware' % (self.__class__.__name__,))
return value

View File

@@ -0,0 +1,58 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Contains the DOMWidget class"""
from traitlets import Bool, Unicode
from .widget import Widget, widget_serialization, register
from .trait_types import InstanceDict
from .widget_style import Style
from .widget_core import CoreWidget
from .domwidget import DOMWidget
from .utils import deprecation
import warnings
@register
class DescriptionStyle(Style, CoreWidget, Widget):
"""Description style widget."""
_model_name = Unicode('DescriptionStyleModel').tag(sync=True)
description_width = Unicode(help="Width of the description to the side of the control.").tag(sync=True)
class DescriptionWidget(DOMWidget, CoreWidget):
"""Widget that has a description label to the side."""
_model_name = Unicode('DescriptionModel').tag(sync=True)
description = Unicode('', help="Description of the control.").tag(sync=True)
description_allow_html = Bool(False, help="Accept HTML in the description.").tag(sync=True)
style = InstanceDict(DescriptionStyle, help="Styling customizations").tag(sync=True, **widget_serialization)
def __init__(self, *args, **kwargs):
if 'description_tooltip' in kwargs:
deprecation("the description_tooltip argument is deprecated, use tooltip instead")
kwargs.setdefault('tooltip', kwargs['description_tooltip'])
del kwargs['description_tooltip']
super().__init__(*args, **kwargs)
def _repr_keys(self):
for key in super()._repr_keys():
# Exclude style if it had the default value
if key == 'style':
value = getattr(self, key)
if repr(value) == '%s()' % value.__class__.__name__:
continue
yield key
@property
def description_tooltip(self):
"""The tooltip information.
.. deprecated :: 8.0.0
Use tooltip attribute instead.
"""
deprecation(".description_tooltip is deprecated, use .tooltip instead")
return self.tooltip
@description_tooltip.setter
def description_tooltip(self, tooltip):
deprecation(".description_tooltip is deprecated, use .tooltip instead")
self.tooltip = tooltip

View File

@@ -0,0 +1,371 @@
"""Float class.
Represents an unbounded float using a widget.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from traitlets import (
Instance, Unicode, CFloat, Bool, CaselessStrEnum, Tuple, TraitError, validate, default
)
from .widget_description import DescriptionWidget
from .trait_types import InstanceDict, NumberFormat
from .valuewidget import ValueWidget
from .widget import register, widget_serialization
from .widget_core import CoreWidget
from .widget_int import ProgressStyle, SliderStyle
class _Float(DescriptionWidget, ValueWidget, CoreWidget):
value = CFloat(0.0, help="Float value").tag(sync=True)
def __init__(self, value=None, **kwargs):
if value is not None:
kwargs['value'] = value
super().__init__(**kwargs)
class _BoundedFloat(_Float):
max = CFloat(100.0, help="Max value").tag(sync=True)
min = CFloat(0.0, help="Min value").tag(sync=True)
@validate('value')
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal['value']
if self.min > value or self.max < value:
value = min(max(value, self.min), self.max)
return value
@validate('min')
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal['value']
if min > self.max:
raise TraitError('Setting min > max')
if min > self.value:
self.value = min
return min
@validate('max')
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal['value']
if max < self.min:
raise TraitError('setting max < min')
if max < self.value:
self.value = max
return max
class _BoundedLogFloat(_Float):
max = CFloat(4.0, help="Max value for the exponent").tag(sync=True)
min = CFloat(0.0, help="Min value for the exponent").tag(sync=True)
base = CFloat(10.0, help="Base of value").tag(sync=True)
value = CFloat(1.0, help="Float value").tag(sync=True)
@validate('value')
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal['value']
if self.base ** self.min > value or self.base ** self.max < value:
value = min(max(value, self.base ** self.min), self.base ** self.max)
return value
@validate('min')
def _validate_min(self, proposal):
"""Enforce base ** min <= value <= base ** max"""
min = proposal['value']
if min > self.max:
raise TraitError('Setting min > max')
if self.base ** min > self.value:
self.value = self.base ** min
return min
@validate('max')
def _validate_max(self, proposal):
"""Enforce base ** min <= value <= base ** max"""
max = proposal['value']
if max < self.min:
raise TraitError('setting max < min')
if self.base ** max < self.value:
self.value = self.base ** max
return max
@register
class FloatText(_Float):
""" Displays a float value within a textbox. For a textbox in
which the value must be within a specific range, use BoundedFloatText.
Parameters
----------
value : float
value displayed
step : float
step of the increment (if None, any step is allowed)
description : str
description displayed next to the text box
"""
_view_name = Unicode('FloatTextView').tag(sync=True)
_model_name = Unicode('FloatTextModel').tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True)
step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True)
@register
class BoundedFloatText(_BoundedFloat):
""" Displays a float value within a textbox. Value must be within the range specified.
For a textbox in which the value doesn't need to be within a specific range, use FloatText.
Parameters
----------
value : float
value displayed
min : float
minimal value of the range of possible values displayed
max : float
maximal value of the range of possible values displayed
step : float
step of the increment (if None, any step is allowed)
description : str
description displayed next to the textbox
"""
_view_name = Unicode('FloatTextView').tag(sync=True)
_model_name = Unicode('BoundedFloatTextModel').tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True)
step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True)
@register
class FloatSlider(_BoundedFloat):
""" Slider/trackbar of floating values with the specified range.
Parameters
----------
value : float
position of the slider
min : float
minimal position of the slider
max : float
maximal position of the slider
step : float
step of the trackbar
description : str
name of the slider
orientation : {'horizontal', 'vertical'}
default is 'horizontal', orientation of the slider
readout : {True, False}
default is True, display the current value of the slider next to it
behavior : str
slider handle and connector dragging behavior. Default is 'drag-tap'.
readout_format : str
default is '.2f', specifier for the format function used to represent
slider value for human consumption, modeled after Python 3's format
specification mini-language (PEP 3101).
"""
_view_name = Unicode('FloatSliderView').tag(sync=True)
_model_name = Unicode('FloatSliderModel').tag(sync=True)
step = CFloat(0.1, allow_none=True, help="Minimum step to increment the value").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'.2f', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
behavior = CaselessStrEnum(values=['drag-tap', 'drag-snap', 'tap', 'drag', 'snap'],
default_value='drag-tap', help="Slider dragging behavior.").tag(sync=True)
@register
class FloatLogSlider(_BoundedLogFloat):
""" Slider/trackbar of logarithmic floating values with the specified range.
Parameters
----------
value : float
position of the slider
base : float
base of the logarithmic scale. Default is 10
min : float
minimal position of the slider in log scale, i.e., actual minimum is base ** min
max : float
maximal position of the slider in log scale, i.e., actual maximum is base ** max
step : float
step of the trackbar, denotes steps for the exponent, not the actual value
description : str
name of the slider
orientation : {'horizontal', 'vertical'}
default is 'horizontal', orientation of the slider
readout : {True, False}
default is True, display the current value of the slider next to it
behavior : str
slider handle and connector dragging behavior. Default is 'drag-tap'.
readout_format : str
default is '.3g', specifier for the format function used to represent
slider value for human consumption, modeled after Python 3's format
specification mini-language (PEP 3101).
"""
_view_name = Unicode('FloatLogSliderView').tag(sync=True)
_model_name = Unicode('FloatLogSliderModel').tag(sync=True)
step = CFloat(0.1, allow_none=True, help="Minimum step in the exponent to increment the value").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'.3g', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
base = CFloat(10., help="Base for the logarithm").tag(sync=True)
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
behavior = CaselessStrEnum(values=['drag-tap', 'drag-snap', 'tap', 'drag', 'snap'],
default_value='drag-tap', help="Slider dragging behavior.").tag(sync=True)
@register
class FloatProgress(_BoundedFloat):
""" Displays a progress bar.
Parameters
-----------
value : float
position within the range of the progress bar
min : float
minimal position of the slider
max : float
maximal position of the slider
description : str
name of the progress bar
orientation : {'horizontal', 'vertical'}
default is 'horizontal', orientation of the progress bar
bar_style: {'success', 'info', 'warning', 'danger', ''}
color of the progress bar, default is '' (blue)
colors are: 'success'-green, 'info'-light blue, 'warning'-orange, 'danger'-red
"""
_view_name = Unicode('ProgressView').tag(sync=True)
_model_name = Unicode('FloatProgressModel').tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
bar_style = CaselessStrEnum(
values=['success', 'info', 'warning', 'danger', ''],
default_value='', allow_none=True,
help="Use a predefined styling for the progress bar.").tag(sync=True)
style = InstanceDict(ProgressStyle).tag(sync=True, **widget_serialization)
class _FloatRange(_Float):
value = Tuple(CFloat(), CFloat(), default_value=(0.0, 1.0),
help="Tuple of (lower, upper) bounds").tag(sync=True)
@property
def lower(self):
return self.value[0]
@lower.setter
def lower(self, lower):
self.value = (lower, self.value[1])
@property
def upper(self):
return self.value[1]
@upper.setter
def upper(self, upper):
self.value = (self.value[0], upper)
@validate('value')
def _validate_value(self, proposal):
lower, upper = proposal['value']
if upper < lower:
raise TraitError('setting lower > upper')
return lower, upper
class _BoundedFloatRange(_FloatRange):
step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)").tag(sync=True)
max = CFloat(100.0, help="Max value").tag(sync=True)
min = CFloat(0.0, help="Min value").tag(sync=True)
def __init__(self, *args, **kwargs):
min, max = kwargs.get('min', 0.0), kwargs.get('max', 100.0)
if kwargs.get('value', None) is None:
kwargs['value'] = (0.75 * min + 0.25 * max,
0.25 * min + 0.75 * max)
elif not isinstance(kwargs['value'], tuple):
try:
kwargs['value'] = tuple(kwargs['value'])
except:
raise TypeError(
"A 'range' must be able to be cast to a tuple. The input of type"
" {} could not be cast to a tuple".format(type(kwargs['value']))
)
super().__init__(*args, **kwargs)
@validate('min', 'max')
def _validate_bounds(self, proposal):
trait = proposal['trait']
new = proposal['value']
if trait.name == 'min' and new > self.max:
raise TraitError('setting min > max')
if trait.name == 'max' and new < self.min:
raise TraitError('setting max < min')
if trait.name == 'min':
self.value = (max(new, self.value[0]), max(new, self.value[1]))
if trait.name == 'max':
self.value = (min(new, self.value[0]), min(new, self.value[1]))
return new
@validate('value')
def _validate_value(self, proposal):
lower, upper = super()._validate_value(proposal)
lower, upper = min(lower, self.max), min(upper, self.max)
lower, upper = max(lower, self.min), max(upper, self.min)
return lower, upper
@register
class FloatRangeSlider(_BoundedFloatRange):
""" Slider/trackbar that represents a pair of floats bounded by minimum and maximum value.
Parameters
----------
value : float tuple
range of the slider displayed
min : float
minimal position of the slider
max : float
maximal position of the slider
step : float
step of the trackbar
description : str
name of the slider
orientation : {'horizontal', 'vertical'}
default is 'horizontal'
readout : {True, False}
default is True, display the current value of the slider next to it
behavior : str
slider handle and connector dragging behavior. Default is 'drag-tap'.
readout_format : str
default is '.2f', specifier for the format function used to represent
slider value for human consumption, modeled after Python 3's format
specification mini-language (PEP 3101).
"""
_view_name = Unicode('FloatRangeSliderView').tag(sync=True)
_model_name = Unicode('FloatRangeSliderModel').tag(sync=True)
step = CFloat(0.1, allow_none=True, help="Minimum step to increment the value").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'.2f', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is sliding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
behavior = CaselessStrEnum(values=['drag-tap', 'drag-snap', 'tap', 'drag', 'snap'],
default_value='drag-tap', help="Slider dragging behavior.").tag(sync=True)

View File

@@ -0,0 +1,322 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Int class.
Represents an unbounded int using a widget.
"""
from .widget_description import DescriptionWidget, DescriptionStyle
from .valuewidget import ValueWidget
from .widget import register, widget_serialization
from .widget_core import CoreWidget
from traitlets import Instance
from .trait_types import Color, InstanceDict, NumberFormat
from traitlets import (
Unicode, CInt, Bool, CaselessStrEnum, Tuple, TraitError, default, validate
)
_int_doc_t = """
Parameters
----------
value: integer
The initial value.
"""
_bounded_int_doc_t = """
Parameters
----------
value: integer
The initial value.
min: integer
The lower limit for the value.
max: integer
The upper limit for the value.
step: integer
The step between allowed values.
behavior : str
slider handle and connector dragging behavior. Default is 'drag-tap'.
"""
def _int_doc(cls):
"""Add int docstring template to class init."""
def __init__(self, value=None, **kwargs):
if value is not None:
kwargs['value'] = value
super(cls, self).__init__(**kwargs)
__init__.__doc__ = _int_doc_t
cls.__init__ = __init__
return cls
def _bounded_int_doc(cls):
"""Add bounded int docstring template to class init."""
def __init__(self, value=None, min=None, max=None, step=None, **kwargs):
if value is not None:
kwargs['value'] = value
if min is not None:
kwargs['min'] = min
if max is not None:
kwargs['max'] = max
if step is not None:
kwargs['step'] = step
super(cls, self).__init__(**kwargs)
__init__.__doc__ = _bounded_int_doc_t
cls.__init__ = __init__
return cls
class _Int(DescriptionWidget, ValueWidget, CoreWidget):
"""Base class for widgets that represent an integer."""
value = CInt(0, help="Int value").tag(sync=True)
def __init__(self, value=None, **kwargs):
if value is not None:
kwargs['value'] = value
super().__init__(**kwargs)
class _BoundedInt(_Int):
"""Base class for widgets that represent an integer bounded from above and below.
"""
max = CInt(100, help="Max value").tag(sync=True)
min = CInt(0, help="Min value").tag(sync=True)
def __init__(self, value=None, min=None, max=None, step=None, **kwargs):
if value is not None:
kwargs['value'] = value
if min is not None:
kwargs['min'] = min
if max is not None:
kwargs['max'] = max
if step is not None:
kwargs['step'] = step
super().__init__(**kwargs)
@validate('value')
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal['value']
if self.min > value or self.max < value:
value = min(max(value, self.min), self.max)
return value
@validate('min')
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal['value']
if min > self.max:
raise TraitError('setting min > max')
if min > self.value:
self.value = min
return min
@validate('max')
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal['value']
if max < self.min:
raise TraitError('setting max < min')
if max < self.value:
self.value = max
return max
@register
@_int_doc
class IntText(_Int):
"""Textbox widget that represents an integer."""
_view_name = Unicode('IntTextView').tag(sync=True)
_model_name = Unicode('IntTextModel').tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True)
step = CInt(1, help="Minimum step to increment the value").tag(sync=True)
@register
@_bounded_int_doc
class BoundedIntText(_BoundedInt):
"""Textbox widget that represents an integer bounded from above and below.
"""
_view_name = Unicode('IntTextView').tag(sync=True)
_model_name = Unicode('BoundedIntTextModel').tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True)
step = CInt(1, help="Minimum step to increment the value").tag(sync=True)
@register
class SliderStyle(DescriptionStyle, CoreWidget):
"""Button style widget."""
_model_name = Unicode('SliderStyleModel').tag(sync=True)
handle_color = Color(None, allow_none=True, help="Color of the slider handle.").tag(sync=True)
@register
@_bounded_int_doc
class IntSlider(_BoundedInt):
"""Slider widget that represents an integer bounded from above and below.
"""
_view_name = Unicode('IntSliderView').tag(sync=True)
_model_name = Unicode('IntSliderModel').tag(sync=True)
step = CInt(1, help="Minimum step to increment the value").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'd', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
behavior = CaselessStrEnum(values=['drag-tap', 'drag-snap', 'tap', 'drag', 'snap'],
default_value='drag-tap', help="Slider dragging behavior.").tag(sync=True)
@register
class ProgressStyle(DescriptionStyle, CoreWidget):
"""Button style widget."""
_model_name = Unicode('ProgressStyleModel').tag(sync=True)
bar_color = Color(None, allow_none=True, help="Color of the progress bar.").tag(sync=True)
@register
@_bounded_int_doc
class IntProgress(_BoundedInt):
"""Progress bar that represents an integer bounded from above and below.
"""
_view_name = Unicode('ProgressView').tag(sync=True)
_model_name = Unicode('IntProgressModel').tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
bar_style = CaselessStrEnum(
values=['success', 'info', 'warning', 'danger', ''], default_value='',
help="""Use a predefined styling for the progress bar.""").tag(sync=True)
style = InstanceDict(ProgressStyle).tag(sync=True, **widget_serialization)
class _IntRange(_Int):
value = Tuple(CInt(), CInt(), default_value=(0, 1),
help="Tuple of (lower, upper) bounds").tag(sync=True)
@property
def lower(self):
return self.value[0]
@lower.setter
def lower(self, lower):
self.value = (lower, self.value[1])
@property
def upper(self):
return self.value[1]
@upper.setter
def upper(self, upper):
self.value = (self.value[0], upper)
@validate('value')
def _validate_value(self, proposal):
lower, upper = proposal['value']
if upper < lower:
raise TraitError('setting lower > upper')
return lower, upper
@register
class Play(_BoundedInt):
"""Play/repeat buttons to step through values automatically, and optionally loop.
"""
_view_name = Unicode('PlayView').tag(sync=True)
_model_name = Unicode('PlayModel').tag(sync=True)
playing = Bool(help="Whether the control is currently playing.").tag(sync=True)
repeat = Bool(help="Whether the control will repeat in a continuous loop.").tag(sync=True)
interval = CInt(100, help="The time between two animation steps (ms).").tag(sync=True)
step = CInt(1, help="Increment step").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
show_repeat = Bool(True, help="Show the repeat toggle button in the widget.").tag(sync=True)
class _BoundedIntRange(_IntRange):
max = CInt(100, help="Max value").tag(sync=True)
min = CInt(0, help="Min value").tag(sync=True)
def __init__(self, *args, **kwargs):
min, max = kwargs.get('min', 0), kwargs.get('max', 100)
if kwargs.get('value', None) is None:
kwargs['value'] = (0.75 * min + 0.25 * max,
0.25 * min + 0.75 * max)
elif not isinstance(kwargs['value'], tuple):
try:
kwargs['value'] = tuple(kwargs['value'])
except:
raise TypeError(
"A 'range' must be able to be cast to a tuple. The input of type"
" {} could not be cast to a tuple".format(type(kwargs['value']))
)
super().__init__(*args, **kwargs)
@validate('min', 'max')
def _validate_bounds(self, proposal):
trait = proposal['trait']
new = proposal['value']
if trait.name == 'min' and new > self.max:
raise TraitError('setting min > max')
if trait.name == 'max' and new < self.min:
raise TraitError('setting max < min')
if trait.name == 'min':
self.value = (max(new, self.value[0]), max(new, self.value[1]))
if trait.name == 'max':
self.value = (min(new, self.value[0]), min(new, self.value[1]))
return new
@validate('value')
def _validate_value(self, proposal):
lower, upper = super()._validate_value(proposal)
lower, upper = min(lower, self.max), min(upper, self.max)
lower, upper = max(lower, self.min), max(upper, self.min)
return lower, upper
@register
class IntRangeSlider(_BoundedIntRange):
"""Slider/trackbar that represents a pair of ints bounded by minimum and maximum value.
Parameters
----------
value : int tuple
The pair (`lower`, `upper`) of integers
min : int
The lowest allowed value for `lower`
max : int
The highest allowed value for `upper`
step : int
step of the trackbar
description : str
name of the slider
orientation : {'horizontal', 'vertical'}
default is 'horizontal'
readout : {True, False}
default is True, display the current value of the slider next to it
behavior : str
slider handle and connector dragging behavior. Default is 'drag-tap'.
readout_format : str
default is '.2f', specifier for the format function used to represent
slider value for human consumption, modeled after Python 3's format
specification mini-language (PEP 3101).
"""
_view_name = Unicode('IntRangeSliderView').tag(sync=True)
_model_name = Unicode('IntRangeSliderModel').tag(sync=True)
step = CInt(1, help="Minimum step that the value can take").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'd', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is sliding the slider.").tag(sync=True)
style = InstanceDict(SliderStyle, help="Slider style customizations.").tag(sync=True, **widget_serialization)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
behavior = CaselessStrEnum(values=['drag-tap', 'drag-snap', 'tap', 'drag', 'snap'],
default_value='drag-tap', help="Slider dragging behavior.").tag(sync=True)

View File

@@ -0,0 +1,121 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""Contains the Layout class"""
from traitlets import Unicode, Instance, CaselessStrEnum, validate
from .widget import Widget, register
from .._version import __jupyter_widgets_base_version__
CSS_PROPERTIES=['inherit', 'initial', 'unset']
@register
class Layout(Widget):
"""Layout specification
Defines a layout that can be expressed using CSS. Supports a subset of
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference
When a property is also accessible via a shorthand property, we only
expose the shorthand.
For example:
- ``flex-grow``, ``flex-shrink`` and ``flex-basis`` are bound to ``flex``.
- ``flex-wrap`` and ``flex-direction`` are bound to ``flex-flow``.
- ``margin-[top/bottom/left/right]`` values are bound to ``margin``, etc.
"""
_view_name = Unicode('LayoutView').tag(sync=True)
_view_module = Unicode('@jupyter-widgets/base').tag(sync=True)
_view_module_version = Unicode(__jupyter_widgets_base_version__).tag(sync=True)
_model_name = Unicode('LayoutModel').tag(sync=True)
# Keys
align_content = CaselessStrEnum(['flex-start', 'flex-end', 'center', 'space-between',
'space-around', 'space-evenly', 'stretch'] + CSS_PROPERTIES, allow_none=True, help="The align-content CSS attribute.").tag(sync=True)
align_items = CaselessStrEnum(['flex-start', 'flex-end', 'center',
'baseline', 'stretch'] + CSS_PROPERTIES, allow_none=True, help="The align-items CSS attribute.").tag(sync=True)
align_self = CaselessStrEnum(['auto', 'flex-start', 'flex-end',
'center', 'baseline', 'stretch'] + CSS_PROPERTIES, allow_none=True, help="The align-self CSS attribute.").tag(sync=True)
border_top = Unicode(None, allow_none=True, help="The border top CSS attribute.").tag(sync=True)
border_right = Unicode(None, allow_none=True, help="The border right CSS attribute.").tag(sync=True)
border_bottom = Unicode(None, allow_none=True, help="The border bottom CSS attribute.").tag(sync=True)
border_left = Unicode(None, allow_none=True, help="The border left CSS attribute.").tag(sync=True)
bottom = Unicode(None, allow_none=True, help="The bottom CSS attribute.").tag(sync=True)
display = Unicode(None, allow_none=True, help="The display CSS attribute.").tag(sync=True)
flex = Unicode(None, allow_none=True, help="The flex CSS attribute.").tag(sync=True)
flex_flow = Unicode(None, allow_none=True, help="The flex-flow CSS attribute.").tag(sync=True)
height = Unicode(None, allow_none=True, help="The height CSS attribute.").tag(sync=True)
justify_content = CaselessStrEnum(['flex-start', 'flex-end', 'center',
'space-between', 'space-around'] + CSS_PROPERTIES, allow_none=True, help="The justify-content CSS attribute.").tag(sync=True)
justify_items = CaselessStrEnum(['flex-start', 'flex-end', 'center'] + CSS_PROPERTIES,
allow_none=True, help="The justify-items CSS attribute.").tag(sync=True)
left = Unicode(None, allow_none=True, help="The left CSS attribute.").tag(sync=True)
margin = Unicode(None, allow_none=True, help="The margin CSS attribute.").tag(sync=True)
max_height = Unicode(None, allow_none=True, help="The max-height CSS attribute.").tag(sync=True)
max_width = Unicode(None, allow_none=True, help="The max-width CSS attribute.").tag(sync=True)
min_height = Unicode(None, allow_none=True, help="The min-height CSS attribute.").tag(sync=True)
min_width = Unicode(None, allow_none=True, help="The min-width CSS attribute.").tag(sync=True)
overflow = Unicode(None, allow_none=True, help="The overflow CSS attribute.").tag(sync=True)
order = Unicode(None, allow_none=True, help="The order CSS attribute.").tag(sync=True)
padding = Unicode(None, allow_none=True, help="The padding CSS attribute.").tag(sync=True)
right = Unicode(None, allow_none=True, help="The right CSS attribute.").tag(sync=True)
top = Unicode(None, allow_none=True, help="The top CSS attribute.").tag(sync=True)
visibility = CaselessStrEnum(['visible', 'hidden']+CSS_PROPERTIES, allow_none=True, help="The visibility CSS attribute.").tag(sync=True)
width = Unicode(None, allow_none=True, help="The width CSS attribute.").tag(sync=True)
object_fit = CaselessStrEnum(['contain', 'cover', 'fill', 'scale-down', 'none'], allow_none=True, help="The object-fit CSS attribute.").tag(sync=True)
object_position = Unicode(None, allow_none=True, help="The object-position CSS attribute.").tag(sync=True)
grid_auto_columns = Unicode(None, allow_none=True, help="The grid-auto-columns CSS attribute.").tag(sync=True)
grid_auto_flow = CaselessStrEnum(['column','row','row dense','column dense']+ CSS_PROPERTIES, allow_none=True, help="The grid-auto-flow CSS attribute.").tag(sync=True)
grid_auto_rows = Unicode(None, allow_none=True, help="The grid-auto-rows CSS attribute.").tag(sync=True)
grid_gap = Unicode(None, allow_none=True, help="The grid-gap CSS attribute.").tag(sync=True)
grid_template_rows = Unicode(None, allow_none=True, help="The grid-template-rows CSS attribute.").tag(sync=True)
grid_template_columns = Unicode(None, allow_none=True, help="The grid-template-columns CSS attribute.").tag(sync=True)
grid_template_areas = Unicode(None, allow_none=True, help="The grid-template-areas CSS attribute.").tag(sync=True)
grid_row = Unicode(None, allow_none=True, help="The grid-row CSS attribute.").tag(sync=True)
grid_column = Unicode(None, allow_none=True, help="The grid-column CSS attribute.").tag(sync=True)
grid_area = Unicode(None, allow_none=True, help="The grid-area CSS attribute.").tag(sync=True)
def __init__(self, **kwargs):
if 'border' in kwargs:
border = kwargs.pop('border')
for side in ['top', 'right', 'bottom', 'left']:
kwargs.setdefault(f'border_{side}', border)
super().__init__(**kwargs)
def _get_border(self):
"""
`border` property getter. Return the common value of all side
borders if they are identical. Otherwise return None.
"""
found = None
for side in ['top', 'right', 'bottom', 'left']:
if not hasattr(self, "border_" + side):
return
old, found = found, getattr(self, "border_" + side)
if found is None or (old is not None and found != old):
return
return found
def _set_border(self, border):
"""
`border` property setter. Set all 4 sides to `border` string.
"""
for side in ['top', 'right', 'bottom', 'left']:
setattr(self, "border_" + side, border)
border = property(_get_border, _set_border)
class LayoutTraitType(Instance):
klass = Layout
def validate(self, obj, value):
if isinstance(value, dict):
return super().validate(obj, self.klass(**value))
else:
return super().validate(obj, value)

Some files were not shown because too many files have changed in this diff Show More