Use faster and more robust fromisoformat when possible.

This commit is contained in:
Charles Leifer
2026-05-05 17:15:04 -05:00
parent cefdec2116
commit 4fedb8f4d0
3 changed files with 59 additions and 0 deletions
+11
View File
@@ -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.%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' # 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 '%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 SQLite does not have a native datetime data-type, so datetimes are
stored as strings. This is handled transparently by Peewee, but if you stored as strings. This is handled transparently by Peewee, but if you
have pre-existing data you should ensure it is stored as 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' # year-month-day hour-minute-second
'%Y-%m-%d %H:%M:%S.%f' # year-month-day hour-minute-second.microsecond '%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:: .. note::
If the incoming value does not match a format, it is returned as-is. If the incoming value does not match a format, it is returned as-is.
+15
View File
@@ -5472,8 +5472,23 @@ def _date_part(date_part):
return self.model._meta.database.extract_date(date_part, self) return self.model._meta.database.extract_date(date_part, self)
return dec 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): def format_date_time(value, formats, post_process=None):
post_process = post_process or (lambda x: x) 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: for fmt in formats:
try: try:
return post_process(datetime.datetime.strptime(value, fmt)) return post_process(datetime.datetime.strptime(value, fmt))
+33
View File
@@ -28,6 +28,7 @@ from decimal import ROUND_UP
from peewee import NodeList from peewee import NodeList
from peewee import VirtualField from peewee import VirtualField
from peewee import format_date_time
from peewee import * from peewee import *
from playhouse.hybrid import * from playhouse.hybrid import *
@@ -404,6 +405,38 @@ class TestDateFields(ModelTestCase):
datetime.datetime(2002, 3, 1, 0, 0, 0), datetime.datetime(2002, 3, 1, 0, 0, 0),
datetime.datetime(2002, 3, 4, 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): def test_to_timestamp(self):
dt = datetime.datetime(2019, 1, 2, 3, 4, 5) dt = datetime.datetime(2019, 1, 2, 3, 4, 5)
ts = calendar.timegm(dt.utctimetuple()) ts = calendar.timegm(dt.utctimetuple())