Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datetime widget fixes #3581

Merged
merged 8 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/source/examples/Widget List.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,28 @@
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Naive picker\n",
"\n",
"In some cases you might want to be able to pick naive datetime objects, i.e. timezone-unaware datetimes. To quote the Python 3 docs:\n",
"\n",
"> Naive objects are easy to understand and to work with, at the cost of ignoring some aspects of reality.\n",
"\n",
"This is useful if you need to compare the picked datetime to naive datetime objects, as Python will otherwise complain!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ipywidgets.NaiveDatetimePicker(description='Pick a Time')"
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
2 changes: 1 addition & 1 deletion python/ipywidgets/ipywidgets/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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
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
Expand Down
156 changes: 101 additions & 55 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,106 +6,152 @@

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():
t = datetime.datetime.now(pytz.utc)
w = DatetimePicker(value=t)
assert w.value is t
dt = datetime.datetime.now(pytz.utc)
w = DatetimePicker(value=dt)
assert w.value is dt


def test_time_validate_value_none():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=t, min=t_min, max=t_max)
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 test_time_validate_value_vs_min():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(min=t_min, max=t_max)
w.value = t
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_time_validate_value_vs_max():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(min=t_min, max=t_max)
w.value = t
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_time_validate_min_vs_value():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=t, max=t_max)
w.min = t_min
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_time_validate_min_vs_max():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(2112, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=t, max=t_max)
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 = t_min
w.min = dt_min


def test_time_validate_max_vs_value():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=t, min=t_min)
w.max = t_max
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_time_validate_max_vs_min():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(1337, 1, 1, tzinfo=pytz.utc)
w = DatetimePicker(value=t, min=t_min)
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 = t_max
w.max = dt_max


def test_time_validate_naive():
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
def test_datetime_validate_naive():
dt = dt_2002
dt_min = dt_1442
dt_max = dt_2056

w = DatetimePicker(value=t, min=t_min, max=t_max)
w = DatetimePicker(value=dt, min=dt_min, max=dt_max)
with pytest.raises(TraitError):
w.max = t_max.replace(tzinfo=None)
w.max = dt_max.replace(tzinfo=None)
with pytest.raises(TraitError):
w.min = t_min.replace(tzinfo=None)
w.min = dt_min.replace(tzinfo=None)
with pytest.raises(TraitError):
w.value = t.replace(tzinfo=None)
w.value = dt.replace(tzinfo=None)


def test_datetime_tzinfo():
tz = pytz.timezone('Australia/Sydney')
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz)
w = DatetimePicker(value=t)
assert w.value == t
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
14 changes: 14 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ def test_time_creation_value():
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)
Expand Down
6 changes: 6 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/widget_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class DatePicker(DescriptionWidget, ValueWidget, CoreWidget):
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:
Expand All @@ -64,6 +66,8 @@ def _validate_value(self, proposal):
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:
Expand All @@ -74,6 +78,8 @@ def _validate_min(self, proposal):
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:
Expand Down
6 changes: 6 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/widget_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def _validate_tz(self, 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)
Expand All @@ -72,6 +74,8 @@ def _validate_value(self, proposal):
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")
Expand All @@ -83,6 +87,8 @@ def _validate_min(self, proposal):
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")
Expand Down
6 changes: 6 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/widget_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class TimePicker(DescriptionWidget, ValueWidget, CoreWidget):
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:
Expand All @@ -73,6 +75,8 @@ def _validate_value(self, proposal):
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:
Expand All @@ -83,6 +87,8 @@ def _validate_min(self, proposal):
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:
Expand Down