From 98b2e91ebf3be52383437cf6f67fd5ff934f7ebd Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Wed, 22 Jun 2011 10:51:56 +0200 Subject: [PATCH] Add Time type. --- CHANGES.txt | 2 ++ colander/__init__.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ colander/tests.py | 79 +++++++++++++++++++++++++++++++++++++++++++ docs/api.rst | 2 ++ 4 files changed, 163 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 5282d94..ba28756 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,8 @@ Changes Next release ------------ +- Add ``Time`` type. + - Add Dutch translation. - Fix documentation: 0.9.2 requires ``deserialize`` of types to explicitly diff --git a/colander/__init__.py b/colander/__init__.py index c579c89..015aa4a 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1204,6 +1204,86 @@ class Date(SchemaType): ) return result +class Time(SchemaType): + """ A type representing a Python ``datetime.time`` object. + + This type serializes python ``datetime.time`` objects to a + `ISO8601 `_ string format. + The format includes the date only. + + The constructor accepts no arguments. + + You can adjust the error message reported by this class by + changing its ``err_template`` attribute in a subclass on an + instance of this class. By default, the ``err_template`` + attribute is the string ``Invalid date``. This string is used as + the interpolation subject of a dictionary composed of ``val`` and + ``err``. ``val`` and ``err`` are the unvalidatable value and the + exception caused trying to convert the value, respectively. These + may be used in an overridden err_template as ``${val}`` and + ``${err}`` respectively as necessary, e.g. ``_('${val} cannot be + parsed as an iso8601 date: ${err}')``. + + For convenience, this type is also willing to coerce + ``datetime.datetime`` objects to a time-only ISO string + representation during serialization. It does so by stripping off + any date information, converting the ``datetime.datetime`` into a + time before serializing. + + Likewise, for convenience, this type is also willing to coerce ISO + representations that contain time info into a ``datetime.time`` + object during deserialization. It does so by throwing away any + date information related to the serialized value during + deserialization. + + If the :attr:`colander.null` value is passed to the serialize + method of this class, the :attr:`colander.null` value will be + returned. + + The subnodes of the :class:`colander.SchemaNode` that wraps + this type are ignored. + """ + + err_template = _('Invalid time') + + def serialize(self, node, appstruct): + if appstruct is null: + return null + + if isinstance(appstruct, datetime.datetime): + appstruct = appstruct.time() + + if not isinstance(appstruct, datetime.time): + raise Invalid(node, + _('"${val}" is not a time object', + mapping={'val':appstruct}) + ) + + return appstruct.isoformat().split('.')[0] + + def deserialize(self, node, cstruct): + if not cstruct: + return null + try: + result = iso8601.parse_date(cstruct) + result = result.time() + except (iso8601.ParseError, TypeError): + try: + result = iso8601.parse_date('1970-01-01 %s' % cstruct) + result = result.time() + except (iso8601.ParseError, TypeError): + try: + parts = map(int, cstruct.split(':')) + if len(parts) > 3: + raise ValueError('Too many digits') + result = datetime.date(*parts[:3]) + except Exception, e: + raise Invalid(node, + _(self.err_template, + mapping={'val':cstruct, 'err':e}) + ) + return result + class SchemaNode(object): """ Fundamental building block of schemas. diff --git a/colander/tests.py b/colander/tests.py index f3c457e..8a4c03d 100644 --- a/colander/tests.py +++ b/colander/tests.py @@ -1424,6 +1424,85 @@ class TestDate(unittest.TestCase): result = typ.deserialize(node, iso) self.assertEqual(result.isoformat(), dt.date().isoformat()) +class TestTime(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from colander import Time + return Time(*arg, **kw) + + def _dt(self): + import datetime + return datetime.datetime(2010, 4, 26, 10, 48) + + def _now(self): + import datetime + return datetime.datetime.now().time() + + def test_serialize_null(self): + import colander + val = colander.null + node = DummySchemaNode(None) + typ = self._makeOne() + result = typ.serialize(node, val) + self.assertEqual(result, colander.null) + + def test_serialize_with_garbage(self): + typ = self._makeOne() + node = DummySchemaNode(None) + e = invalid_exc(typ.serialize, node, 'garbage') + self.assertEqual(e.msg.interpolate(), '"garbage" is not a time object') + + def test_serialize_with_time(self): + typ = self._makeOne() + time = self._now() + node = DummySchemaNode(None) + result = typ.serialize(node, time) + expected = time.isoformat().split('.')[0] + self.assertEqual(result, expected) + + def test_serialize_with_datetime(self): + typ = self._makeOne() + dt = self._dt() + node = DummySchemaNode(None) + result = typ.serialize(node, dt) + expected = dt.time().isoformat().split('.')[0] + self.assertEqual(result, expected) + + def test_deserialize_invalid_ParseError(self): + node = DummySchemaNode(None) + typ = self._makeOne() + e = invalid_exc(typ.deserialize, node, 'garbage') + self.failUnless('Invalid' in e.msg) + + def test_deserialize_null(self): + import colander + node = DummySchemaNode(None) + typ = self._makeOne() + result = typ.deserialize(node, colander.null) + self.assertEqual(result, colander.null) + + def test_deserialize_empty(self): + import colander + node = DummySchemaNode(None) + typ = self._makeOne() + result = typ.deserialize(node, '') + self.assertEqual(result, colander.null) + + def test_deserialize_success_time(self): + import datetime + typ = self._makeOne() + node = DummySchemaNode(None) + result = typ.deserialize(node, '10:12:13') + self.assertEqual(result, datetime.time(10, 12, 13)) + + def test_deserialize_success_datetime(self): + dt = self._dt() + typ = self._makeOne() + iso = dt.isoformat() + node = DummySchemaNode(None) + result = typ.deserialize(node, iso) + self.assertEqual(result.isoformat(), + dt.time().isoformat().split('.')[0]) + class TestSchemaNode(unittest.TestCase): def _makeOne(self, *arg, **kw): from colander import SchemaNode diff --git a/docs/api.rst b/docs/api.rst index 8ab97f5..e92ec02 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -98,6 +98,8 @@ Types .. autoclass:: Date + .. autoclass:: Time + Schema-Related ~~~~~~~~~~~~~~