Skip to content
2 changes: 1 addition & 1 deletion ipywidgets/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .domwidget import DOMWidget
from .valuewidget import ValueWidget

from .trait_types import Color, Datetime
from .trait_types import Color, Datetime, NumberFormat

from .widget_core import CoreWidget
from .widget_bool import Checkbox, ToggleButton, Valid
Expand Down
17 changes: 16 additions & 1 deletion ipywidgets/widgets/tests/test_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@
from unittest import TestCase
from traitlets import HasTraits
from traitlets.tests.test_traitlets import TraitTestBase
from ipywidgets import Color

from ipywidgets import Color, NumberFormat
from ipywidgets.widgets.widget import _remove_buffers, _put_buffers
from ipywidgets.widgets.trait_types import date_serialization


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")

Expand Down
22 changes: 22 additions & 0 deletions ipywidgets/widgets/tests/test_widget_float.py
Original file line number Diff line number Diff line change
@@ -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')
60 changes: 53 additions & 7 deletions ipywidgets/widgets/trait_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import traitlets
import datetime as dt

_color_names = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred ', 'indigo ', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', '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', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']

_color_names = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred ', 'indigo ', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', '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', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']
_color_re = re.compile(r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$')


Expand Down Expand Up @@ -61,8 +62,8 @@ def datetime_to_json(pydt, manager):
year=pydt.year,
month=pydt.month - 1, # Months are 0-based indices in JS
date=pydt.day,
hours=pydt.hour, # Hours, Minutes, Seconds and Milliseconds are
minutes=pydt.minute, # plural in JS
hours=pydt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=pydt.minute, # are plural in JS
seconds=pydt.second,
milliseconds=pydt.microsecond / 1000
)
Expand All @@ -88,6 +89,7 @@ def datetime_from_json(js, manager):
'to_json': datetime_to_json
}


def date_to_json(pydate, manager):
"""Serialize a Python date object.

Expand Down Expand Up @@ -123,10 +125,12 @@ def date_from_json(js, manager):

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."""

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):
Expand All @@ -137,3 +141,45 @@ def validate(self, 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/master/src/formatSpecifier.js
_number_format_re = re.compile('^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$', re.I)

# The valid types are taken from
# https://github.com/d3/d3-format/blob/master/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(NumberFormat, self).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)
)
8 changes: 5 additions & 3 deletions ipywidgets/widgets/widget_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Instance, Unicode, CFloat, Bool, CaselessStrEnum, Tuple, TraitError, validate, default
)
from .widget_description import DescriptionWidget
from .trait_types import InstanceDict
from .trait_types import InstanceDict, NumberFormat
from .valuewidget import ValueWidget
from .widget import register, widget_serialization
from .widget_core import CoreWidget
Expand Down Expand Up @@ -129,7 +129,8 @@ class FloatSlider(_BoundedFloat):
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 = Unicode('.2f', help="Format for the readout").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)

Expand Down Expand Up @@ -265,7 +266,8 @@ class FloatRangeSlider(_BoundedFloatRange):
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 = Unicode('.2f', help="Format for the readout").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)

Expand Down
8 changes: 5 additions & 3 deletions ipywidgets/widgets/widget_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .widget import register, widget_serialization
from .widget_core import CoreWidget
from traitlets import Instance
from .trait_types import Color, InstanceDict
from .trait_types import Color, InstanceDict, NumberFormat
from traitlets import (
Unicode, CInt, Bool, CaselessStrEnum, Tuple, TraitError, default, validate
)
Expand Down Expand Up @@ -157,7 +157,8 @@ class IntSlider(_BoundedInt):
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 = Unicode('d', help="Format for the readout").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)

Expand Down Expand Up @@ -282,7 +283,8 @@ class IntRangeSlider(_BoundedIntRange):
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 = Unicode('d', help="Format for the readout").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)