diff --git a/param/__init__.py b/param/__init__.py index d51be21c7..5c262ca74 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2127,6 +2127,37 @@ def _validate_value(self, val, allow_None): "is before start datetime %s." % (self.name, val[1], val[0])) + @classmethod + def serialize(cls, value): + if value is None: + return 'null' + # List as JSON has no tuple representation + serialized = [] + for v in value: + if not isinstance(v, (dt.datetime, dt.date)): # i.e np.datetime64 + v = v.astype(dt.datetime) + # Separate date and datetime to deserialize to the right type. + if type(v) == dt.date: + v = v.strftime("%Y-%m-%d") + else: + v = v.strftime("%Y-%m-%dT%H:%M:%S.%f") + serialized.append(v) + return serialized + + def deserialize(cls, value): + if value == 'null': + return None + deserialized = [] + for v in value: + # Date + if len(v) == 10: + v = dt.datetime.strptime(v, "%Y-%m-%d").date() + # Datetime + else: + v = dt.datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f") + deserialized.append(v) + # As JSON has no tuple representation + return tuple(deserialized) class CalendarDateRange(Range): @@ -2148,6 +2179,20 @@ def _validate_value(self, val, allow_None): "%s is before start date %s." % (self.name, val[1], val[0])) + @classmethod + def serialize(cls, value): + if value is None: + return 'null' + # As JSON has no tuple representation + return [v.strftime("%Y-%m-%d") for v in value] + + @classmethod + def deserialize(cls, value): + if value == 'null': + return None + # As JSON has no tuple representation + return tuple([dt.datetime.strptime(v, "%Y-%m-%d").date() for v in value]) + class Event(Boolean): """ diff --git a/tests/API1/testjsonserialization.py b/tests/API1/testjsonserialization.py index f4b6d070f..7ec044226 100644 --- a/tests/API1/testjsonserialization.py +++ b/tests/API1/testjsonserialization.py @@ -4,6 +4,7 @@ import datetime import json +import sys import param @@ -18,20 +19,30 @@ raise ImportError("PARAM_TEST_JSONSCHEMA=1 but jsonschema not available.") validate = None +now = datetime.datetime.now() +after_now = now + datetime.timedelta(days=1) + try: import numpy as np ndarray = np.array([[1,2,3],[4,5,6]]) + npdt1 = np.datetime64(now) + npdt2 = np.datetime64(after_now) except: - np, ndarray = None, None + np, ndarray, npdt1, npdt2 = None, None, None, None np_skip = skipIf(np is None, "NumPy is not available") +on_py2 = sys.version_info[0] == 2 +py2_skip = skipIf(on_py2, "Ignore Python 2") + try: import pandas as pd df1 = pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]}) df2 = pd.DataFrame({'A':[1.1,2.2,3.3], 'B':[1.1,2.2,3.3]}) + pdts1 = pd.Timestamp(now) + pdts2 = pd.Timestamp(after_now) except: - pd, df1, df2 = None, None, None + pd, df1, df2, pdts1, pdts2 = None, None, None, None, None pd_skip = skipIf(pd is None, "pandas is not available") @@ -52,6 +63,8 @@ class TestSet(param.Parameterized): e = param.List([1,2,3], class_=int) f = param.List([1,2,3]) g = param.Date(default=datetime.datetime.now()) + g2 = None if (np is None or on_py2) else param.Date(default=npdt1) + g3 = None if pd is None else param.Date(default=pdts1) h = param.Tuple(default=(1,2,3), length=3) i = param.NumericTuple(default=(1,2,3,4)) j = param.XYCoordinates(default=(32.1, 51.5)) @@ -73,6 +86,21 @@ class TestSet(param.Parameterized): y = None if np is None else param.Array(default=None) z = None if pd is None else param.DataFrame(default=None, allow_None=True) aa = param.Tuple(default=None, allow_None=True, length=1) + ab = param.CalendarDateRange(default=( + datetime.date(2020, 1, 1), + datetime.date(2021, 1, 1) + )) + ac = param.DateRange(default=( + datetime.date(2020, 1, 1), + datetime.date(2021, 1, 1) + )) + ad = param.DateRange(default=( + datetime.datetime(2020, 1, 1, 1, 1, 1, 1), + datetime.datetime(2021, 1, 1, 1, 1, 1, 1) + )) + # datetime.datetime comparison with numpy.datetime64 fails on Python 2 + ae = None if (np is None or on_py2) else param.DateRange(default=(npdt1, npdt2)) + af = None if pd is None else param.DateRange(default=(pdts1, pdts2)) test = TestSet(a=29) @@ -128,6 +156,24 @@ def test_serialize_date_class(self): def test_serialize_date_instance(self): self._test_serialize(test, 'g') + @py2_skip + @np_skip + def test_serialize_date_numpy_class(self): + self._test_serialize(TestSet, 'g2') + + @py2_skip + @np_skip + def test_serialize_date_numpy_instance(self): + self._test_serialize(test, 'g2') + + @pd_skip + def test_serialize_date_pandas_class(self): + self._test_serialize(TestSet, 'g3') + + @pd_skip + def test_serialize_date_pandas_instance(self): + self._test_serialize(test, 'g3') + def test_serialize_tuple_class(self): self._test_serialize(TestSet, 'h') @@ -194,6 +240,41 @@ def test_pandas_instance_serialization(self): else: self.assertTrue(getattr(test, pname).equals(deserialized[pname])) + def test_serialize_calendar_date_range_class(self): + self._test_serialize(TestSet, 'ab') + + def test_serialize_calendar_date_range_instance(self): + self._test_serialize(test, 'ab') + + def test_serialize_date_range_class(self): + self._test_serialize(TestSet, 'ac') + + def test_serialize_date_range_instance(self): + self._test_serialize(test, 'ac') + + def test_serialize_datetime_range_class(self): + self._test_serialize(TestSet, 'ad') + + def test_serialize_datetime_range_instance(self): + self._test_serialize(test, 'ad') + + @py2_skip + @np_skip + def test_serialize_datetime_range_numpy_class(self): + self._test_serialize(TestSet, 'ae') + + @py2_skip + @np_skip + def test_serialize_datetime_range_numpy_instance(self): + self._test_serialize(test, 'ae') + + @pd_skip + def test_serialize_datetime_range_pandas_class(self): + self._test_serialize(TestSet, 'af') + + @pd_skip + def test_serialize_datetime_range_pandas_instance(self): + self._test_serialize(test, 'af') class TestJSONSerialization(TestSerialization):