- Add `colander.DateTime and colander.Date` data types.
- Depend on the ``iso8601`` package for date support.
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
Next release
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Add ``colander.DateTime`` and ``colander.Date`` data types.
|
||||||
|
|
||||||
|
- Depend on the ``iso8601`` package for date support.
|
||||||
|
|
||||||
0.3 (2010-03-29)
|
0.3 (2010-03-29)
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
|
import iso8601
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
class _missing(object):
|
class _missing(object):
|
||||||
@@ -580,6 +582,102 @@ class GlobalObject(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise Invalid(node, '%r has no __name__' % value)
|
raise Invalid(node, '%r has no __name__' % value)
|
||||||
|
|
||||||
|
class DateTime(object):
|
||||||
|
""" A type representing a Python ``datetime.datetime`` object.
|
||||||
|
|
||||||
|
This type serializes python ``datetime.datetime`` objects to a
|
||||||
|
`ISO8601 <http://en.wikipedia.org/wiki/ISO_8601>`_ string format.
|
||||||
|
The format includes the date, the time, and the timezone of the
|
||||||
|
datetime.
|
||||||
|
|
||||||
|
The constructor accepts a single argument named ``default_tzinfo``
|
||||||
|
which should be a Python ``tzinfo`` object; by default it is None,
|
||||||
|
meaning that the default tzinfo will be equivalent to UTC (Zulu
|
||||||
|
time). The ``default_tzinfo`` tzinfo object is used to convert
|
||||||
|
'naive' datetimes to a timezone-aware representation during
|
||||||
|
serialization.
|
||||||
|
|
||||||
|
For convenience, this type is also willing to coerce
|
||||||
|
``datetime.date`` objects to a DateTime ISO string representation
|
||||||
|
during serialization. It does so by using midnight of the day as
|
||||||
|
the time, and uses the ``default_tzinfo`` to give the
|
||||||
|
serialization a timezone.
|
||||||
|
|
||||||
|
This type can only convert ISO8601 values that include a date, a
|
||||||
|
time, and a timezone (or ``Z``) in their representations. It will
|
||||||
|
fail to parse date-only ISO8601 representations.
|
||||||
|
|
||||||
|
The subnodes of the :class:`colander.SchemaNode` that wraps
|
||||||
|
this type are ignored.
|
||||||
|
"""
|
||||||
|
def __init__(self, default_tzinfo=None):
|
||||||
|
if default_tzinfo is None:
|
||||||
|
default_tzinfo = iso8601.iso8601.Utc()
|
||||||
|
self.default_tzinfo = default_tzinfo
|
||||||
|
|
||||||
|
def serialize(self, node, value):
|
||||||
|
if type(value) is datetime.date: # cant use isinstance; dt subs date
|
||||||
|
value = datetime.datetime.combine(value, datetime.time())
|
||||||
|
if not isinstance(value, datetime.datetime):
|
||||||
|
raise Invalid(node, '%r is not a datetime object' % value)
|
||||||
|
if value.tzinfo is None:
|
||||||
|
value = value.replace(tzinfo=self.default_tzinfo)
|
||||||
|
return value.isoformat()
|
||||||
|
|
||||||
|
def deserialize(self, node, value):
|
||||||
|
try:
|
||||||
|
result = iso8601.parse_date(value)
|
||||||
|
except (iso8601.ParseError, TypeError), e:
|
||||||
|
raise Invalid(node,
|
||||||
|
'%s cannot be parsed as an iso8601 datetime: %s' %
|
||||||
|
(value, e))
|
||||||
|
return result
|
||||||
|
|
||||||
|
class Date(object):
|
||||||
|
""" A type representing a Python ``datetime.date`` object.
|
||||||
|
|
||||||
|
This type serializes python ``datetime.date`` objects to a
|
||||||
|
`ISO8601 <http://en.wikipedia.org/wiki/ISO_8601>`_ string format.
|
||||||
|
The format includes the date only.
|
||||||
|
|
||||||
|
The constructor accepts no arguments.
|
||||||
|
|
||||||
|
For convenience, this type is also willing to coerce
|
||||||
|
``datetime.datetime`` objects to a date-only ISO string
|
||||||
|
representation during serialization. It does so by stripping off
|
||||||
|
any time information, converting the ``datetime.datetime`` into a
|
||||||
|
date before serializing.
|
||||||
|
|
||||||
|
Likewise, for convenience, this type is also willing to coerce ISO
|
||||||
|
representations that contain time info into a ``datetime.date``
|
||||||
|
object during deserialization. It does so by throwing away any
|
||||||
|
time information related to the serialized value during
|
||||||
|
deserialization.
|
||||||
|
|
||||||
|
The subnodes of the :class:`colander.SchemaNode` that wraps
|
||||||
|
this type are ignored.
|
||||||
|
"""
|
||||||
|
def serialize(self, node, value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
value = value.date()
|
||||||
|
if not isinstance(value, datetime.date):
|
||||||
|
raise Invalid(node, '%r is not a date object' % value)
|
||||||
|
return value.isoformat()
|
||||||
|
|
||||||
|
def deserialize(self, node, value):
|
||||||
|
try:
|
||||||
|
result = iso8601.parse_date(value)
|
||||||
|
result = result.date()
|
||||||
|
except (iso8601.ParseError, TypeError):
|
||||||
|
try:
|
||||||
|
year, month, day = map(int, value.split('-', 3))
|
||||||
|
result = datetime.date(year, month, day)
|
||||||
|
except Exception, e:
|
||||||
|
raise Invalid(node,
|
||||||
|
'%s cannot be parsed as an iso8601 date: %s' %
|
||||||
|
(value, e))
|
||||||
|
return result
|
||||||
|
|
||||||
class SchemaNode(object):
|
class SchemaNode(object):
|
||||||
"""
|
"""
|
||||||
Fundamental building block of schemas.
|
Fundamental building block of schemas.
|
||||||
|
|||||||
@@ -899,6 +899,146 @@ class TestGlobalObject(unittest.TestCase):
|
|||||||
e = invalid_exc(typ.serialize, None, None)
|
e = invalid_exc(typ.serialize, None, None)
|
||||||
self.assertEqual(e.msg, 'None has no __name__')
|
self.assertEqual(e.msg, 'None has no __name__')
|
||||||
|
|
||||||
|
class TestDateTime(unittest.TestCase):
|
||||||
|
def _makeOne(self, *arg, **kw):
|
||||||
|
from colander import DateTime
|
||||||
|
return DateTime(*arg, **kw)
|
||||||
|
|
||||||
|
def test_ctor_default_tzinfo_None(self):
|
||||||
|
import iso8601
|
||||||
|
typ = self._makeOne()
|
||||||
|
self.assertEqual(typ.default_tzinfo.__class__, iso8601.iso8601.Utc)
|
||||||
|
|
||||||
|
def test_ctor_default_tzinfo_non_None(self):
|
||||||
|
import iso8601
|
||||||
|
tzinfo = iso8601.iso8601.FixedOffset(1, 0, 'myname')
|
||||||
|
typ = self._makeOne(default_tzinfo=tzinfo)
|
||||||
|
self.assertEqual(typ.default_tzinfo, tzinfo)
|
||||||
|
|
||||||
|
def test_serialize_with_garbage(self):
|
||||||
|
typ = self._makeOne()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
e = invalid_exc(typ.serialize, node, 'garbage')
|
||||||
|
self.assertEqual(e.msg, "'garbage' is not a datetime object")
|
||||||
|
|
||||||
|
def test_serialize_with_date(self):
|
||||||
|
import datetime
|
||||||
|
typ = self._makeOne()
|
||||||
|
date = datetime.date.today()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.serialize(node, date)
|
||||||
|
expected = datetime.datetime.combine(date, datetime.time())
|
||||||
|
expected = expected.replace(tzinfo=typ.default_tzinfo).isoformat()
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_serialize_with_naive_datetime(self):
|
||||||
|
import datetime
|
||||||
|
typ = self._makeOne()
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.serialize(node, dt)
|
||||||
|
expected = dt.replace(tzinfo=typ.default_tzinfo).isoformat()
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_serialize_with_tzware_datetime(self):
|
||||||
|
import datetime
|
||||||
|
import iso8601
|
||||||
|
typ = self._makeOne()
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
tzinfo = iso8601.iso8601.FixedOffset(1, 0, 'myname')
|
||||||
|
dt = dt.replace(tzinfo=tzinfo)
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.serialize(node, dt)
|
||||||
|
expected = dt.isoformat()
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_deserialize_invalid_TypeError(self):
|
||||||
|
import datetime
|
||||||
|
# cant parse dates without times
|
||||||
|
date = datetime.date.today()
|
||||||
|
typ = self._makeOne()
|
||||||
|
formatted = date.isoformat()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
e = invalid_exc(typ.deserialize, node, formatted)
|
||||||
|
self.failUnless('cannot be parsed' in e.msg)
|
||||||
|
|
||||||
|
def test_deserialize_invalid_ParseError(self):
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
typ = self._makeOne()
|
||||||
|
e = invalid_exc(typ.deserialize, node, 'garbage')
|
||||||
|
self.failUnless('Unable to parse' in e.msg)
|
||||||
|
|
||||||
|
def test_deserialize_success(self):
|
||||||
|
import datetime
|
||||||
|
import iso8601
|
||||||
|
typ = self._makeOne()
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
tzinfo = iso8601.iso8601.FixedOffset(1, 0, 'myname')
|
||||||
|
dt = dt.replace(tzinfo=tzinfo)
|
||||||
|
iso = dt.isoformat()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.deserialize(node, iso)
|
||||||
|
self.assertEqual(result.isoformat(), iso)
|
||||||
|
|
||||||
|
class TestDate(unittest.TestCase):
|
||||||
|
def _makeOne(self, *arg, **kw):
|
||||||
|
from colander import Date
|
||||||
|
return Date(*arg, **kw)
|
||||||
|
|
||||||
|
def test_serialize_with_garbage(self):
|
||||||
|
typ = self._makeOne()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
e = invalid_exc(typ.serialize, node, 'garbage')
|
||||||
|
self.assertEqual(e.msg, "'garbage' is not a date object")
|
||||||
|
|
||||||
|
def test_serialize_with_date(self):
|
||||||
|
import datetime
|
||||||
|
typ = self._makeOne()
|
||||||
|
date = datetime.date.today()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.serialize(node, date)
|
||||||
|
expected = date.isoformat()
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_serialize_with_datetime(self):
|
||||||
|
import datetime
|
||||||
|
typ = self._makeOne()
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.serialize(node, dt)
|
||||||
|
expected = dt.date().isoformat()
|
||||||
|
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('cannot be parsed' in e.msg)
|
||||||
|
|
||||||
|
def test_deserialize_invalid_weird(self):
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
typ = self._makeOne()
|
||||||
|
e = invalid_exc(typ.deserialize, node, '10-10-10-10')
|
||||||
|
self.failUnless('cannot be parsed' in e.msg)
|
||||||
|
|
||||||
|
def test_deserialize_success_date(self):
|
||||||
|
import datetime
|
||||||
|
typ = self._makeOne()
|
||||||
|
date = datetime.date.today()
|
||||||
|
iso = date.isoformat()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.deserialize(node, iso)
|
||||||
|
self.assertEqual(result.isoformat(), iso)
|
||||||
|
|
||||||
|
def test_deserialize_success_datetime(self):
|
||||||
|
import datetime
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
typ = self._makeOne()
|
||||||
|
iso = dt.isoformat()
|
||||||
|
node = DummySchemaNode(None)
|
||||||
|
result = typ.deserialize(node, iso)
|
||||||
|
self.assertEqual(result.isoformat(), dt.date().isoformat())
|
||||||
|
|
||||||
class TestSchemaNode(unittest.TestCase):
|
class TestSchemaNode(unittest.TestCase):
|
||||||
def _makeOne(self, *arg, **kw):
|
def _makeOne(self, *arg, **kw):
|
||||||
from colander import SchemaNode
|
from colander import SchemaNode
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ Types
|
|||||||
|
|
||||||
.. autoclass:: GlobalObject
|
.. autoclass:: GlobalObject
|
||||||
|
|
||||||
|
.. autoclass:: DateTime
|
||||||
|
|
||||||
|
.. autoclass:: Date
|
||||||
|
|
||||||
Schema-Related
|
Schema-Related
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -21,7 +21,7 @@ here = os.path.abspath(os.path.dirname(__file__))
|
|||||||
README = open(os.path.join(here, 'README.txt')).read()
|
README = open(os.path.join(here, 'README.txt')).read()
|
||||||
CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
|
CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
|
||||||
|
|
||||||
requires = []
|
requires = ['iso8601']
|
||||||
|
|
||||||
setup(name='colander',
|
setup(name='colander',
|
||||||
version='0.3',
|
version='0.3',
|
||||||
|
|||||||
Reference in New Issue
Block a user