diff options
author | Olivier Tilloy <olivier@tilloy.net> | 2011-08-12 08:29:47 +0200 |
---|---|---|
committer | Olivier Tilloy <olivier@tilloy.net> | 2011-08-12 08:29:47 +0200 |
commit | aecc90f8bc3c9739d818749fd1bc458884300148 (patch) | |
tree | ec913e0c133aac45ac9818bc3afc4942074d6817 | |
parent | 75693ed13e3ed48e2f0636b1681948b27ae8f312 (diff) | |
download | pyexiv2-aecc90f8bc3c9739d818749fd1bc458884300148.tar.gz |
Custom DateTimeFormatter helper to convert date/time objects to strings conforming to the EXIF/IPTC/XMP formats.
-rw-r--r-- | src/pyexiv2/exif.py | 11 | ||||
-rw-r--r-- | src/pyexiv2/iptc.py | 24 | ||||
-rw-r--r-- | src/pyexiv2/utils.py | 156 | ||||
-rw-r--r-- | src/pyexiv2/xmp.py | 28 | ||||
-rwxr-xr-x | test/TestsRunner.py | 4 | ||||
-rw-r--r-- | test/datetimeformatter.py | 190 |
6 files changed, 364 insertions, 49 deletions
diff --git a/src/pyexiv2/exif.py b/src/pyexiv2/exif.py index b0c6374..1d56be8 100644 --- a/src/pyexiv2/exif.py +++ b/src/pyexiv2/exif.py @@ -2,7 +2,7 @@ # ****************************************************************************** # -# Copyright (C) 2006-2010 Olivier Tilloy <olivier@tilloy.net> +# Copyright (C) 2006-2011 Olivier Tilloy <olivier@tilloy.net> # # This file is part of the pyexiv2 distribution. # @@ -32,7 +32,8 @@ import libexiv2python from pyexiv2.utils import is_fraction, make_fraction, fraction_to_string, \ NotifyingList, ListenerInterface, \ - undefined_to_string, string_to_undefined + undefined_to_string, string_to_undefined, \ + DateTimeFormatter import time import datetime @@ -347,13 +348,13 @@ class ExifTag(ListenerInterface): """ if self.type == 'Ascii': if isinstance(value, datetime.datetime): - return value.strftime(self._datetime_formats[0]) + return DateTimeFormatter.exif(value) elif isinstance(value, datetime.date): if self.key == 'Exif.GPSInfo.GPSDateStamp': # Special case - return value.strftime(self._date_formats[0]) + return DateTimeFormatter.exif(value) else: - return value.strftime('%s 00:00:00' % self._date_formats[0]) + return '%s 00:00:00' % DateTimeFormatter.exif(value) elif isinstance(value, unicode): try: return value.encode('utf-8') diff --git a/src/pyexiv2/iptc.py b/src/pyexiv2/iptc.py index f2a360d..c6ed36f 100644 --- a/src/pyexiv2/iptc.py +++ b/src/pyexiv2/iptc.py @@ -2,7 +2,7 @@ # ****************************************************************************** # -# Copyright (C) 2006-2010 Olivier Tilloy <olivier@tilloy.net> +# Copyright (C) 2006-2011 Olivier Tilloy <olivier@tilloy.net> # # This file is part of the pyexiv2 distribution. # @@ -30,7 +30,8 @@ IPTC specific code. import libexiv2python -from pyexiv2.utils import ListenerInterface, NotifyingList, FixedOffset +from pyexiv2.utils import ListenerInterface, NotifyingList, \ + FixedOffset, DateTimeFormatter import time import datetime @@ -335,28 +336,13 @@ class IptcTag(ListenerInterface): elif self.type == 'Date': if isinstance(value, (datetime.date, datetime.datetime)): - # ISO 8601 date format. - # According to the IPTC specification, the format for a string - # field representing a date is '%Y%m%d'. However, the string - # expected by exiv2's DateValue::read(string) should be - # formatted using pattern '%Y-%m-%d'. - return value.strftime('%Y-%m-%d') + return DateTimeFormatter.iptc_date(value) else: raise IptcValueError(value, self.type) elif self.type == 'Time': if isinstance(value, (datetime.time, datetime.datetime)): - # According to the IPTC specification, the format for a string - # field representing a time is '%H%M%S±%H%M'. However, the - # string expected by exiv2's TimeValue::read(string) should be - # formatted using pattern '%H:%M:%S±%H:%M'. - r = value.strftime('%H:%M:%S') - if value.tzinfo is not None: - s = value.strftime('%z') # of the form ±%H%M - r += s[:3] + ':' + s[3:] - else: - r += '+00:00' - return r + return DateTimeFormatter.iptc_time(value) else: raise IptcValueError(value, self.type) diff --git a/src/pyexiv2/utils.py b/src/pyexiv2/utils.py index de56c21..9473aff 100644 --- a/src/pyexiv2/utils.py +++ b/src/pyexiv2/utils.py @@ -2,7 +2,7 @@ # ****************************************************************************** # -# Copyright (C) 2006-2010 Olivier Tilloy <olivier@tilloy.net> +# Copyright (C) 2006-2011 Olivier Tilloy <olivier@tilloy.net> # # This file is part of the pyexiv2 distribution. # @@ -573,3 +573,157 @@ class GPSCoordinate(object): return '%d,%d,%d%s' % (self._degrees, self._minutes, self._seconds, self._direction) + +class DateTimeFormatter(object): + + """ + Convenience object that exposes static methods to convert a date, time or + datetime object to a string representation suitable for various metadata + standards. + + This is needed because python’s + `strftime() <http://docs.python.org/library/datetime.html#strftime-strptime-behavior>`_ + doesn’t work for years before 1900. + + This class mostly exists for internal usage only. Clients should never need + to use it. + """ + + @staticmethod + def timedelta_to_offset(t): + """ + Convert a time delta to a string representation in the form ``±%H:%M``. + + :param t: a time delta + :type t: :class:`datetime.timedelta` + + :return: a string representation of the time delta in the form + ``±%H:%M`` + :rtype: string + """ + seconds = t.total_seconds() + hours = int(seconds / 3600) + minutes = abs(int((seconds - hours * 3600) / 60)) + return '%+03d:%02d' % (hours, minutes) + + @staticmethod + def exif(d): + """ + Convert a date/time object to a string representation conforming to + libexiv2’s internal representation for the EXIF standard. + + :param d: a datetime or date object + :type d: :class:`datetime.datetime` or :class:`datetime.date` + + :return: a string representation conforming to the EXIF standard + :rtype: string + + :raise TypeError: if the parameter is not a datetime or a date object + """ + if isinstance(d, datetime.datetime): + return '%04d:%02d:%02d %02d:%02d:%02d' % \ + (d.year, d.month, d.day, d.hour, d.minute, d.second) + elif isinstance(d, datetime.date): + return '%04d:%02d:%02d' % (d.year, d.month, d.day) + else: + raise TypeError('expecting an object of type ' + 'datetime.datetime or datetime.date') + + @staticmethod + def iptc_date(d): + """ + Convert a date object to a string representation conforming to + libexiv2’s internal representation for the IPTC standard. + + :param d: a datetime or date object + :type d: :class:`datetime.datetime` or :class:`datetime.date` + + :return: a string representation conforming to the IPTC standard + :rtype: string + + :raise TypeError: if the parameter is not a datetime or a date object + """ + if isinstance(d, (datetime.date, datetime.datetime)): + # ISO 8601 date format. + # According to the IPTC specification, the format for a string + # field representing a date is '%Y%m%d'. However, the string + # expected by exiv2's DateValue::read(string) should be + # formatted using pattern '%Y-%m-%d'. + return '%04d-%02d-%02d' % (d.year, d.month, d.day) + else: + raise TypeError('expecting an object of type ' + 'datetime.datetime or datetime.date') + + @staticmethod + def iptc_time(d): + """ + Convert a time object to a string representation conforming to + libexiv2’s internal representation for the IPTC standard. + + :param d: a datetime or time object + :type d: :class:`datetime.datetime` or :class:`datetime.time` + + :return: a string representation conforming to the IPTC standard + :rtype: string + + :raise TypeError: if the parameter is not a datetime or a time object + """ + if isinstance(d, (datetime.time, datetime.datetime)): + # According to the IPTC specification, the format for a string + # field representing a time is '%H%M%S±%H%M'. However, the + # string expected by exiv2's TimeValue::read(string) should be + # formatted using pattern '%H:%M:%S±%H:%M'. + r = '%02d:%02d:%02d' % (d.hour, d.minute, d.second) + if d.tzinfo is not None: + t = d.utcoffset() + if t is not None: + r += DateTimeFormatter.timedelta_to_offset(t) + else: + r += '+00:00' + return r + else: + raise TypeError('expecting an object of type ' + 'datetime.datetime or datetime.time') + + @staticmethod + def xmp(d): + """ + Convert a date/time object to a string representation conforming to + libexiv2’s internal representation for the XMP standard. + + :param d: a datetime or date object + :type d: :class:`datetime.datetime` or :class:`datetime.date` + + :return: a string representation conforming to the XMP standard + :rtype: string + + :raise TypeError: if the parameter is not a datetime or a date object + """ + if isinstance(d, datetime.datetime): + t = d.utcoffset() + if d.tzinfo is None or t is None or t == datetime.timedelta(0): + tz = 'Z' + else: + tz = DateTimeFormatter.timedelta_to_offset(t) + if d.hour == 0 and d.minute == 0 and \ + d.second == 0 and d.microsecond == 0 and \ + (d.tzinfo is None or d.utcoffset() == datetime.timedelta(0)): + return '%04d-%02d-%02d' % (d.year, d.month, d.day) + elif d.second == 0 and d.microsecond == 0: + return '%04d-%02d-%02dT%02d:%02d%s' % \ + (d.year, d.month, d.day, d.hour, d.minute, tz) + elif d.microsecond == 0: + return '%04d-%02d-%02dT%02d:%02d:%02d%s' % \ + (d.year, d.month, d.day, d.hour, d.minute, d.second, tz) + else: + r = '%04d-%02d-%02dT%02d:%02d:%02d.' % \ + (d.year, d.month, d.day, d.hour, d.minute, d.second) + r += str(int(d.microsecond) / 1E6)[2:] + r += tz + return r + elif isinstance(d, datetime.date): + return '%04d-%02d-%02d' % (d.year, d.month, d.day) + else: + raise TypeError('expecting an object of type ' + 'datetime.datetime or datetime.date') + diff --git a/src/pyexiv2/xmp.py b/src/pyexiv2/xmp.py index 7240527..bed1b66 100644 --- a/src/pyexiv2/xmp.py +++ b/src/pyexiv2/xmp.py @@ -2,7 +2,7 @@ # ****************************************************************************** # -# Copyright (C) 2006-2010 Olivier Tilloy <olivier@tilloy.net> +# Copyright (C) 2006-2011 Olivier Tilloy <olivier@tilloy.net> # # This file is part of the pyexiv2 distribution. # @@ -30,7 +30,8 @@ XMP specific code. import libexiv2python -from pyexiv2.utils import FixedOffset, is_fraction, make_fraction, GPSCoordinate +from pyexiv2.utils import FixedOffset, is_fraction, make_fraction, \ + GPSCoordinate, DateTimeFormatter import datetime import re @@ -387,27 +388,8 @@ class XmpTag(object): raise XmpValueError(value, type) elif type == 'Date': - if isinstance(value, datetime.datetime): - if value.tzinfo is None or value.utcoffset() == datetime.timedelta(0): - tz = 'Z' - else: - tz = value.strftime('%z') # of the form ±%H%M - tz = tz[:3] + ':' + tz[3:] - if value.hour == 0 and value.minute == 0 and \ - value.second == 0 and value.microsecond == 0 and \ - (value.tzinfo is None or value.utcoffset() == datetime.timedelta(0)): - return value.strftime('%Y-%m-%d') - elif value.second == 0 and value.microsecond == 0: - return value.strftime('%Y-%m-%dT%H:%M') + tz - elif value.microsecond == 0: - return value.strftime('%Y-%m-%dT%H:%M:%S') + tz - else: - r = value.strftime('%Y-%m-%dT%H:%M:%S.') - r += str(int(value.microsecond) / 1E6)[2:] - r += tz - return r - elif isinstance(value, datetime.date): - return value.isoformat() + if isinstance(value, (datetime.date, datetime.datetime)): + return DateTimeFormatter.xmp(value) else: raise XmpValueError(value, type) diff --git a/test/TestsRunner.py b/test/TestsRunner.py index ba5162c..9b7d9e5 100755 --- a/test/TestsRunner.py +++ b/test/TestsRunner.py @@ -3,7 +3,7 @@ # ****************************************************************************** # -# Copyright (C) 2008-2010 Olivier Tilloy <olivier@tilloy.net> +# Copyright (C) 2008-2011 Olivier Tilloy <olivier@tilloy.net> # # This file is part of the pyexiv2 distribution. # @@ -41,6 +41,7 @@ from encoding import TestEncodings from utils import TestConversions, TestFractions from usercomment import TestUserCommentReadWrite, TestUserCommentAdd from pickling import TestPicklingTags +from datetimeformatter import TestDateTimeFormatter def run_unit_tests(): @@ -62,6 +63,7 @@ def run_unit_tests(): suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestUserCommentReadWrite)) suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestUserCommentAdd)) suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestPicklingTags)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestDateTimeFormatter)) # Run the test suite return unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/datetimeformatter.py b/test/datetimeformatter.py new file mode 100644 index 0000000..2626713 --- /dev/null +++ b/test/datetimeformatter.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +# ****************************************************************************** +# +# Copyright (C) 2011 Olivier Tilloy <olivier@tilloy.net> +# +# This file is part of the pyexiv2 distribution. +# +# pyexiv2 is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# pyexiv2 is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyexiv2; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. +# +# Author: Olivier Tilloy <olivier@tilloy.net> +# +# ****************************************************************************** + +import unittest + +from pyexiv2.utils import DateTimeFormatter, FixedOffset + +import datetime + + +class TestDateTimeFormatter(unittest.TestCase): + + def test_timedelta_to_offset(self): + # positive deltas + t = datetime.timedelta(hours=5) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '+05:00') + t = datetime.timedelta(minutes=300) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '+05:00') + t = datetime.timedelta(hours=5, minutes=12) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '+05:12') + t = datetime.timedelta(seconds=10800) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '+03:00') + + # negative deltas + t = datetime.timedelta(hours=-4) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '-04:00') + t = datetime.timedelta(minutes=-258) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '-04:18') + t = datetime.timedelta(hours=-2, minutes=-12) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '-02:12') + t = datetime.timedelta(seconds=-10000) + self.assertEqual(DateTimeFormatter.timedelta_to_offset(t), '-02:46') + + def test_exif(self): + # datetime + d = datetime.datetime(1899, 12, 31) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 00:00:00') + d = datetime.datetime(1899, 12, 31, 23) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:00:00') + d = datetime.datetime(1899, 12, 31, 23, 59) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:59:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:59:59') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:59:59') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:59:59') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset(hours=5)) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31 23:59:59') + d = datetime.datetime(2011, 8, 8, 19, 3, 37) + self.assertEqual(DateTimeFormatter.exif(d), '2011:08:08 19:03:37') + + # date + d = datetime.date(1899, 12, 31) + self.assertEqual(DateTimeFormatter.exif(d), '1899:12:31') + d = datetime.date(2011, 8, 8) + self.assertEqual(DateTimeFormatter.exif(d), '2011:08:08') + + # invalid type + self.assertRaises(TypeError, DateTimeFormatter.exif, None) + self.assertRaises(TypeError, DateTimeFormatter.exif, 3.14) + + def test_iptc_date(self): + # datetime + d = datetime.datetime(1899, 12, 31) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59, 59) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset(hours=5)) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.datetime(2011, 8, 8, 19, 3, 37) + self.assertEqual(DateTimeFormatter.iptc_date(d), '2011-08-08') + + # date + d = datetime.date(1899, 12, 31) + self.assertEqual(DateTimeFormatter.iptc_date(d), '1899-12-31') + d = datetime.date(2011, 8, 8) + self.assertEqual(DateTimeFormatter.iptc_date(d), '2011-08-08') + + # invalid type + self.assertRaises(TypeError, DateTimeFormatter.iptc_date, None) + self.assertRaises(TypeError, DateTimeFormatter.iptc_date, 3.14) + + def test_iptc_time(self): + # datetime + d = datetime.datetime(1899, 12, 31) + self.assertEqual(DateTimeFormatter.iptc_time(d), '00:00:00+00:00') + d = datetime.datetime(1899, 12, 31, 23) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:00:00+00:00') + d = datetime.datetime(1899, 12, 31, 23, 59) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:00+00:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset(hours=5)) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+05:00') + d = datetime.datetime(2011, 8, 8, 19, 3, 37) + self.assertEqual(DateTimeFormatter.iptc_time(d), '19:03:37+00:00') + + # time + d = datetime.time(23) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:00:00+00:00') + d = datetime.time(23, 59) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:00+00:00') + d = datetime.time(23, 59, 59) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.time(23, 59, 59, 999999) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.time(23, 59, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+00:00') + d = datetime.time(23, 59, 59, tzinfo=FixedOffset(hours=5)) + self.assertEqual(DateTimeFormatter.iptc_time(d), '23:59:59+05:00') + d = datetime.time(19, 3, 37) + self.assertEqual(DateTimeFormatter.iptc_time(d), '19:03:37+00:00') + + # invalid type + self.assertRaises(TypeError, DateTimeFormatter.iptc_time, None) + self.assertRaises(TypeError, DateTimeFormatter.iptc_time, 3.14) + + def test_xmp(self): + # datetime + d = datetime.datetime(1899, 12, 31) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31') + d = datetime.datetime(1899, 12, 31, 23, 59) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59Z') + d = datetime.datetime(1899, 12, 31, 23, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59Z') + d = datetime.datetime(1899, 12, 31, 23, 59, tzinfo=FixedOffset(hours=3)) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59+03:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59Z') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59Z') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, tzinfo=FixedOffset(hours=3)) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59+03:00') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59.999999Z') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999, tzinfo=FixedOffset()) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59.999999Z') + d = datetime.datetime(1899, 12, 31, 23, 59, 59, 999999, tzinfo=FixedOffset(hours=3)) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31T23:59:59.999999+03:00') + d = datetime.datetime(2011, 8, 11, 9, 23, 44) + self.assertEqual(DateTimeFormatter.xmp(d), '2011-08-11T09:23:44Z') + + # date + d = datetime.date(1899, 12, 31) + self.assertEqual(DateTimeFormatter.xmp(d), '1899-12-31') + d = datetime.date(2011, 8, 8) + self.assertEqual(DateTimeFormatter.xmp(d), '2011-08-08') + + # invalid type + self.assertRaises(TypeError, DateTimeFormatter.xmp, None) + self.assertRaises(TypeError, DateTimeFormatter.xmp, 3.14) + |