From b053524d622f59bfb091f9c2487420f838270e71 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 15 Feb 2021 12:01:30 +0000 Subject: [PATCH] Fixes plus tests from ipydatetime --- .../tests/test_datetime_serializers.py | 91 ++++++++++++++ .../widgets/tests/test_widget_datetime.py | 111 ++++++++++++++++++ .../tests/test_widget_naive_datetime.py | 95 +++++++++++++++ ipywidgets/widgets/tests/test_widget_time.py | 87 ++++++++++++++ ipywidgets/widgets/trait_types.py | 52 +++++--- ipywidgets/widgets/widget_datetime.py | 5 +- ipywidgets/widgets/widget_time.py | 5 +- setup.cfg | 1 + 8 files changed, 423 insertions(+), 24 deletions(-) create mode 100644 ipywidgets/widgets/tests/test_datetime_serializers.py create mode 100644 ipywidgets/widgets/tests/test_widget_datetime.py create mode 100644 ipywidgets/widgets/tests/test_widget_naive_datetime.py create mode 100644 ipywidgets/widgets/tests/test_widget_time.py diff --git a/ipywidgets/widgets/tests/test_datetime_serializers.py b/ipywidgets/widgets/tests/test_datetime_serializers.py new file mode 100644 index 00000000000..adfdef36151 --- /dev/null +++ b/ipywidgets/widgets/tests/test_datetime_serializers.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# 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-existant timezone, so it wil 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 + ) diff --git a/ipywidgets/widgets/tests/test_widget_datetime.py b/ipywidgets/widgets/tests/test_widget_datetime.py new file mode 100644 index 00000000000..9e66c688468 --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_datetime.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# 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 DatetimePicker + + +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 + + +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) + 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 + 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 + 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 + 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) + 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, 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 + 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) + with pytest.raises(TraitError): + w.max = t_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) + + w = DatetimePicker(value=t, min=t_min, max=t_max) + with pytest.raises(TraitError): + w.max = t_max.replace(tzinfo=None) + with pytest.raises(TraitError): + w.min = t_min.replace(tzinfo=None) + with pytest.raises(TraitError): + w.value = t.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 + # tzinfo only changes upon input from user + assert w.value.tzinfo == tz diff --git a/ipywidgets/widgets/tests/test_widget_naive_datetime.py b/ipywidgets/widgets/tests/test_widget_naive_datetime.py new file mode 100644 index 00000000000..212673d5db9 --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_naive_datetime.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# 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) diff --git a/ipywidgets/widgets/tests/test_widget_time.py b/ipywidgets/widgets/tests/test_widget_time.py new file mode 100644 index 00000000000..25df8b7c427 --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_time.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# 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_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 diff --git a/ipywidgets/widgets/trait_types.py b/ipywidgets/widgets/trait_types.py index f1a453457cc..591f2a8fb08 100644 --- a/ipywidgets/widgets/trait_types.py +++ b/ipywidgets/widgets/trait_types.py @@ -89,14 +89,20 @@ def datetime_to_json(pydt, manager): 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=pydt.year, - month=pydt.month - 1, # Months are 0-based indices in JS - date=pydt.day, - hours=pydt.hour, # Hours, Minutes, Seconds and Milliseconds - minutes=pydt.minute, # are plural in JS - seconds=pydt.second, - milliseconds=pydt.microsecond / 1000 + 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, ) @@ -105,15 +111,29 @@ def datetime_from_json(js, manager): 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 - ) + 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, diff --git a/ipywidgets/widgets/widget_datetime.py b/ipywidgets/widgets/widget_datetime.py index 0a1ccab6acd..c248c5125c2 100644 --- a/ipywidgets/widgets/widget_datetime.py +++ b/ipywidgets/widgets/widget_datetime.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python -# coding: utf-8 - -# Copyright (c) Vidar Tonaas Fauske. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. """ diff --git a/ipywidgets/widgets/widget_time.py b/ipywidgets/widgets/widget_time.py index 646e9bbe572..8aa820edc8e 100644 --- a/ipywidgets/widgets/widget_time.py +++ b/ipywidgets/widgets/widget_time.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python -# coding: utf-8 - -# Copyright (c) Vidar Tonaas Fauske. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. """ diff --git a/setup.cfg b/setup.cfg index 04741e02b0d..56e1b6784bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ install_requires = test = pytest>=3.6.0 pytest-cov + pytz [options.package_data] ipywidgets =