Skip to content

Commit 3a60698

Browse files
authored
Merge pull request #3581 from vidartf/datetime-fix
Datetime widget fixes
2 parents 90e6be0 + 04ecd58 commit 3a60698

File tree

7 files changed

+156
-56
lines changed

7 files changed

+156
-56
lines changed

docs/source/examples/Widget List.ipynb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,28 @@
11571157
")"
11581158
]
11591159
},
1160+
{
1161+
"cell_type": "markdown",
1162+
"metadata": {},
1163+
"source": [
1164+
"## Naive picker\n",
1165+
"\n",
1166+
"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",
1167+
"\n",
1168+
"> Naive objects are easy to understand and to work with, at the cost of ignoring some aspects of reality.\n",
1169+
"\n",
1170+
"This is useful if you need to compare the picked datetime to naive datetime objects, as Python will otherwise complain!"
1171+
]
1172+
},
1173+
{
1174+
"cell_type": "code",
1175+
"execution_count": null,
1176+
"metadata": {},
1177+
"outputs": [],
1178+
"source": [
1179+
"widgets.NaiveDatetimePicker(description='Pick a Time')"
1180+
]
1181+
},
11601182
{
11611183
"cell_type": "markdown",
11621184
"metadata": {},

python/ipywidgets/ipywidgets/widgets/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider, Play, SliderStyle
1616
from .widget_color import ColorPicker
1717
from .widget_date import DatePicker
18-
from .widget_datetime import DatetimePicker
18+
from .widget_datetime import DatetimePicker, NaiveDatetimePicker
1919
from .widget_time import TimePicker
2020
from .widget_output import Output
2121
from .widget_selection import RadioButtons, ToggleButtons, ToggleButtonsStyle, Dropdown, Select, SelectionSlider, SelectMultiple, SelectionRangeSlider

python/ipywidgets/ipywidgets/widgets/tests/test_widget_datetime.py

Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,106 +6,152 @@
66

77
import pytest
88

9+
from contextlib import nullcontext
910
import datetime
11+
import itertools
1012

1113
import pytz
1214
from traitlets import TraitError
1315

1416
from ..widget_datetime import DatetimePicker
1517

1618

19+
dt_1442 = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
20+
dt_1664 = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
21+
dt_1994 = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
22+
dt_2002 = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
23+
dt_2056 = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
24+
1725
def test_time_creation_blank():
1826
w = DatetimePicker()
1927
assert w.value is None
2028

2129

2230
def test_time_creation_value():
23-
t = datetime.datetime.now(pytz.utc)
24-
w = DatetimePicker(value=t)
25-
assert w.value is t
31+
dt = datetime.datetime.now(pytz.utc)
32+
w = DatetimePicker(value=dt)
33+
assert w.value is dt
2634

2735

28-
def test_time_validate_value_none():
29-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
30-
t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
31-
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
32-
w = DatetimePicker(value=t, min=t_min, max=t_max)
36+
def test_datetime_validate_value_none():
37+
dt = dt_2002
38+
dt_min = dt_1442
39+
dt_max = dt_2056
40+
w = DatetimePicker(value=dt, min=dt_min, max=dt_max)
3341
w.value = None
3442
assert w.value is None
3543

3644

37-
def test_time_validate_value_vs_min():
38-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
39-
t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
40-
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
41-
w = DatetimePicker(min=t_min, max=t_max)
42-
w.value = t
45+
def _permuted_dts():
46+
ret = []
47+
combos = list(itertools.product([None, dt_1442, dt_2002, dt_2056], repeat=3))
48+
for vals in combos:
49+
expected = vals[0]
50+
if vals[1] and vals[2] and vals[1] > vals[2]:
51+
expected = TraitError
52+
elif vals[0] is None:
53+
pass
54+
elif vals[1] and vals[1] > vals[0]:
55+
expected = vals[1]
56+
elif vals[2] and vals[2] < vals[0]:
57+
expected = vals[2]
58+
ret.append(vals + (expected,))
59+
return ret
60+
61+
62+
@pytest.mark.parametrize(
63+
"input_value,input_min,input_max,expected",
64+
_permuted_dts()
65+
)
66+
def test_datetime_cross_validate_value_min_max(
67+
input_value,
68+
input_min,
69+
input_max,
70+
expected,
71+
):
72+
w = DatetimePicker(value=dt_2002, min=dt_2002, max=dt_2002)
73+
should_raise = expected is TraitError
74+
with pytest.raises(expected) if should_raise else nullcontext():
75+
with w.hold_trait_notifications():
76+
w.value = input_value
77+
w.min = input_min
78+
w.max = input_max
79+
if not should_raise:
80+
assert w.value is expected
81+
82+
83+
def test_datetime_validate_value_vs_min():
84+
dt = dt_2002
85+
dt_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
86+
dt_max = dt_2056
87+
w = DatetimePicker(min=dt_min, max=dt_max)
88+
w.value = dt
4389
assert w.value.year == 2019
4490

4591

46-
def test_time_validate_value_vs_max():
47-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
48-
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
49-
t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
50-
w = DatetimePicker(min=t_min, max=t_max)
51-
w.value = t
92+
def test_datetime_validate_value_vs_max():
93+
dt = dt_2002
94+
dt_min = dt_1664
95+
dt_max = dt_1994
96+
w = DatetimePicker(min=dt_min, max=dt_max)
97+
w.value = dt
5298
assert w.value.year == 1994
5399

54100

55-
def test_time_validate_min_vs_value():
56-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
57-
t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
58-
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
59-
w = DatetimePicker(value=t, max=t_max)
60-
w.min = t_min
101+
def test_datetime_validate_min_vs_value():
102+
dt = dt_2002
103+
dt_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
104+
dt_max = dt_2056
105+
w = DatetimePicker(value=dt, max=dt_max)
106+
w.min = dt_min
61107
assert w.value.year == 2019
62108

63109

64-
def test_time_validate_min_vs_max():
65-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
66-
t_min = datetime.datetime(2112, 1, 1, tzinfo=pytz.utc)
67-
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
68-
w = DatetimePicker(value=t, max=t_max)
110+
def test_datetime_validate_min_vs_max():
111+
dt = dt_2002
112+
dt_min = datetime.datetime(2112, 1, 1, tzinfo=pytz.utc)
113+
dt_max = dt_2056
114+
w = DatetimePicker(value=dt, max=dt_max)
69115
with pytest.raises(TraitError):
70-
w.min = t_min
116+
w.min = dt_min
71117

72118

73-
def test_time_validate_max_vs_value():
74-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
75-
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
76-
t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc)
77-
w = DatetimePicker(value=t, min=t_min)
78-
w.max = t_max
119+
def test_datetime_validate_max_vs_value():
120+
dt = dt_2002
121+
dt_min = dt_1664
122+
dt_max = dt_1994
123+
w = DatetimePicker(value=dt, min=dt_min)
124+
w.max = dt_max
79125
assert w.value.year == 1994
80126

81127

82-
def test_time_validate_max_vs_min():
83-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
84-
t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc)
85-
t_max = datetime.datetime(1337, 1, 1, tzinfo=pytz.utc)
86-
w = DatetimePicker(value=t, min=t_min)
128+
def test_datetime_validate_max_vs_min():
129+
dt = dt_2002
130+
dt_min = dt_1664
131+
dt_max = datetime.datetime(1337, 1, 1, tzinfo=pytz.utc)
132+
w = DatetimePicker(value=dt, min=dt_min)
87133
with pytest.raises(TraitError):
88-
w.max = t_max
134+
w.max = dt_max
89135

90136

91-
def test_time_validate_naive():
92-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc)
93-
t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc)
94-
t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc)
137+
def test_datetime_validate_naive():
138+
dt = dt_2002
139+
dt_min = dt_1442
140+
dt_max = dt_2056
95141

96-
w = DatetimePicker(value=t, min=t_min, max=t_max)
142+
w = DatetimePicker(value=dt, min=dt_min, max=dt_max)
97143
with pytest.raises(TraitError):
98-
w.max = t_max.replace(tzinfo=None)
144+
w.max = dt_max.replace(tzinfo=None)
99145
with pytest.raises(TraitError):
100-
w.min = t_min.replace(tzinfo=None)
146+
w.min = dt_min.replace(tzinfo=None)
101147
with pytest.raises(TraitError):
102-
w.value = t.replace(tzinfo=None)
148+
w.value = dt.replace(tzinfo=None)
103149

104150

105151
def test_datetime_tzinfo():
106152
tz = pytz.timezone('Australia/Sydney')
107-
t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz)
108-
w = DatetimePicker(value=t)
109-
assert w.value == t
153+
dt = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz)
154+
w = DatetimePicker(value=dt)
155+
assert w.value == dt
110156
# tzinfo only changes upon input from user
111157
assert w.value.tzinfo == tz

python/ipywidgets/ipywidgets/widgets/tests/test_widget_time.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ def test_time_creation_value():
2424
assert w.value is t
2525

2626

27+
def test_time_cross_validate_value_min_max():
28+
w = TimePicker(value=datetime.time(2), min=datetime.time(2), max=datetime.time(2))
29+
with w.hold_trait_notifications():
30+
w.value = None
31+
w.min = datetime.time(4)
32+
w.max = datetime.time(6)
33+
assert w.value is None
34+
with w.hold_trait_notifications():
35+
w.value = datetime.time(4)
36+
w.min = None
37+
w.max = None
38+
assert w.value == datetime.time(4)
39+
40+
2741
def test_time_validate_value_none():
2842
t = datetime.time(13, 37, 42, 7)
2943
t_min = datetime.time(2)

python/ipywidgets/ipywidgets/widgets/widget_date.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class DatePicker(DescriptionWidget, ValueWidget, CoreWidget):
5454
def _validate_value(self, proposal):
5555
"""Cap and floor value"""
5656
value = proposal["value"]
57+
if value is None:
58+
return value
5759
if self.min and self.min > value:
5860
value = max(value, self.min)
5961
if self.max and self.max < value:
@@ -64,6 +66,8 @@ def _validate_value(self, proposal):
6466
def _validate_min(self, proposal):
6567
"""Enforce min <= value <= max"""
6668
min = proposal["value"]
69+
if min is None:
70+
return min
6771
if self.max and min > self.max:
6872
raise TraitError("Setting min > max")
6973
if self.value and min > self.value:
@@ -74,6 +78,8 @@ def _validate_min(self, proposal):
7478
def _validate_max(self, proposal):
7579
"""Enforce min <= value <= max"""
7680
max = proposal["value"]
81+
if max is None:
82+
return max
7783
if self.min and max < self.min:
7884
raise TraitError("setting max < min")
7985
if self.value and max < self.value:

python/ipywidgets/ipywidgets/widgets/widget_datetime.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def _validate_tz(self, value):
6161
def _validate_value(self, proposal):
6262
"""Cap and floor value"""
6363
value = proposal["value"]
64+
if value is None:
65+
return value
6466
value = self._validate_tz(value)
6567
if self.min and self.min > value:
6668
value = max(value, self.min)
@@ -72,6 +74,8 @@ def _validate_value(self, proposal):
7274
def _validate_min(self, proposal):
7375
"""Enforce min <= value <= max"""
7476
min = proposal["value"]
77+
if min is None:
78+
return min
7579
min = self._validate_tz(min)
7680
if self.max and min > self.max:
7781
raise TraitError("Setting min > max")
@@ -83,6 +87,8 @@ def _validate_min(self, proposal):
8387
def _validate_max(self, proposal):
8488
"""Enforce min <= value <= max"""
8589
max = proposal["value"]
90+
if max is None:
91+
return max
8692
max = self._validate_tz(max)
8793
if self.min and max < self.min:
8894
raise TraitError("setting max < min")

python/ipywidgets/ipywidgets/widgets/widget_time.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class TimePicker(DescriptionWidget, ValueWidget, CoreWidget):
6363
def _validate_value(self, proposal):
6464
"""Cap and floor value"""
6565
value = proposal["value"]
66+
if value is None:
67+
return value
6668
if self.min and self.min > value:
6769
value = max(value, self.min)
6870
if self.max and self.max < value:
@@ -73,6 +75,8 @@ def _validate_value(self, proposal):
7375
def _validate_min(self, proposal):
7476
"""Enforce min <= value <= max"""
7577
min = proposal["value"]
78+
if min is None:
79+
return min
7680
if self.max and min > self.max:
7781
raise TraitError("Setting min > max")
7882
if self.value and min > self.value:
@@ -83,6 +87,8 @@ def _validate_min(self, proposal):
8387
def _validate_max(self, proposal):
8488
"""Enforce min <= value <= max"""
8589
max = proposal["value"]
90+
if max is None:
91+
return max
8692
if self.min and max < self.min:
8793
raise TraitError("setting max < min")
8894
if self.value and max < self.value:

0 commit comments

Comments
 (0)