From 452ad9034ab38d6881d0331b437072a598aa3270 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Thu, 8 Nov 2018 10:01:32 -0800 Subject: [PATCH] Implementing `DateProperty` and `TimeProperty` in `ndb`. --- ndb/src/google/cloud/ndb/model.py | 130 ++++++++++++++++++++++++++++-- ndb/tests/unit/test_model.py | 70 ++++++++++++++-- 2 files changed, 188 insertions(+), 12 deletions(-) diff --git a/ndb/src/google/cloud/ndb/model.py b/ndb/src/google/cloud/ndb/model.py index 58781cf7c76b..0c09cdf684fc 100644 --- a/ndb/src/google/cloud/ndb/model.py +++ b/ndb/src/google/cloud/ndb/model.py @@ -2363,9 +2363,9 @@ def _validate(self, value): @staticmethod def _now(): - """datetime.datetime: Return current time. + """datetime.datetime: Return current datetime. - This is in place so it can be patched in tests. + Subclasses will override this to return different forms of "now". """ return datetime.datetime.utcnow() @@ -2407,17 +2407,135 @@ def _db_get_value(self, v, unused_p): class DateProperty(DateTimeProperty): + """A property that contains :class:`~datetime.date` values. + + .. automethod:: _to_base_type + .. automethod:: _from_base_type + .. automethod:: _validate + """ + __slots__ = () - def __init__(self, *args, **kwargs): - raise NotImplementedError + def _validate(self, value): + """Validate a ``value`` before setting it. + + Args: + value (~datetime.date): The value to check. + + Raises: + .BadValueError: If ``value`` is not a :class:`~datetime.date`. + """ + if not isinstance(value, datetime.date): + raise exceptions.BadValueError( + "Expected date, got {!r}".format(value) + ) + + def _to_base_type(self, value): + """Convert a value to the "base" value type for this property. + + Args: + value (~datetime.date): The value to be converted. + + Returns: + ~datetime.datetime: The converted value: a datetime object with the + time set to ``00:00``. + + Raises: + TypeError: If ``value`` is not a :class:`~datetime.date`. + """ + if not isinstance(value, datetime.date): + raise TypeError( + "Cannot convert to datetime expected date value; " + "received {}".format(value) + ) + return datetime.datetime(value.year, value.month, value.day) + + def _from_base_type(self, value): + """Convert a value from the "base" value type for this property. + + Args: + value (~datetime.datetime): The value to be converted. + + Returns: + ~datetime.date: The converted value: the date that ``value`` + occurs on. + """ + return value.date() + + @staticmethod + def _now(): + """datetime.datetime: Return current date.""" + return datetime.datetime.utcnow().date() class TimeProperty(DateTimeProperty): + """A property that contains :class:`~datetime.time` values. + + .. automethod:: _to_base_type + .. automethod:: _from_base_type + .. automethod:: _validate + """ + __slots__ = () - def __init__(self, *args, **kwargs): - raise NotImplementedError + def _validate(self, value): + """Validate a ``value`` before setting it. + + Args: + value (~datetime.time): The value to check. + + Raises: + .BadValueError: If ``value`` is not a :class:`~datetime.time`. + """ + if not isinstance(value, datetime.time): + raise exceptions.BadValueError( + "Expected time, got {!r}".format(value) + ) + + def _to_base_type(self, value): + """Convert a value to the "base" value type for this property. + + Args: + value (~datetime.time): The value to be converted. + + Returns: + ~datetime.datetime: The converted value: a datetime object with the + date set to ``1970-01-01``. + + Raises: + TypeError: If ``value`` is not a :class:`~datetime.time`. + """ + if not isinstance(value, datetime.time): + raise TypeError( + "Cannot convert to datetime expected time value; " + "received {}".format(value) + ) + return datetime.datetime( + 1970, + 1, + 1, + value.hour, + value.minute, + value.second, + value.microsecond, + ) + + def _from_base_type(self, value): + """Convert a value from the "base" value type for this property. + + Args: + value (~datetime.datetime): The value to be converted. + + Returns: + ~datetime.time: The converted value: the time that ``value`` + occurs at. + """ + return value.time() + + @staticmethod + def _now(): + """datetime.datetime: Return current time.""" + return datetime.datetime.utcnow().time() class StructuredProperty(Property): diff --git a/ndb/tests/unit/test_model.py b/ndb/tests/unit/test_model.py index 0901caadda81..a66e731e800d 100644 --- a/ndb/tests/unit/test_model.py +++ b/ndb/tests/unit/test_model.py @@ -1917,16 +1917,74 @@ def test__db_get_value(): class TestDateProperty: @staticmethod - def test_constructor(): - with pytest.raises(NotImplementedError): - model.DateProperty() + def test__validate(): + prop = model.DateProperty(name="d_val") + value = datetime.datetime.utcnow().date() + assert prop._validate(value) is None + + @staticmethod + def test__validate_invalid(): + prop = model.DateProperty(name="d_val") + with pytest.raises(exceptions.BadValueError): + prop._validate(None) + + @staticmethod + def test__now(): + d_val = model.DateProperty._now() + assert isinstance(d_val, datetime.date) + + def test__to_base_type(self): + prop = model.DateProperty(name="d_val") + value = datetime.date(2014, 10, 7) + expected = datetime.datetime(2014, 10, 7) + assert prop._to_base_type(value) == expected + + def test__to_base_type_invalid(self): + prop = model.DateProperty(name="d_val") + with pytest.raises(TypeError): + prop._to_base_type(None) + + def test__from_base_type(self): + prop = model.DateProperty(name="d_val") + value = datetime.datetime(2014, 10, 7) + expected = datetime.date(2014, 10, 7) + assert prop._from_base_type(value) == expected class TestTimeProperty: @staticmethod - def test_constructor(): - with pytest.raises(NotImplementedError): - model.TimeProperty() + def test__validate(): + prop = model.TimeProperty(name="t_val") + value = datetime.datetime.utcnow().time() + assert prop._validate(value) is None + + @staticmethod + def test__validate_invalid(): + prop = model.TimeProperty(name="t_val") + with pytest.raises(exceptions.BadValueError): + prop._validate(None) + + @staticmethod + def test__now(): + t_val = model.TimeProperty._now() + assert isinstance(t_val, datetime.time) + + def test__to_base_type(self): + prop = model.TimeProperty(name="t_val") + value = datetime.time(17, 57, 18, 453529) + expected = datetime.datetime(1970, 1, 1, 17, 57, 18, 453529) + assert prop._to_base_type(value) == expected + + def test__to_base_type_invalid(self): + prop = model.TimeProperty(name="t_val") + with pytest.raises(TypeError): + prop._to_base_type(None) + + def test__from_base_type(self): + prop = model.TimeProperty(name="t_val") + value = datetime.datetime(1970, 1, 1, 1, 15, 59, 900101) + expected = datetime.time(1, 15, 59, 900101) + assert prop._from_base_type(value) == expected class TestStructuredProperty: