diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 9390da29..e8b78fec 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -3192,8 +3192,16 @@ Fields '%Y-%m-%d %H:%M:%S.%f' # year-month-day hour-minute-second.microsecond '%Y-%m-%d %H:%M:%S' # year-month-day hour-minute-second + '%Y-%m-%d %H:%M:%S.%f%z' # ...with timezone offset + '%Y-%m-%d %H:%M:%S%z' # ...with timezone offset '%Y-%m-%d' # year-month-day + In addition, any string accepted by + :py:meth:`datetime.datetime.fromisoformat` is parsed automatically, + including the ``T`` separator and a trailing ``Z`` (UTC). Custom + ``formats`` are still consulted as a fallback for non-ISO inputs (e.g. + ``'01/02/2003 01:37 PM'``). + SQLite does not have a native datetime data-type, so datetimes are stored as strings. This is handled transparently by Peewee, but if you have pre-existing data you should ensure it is stored as @@ -3270,6 +3278,9 @@ Fields '%Y-%m-%d %H:%M:%S' # year-month-day hour-minute-second '%Y-%m-%d %H:%M:%S.%f' # year-month-day hour-minute-second.microsecond + In addition, any string accepted by + :py:meth:`datetime.datetime.fromisoformat` is parsed automatically. + .. note:: If the incoming value does not match a format, it is returned as-is. diff --git a/peewee.py b/peewee.py index fa41f543..90e591d3 100644 --- a/peewee.py +++ b/peewee.py @@ -5472,8 +5472,23 @@ def _date_part(date_part): return self.model._meta.database.extract_date(date_part, self) return dec +# fromisoformat() is C-implemented and ~10x faster than strptime. Available +# since 3.7; pre-3.11 is strict about the separator and rejects 'Z'. +_fromisoformat = getattr(datetime.datetime, 'fromisoformat', None) + + def format_date_time(value, formats, post_process=None): post_process = post_process or (lambda x: x) + if _fromisoformat is not None and value: + s = value + if len(s) > 10 and s[10] == ' ': + s = s[:10] + 'T' + s[11:] + if s[-1:] == 'Z': + s = s[:-1] + '+00:00' + try: + return post_process(_fromisoformat(s)) + except (TypeError, ValueError): + pass for fmt in formats: try: return post_process(datetime.datetime.strptime(value, fmt)) diff --git a/tests/fields.py b/tests/fields.py index cf9edb77..996f9a3d 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -28,6 +28,7 @@ from decimal import ROUND_UP from peewee import NodeList from peewee import VirtualField +from peewee import format_date_time from peewee import * from playhouse.hybrid import * @@ -404,6 +405,38 @@ class TestDateFields(ModelTestCase): datetime.datetime(2002, 3, 1, 0, 0, 0), datetime.datetime(2002, 3, 4, 0, 0, 0)]) + def test_date_time_iso_fast_path(self): + dm = DateModel.create(date_time='2019-01-02 03:04:05.123456') + dm_db = DateModel[dm.id] + self.assertEqual(dm_db.date_time, + datetime.datetime(2019, 1, 2, 3, 4, 5, 123456)) + + dm = DateModel.create(date_time='2019-01-02T03:04:05') + dm_db = DateModel[dm.id] + self.assertEqual(dm_db.date_time, + datetime.datetime(2019, 1, 2, 3, 4, 5)) + + val = format_date_time('2019-01-02T03:04:05Z', + DateTimeField.formats) + self.assertEqual(val, + datetime.datetime(2019, 1, 2, 3, 4, 5, + tzinfo=datetime.timezone.utc)) + + val = format_date_time('2019-01-02', DateField.formats, + lambda x: x.date()) + self.assertEqual(val, datetime.date(2019, 1, 2)) + + def test_date_time_format_fallback(self): + val = format_date_time('01/02/2003 01:37 PM', + ['%m/%d/%Y %I:%M %p']) + self.assertEqual(val, datetime.datetime(2003, 1, 2, 13, 37)) + + val = format_date_time('11:12:13', TimeField.formats, + lambda x: x.time()) + self.assertEqual(val, datetime.time(11, 12, 13)) + + self.assertEqual(format_date_time('not a date', []), 'not a date') + def test_to_timestamp(self): dt = datetime.datetime(2019, 1, 2, 3, 4, 5) ts = calendar.timegm(dt.utctimetuple())