Skip to content

Commit 9f1b7b9

Browse files
pganssleabalkin
authored andcommitted
bpo-32403: Faster date and datetime constructors (#4993)
* Add tests for date subclass alternate constructors * Switch over alternate date constructors to fast path * Switch datetime constructors to fastpath, fix bpo-32404 * Add fast path for datetime in date subclass constructor * Set fold in constructor in datetime.combine * Add news entries.
1 parent 6b5a279 commit 9f1b7b9

File tree

4 files changed

+171
-45
lines changed

4 files changed

+171
-45
lines changed

Lib/test/datetimetester.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,50 @@ def newmeth(self, start):
15521552
self.assertEqual(dt1.toordinal(), dt2.toordinal())
15531553
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month - 7)
15541554

1555+
def test_subclass_alternate_constructors(self):
1556+
# Test that alternate constructors call the constructor
1557+
class DateSubclass(self.theclass):
1558+
def __new__(cls, *args, **kwargs):
1559+
result = self.theclass.__new__(cls, *args, **kwargs)
1560+
result.extra = 7
1561+
1562+
return result
1563+
1564+
args = (2003, 4, 14)
1565+
d_ord = 731319 # Equivalent ordinal date
1566+
d_isoformat = '2003-04-14' # Equivalent isoformat()
1567+
1568+
base_d = DateSubclass(*args)
1569+
self.assertIsInstance(base_d, DateSubclass)
1570+
self.assertEqual(base_d.extra, 7)
1571+
1572+
# Timestamp depends on time zone, so we'll calculate the equivalent here
1573+
ts = datetime.combine(base_d, time(0)).timestamp()
1574+
1575+
test_cases = [
1576+
('fromordinal', (d_ord,)),
1577+
('fromtimestamp', (ts,)),
1578+
('fromisoformat', (d_isoformat,)),
1579+
]
1580+
1581+
for constr_name, constr_args in test_cases:
1582+
for base_obj in (DateSubclass, base_d):
1583+
# Test both the classmethod and method
1584+
with self.subTest(base_obj_type=type(base_obj),
1585+
constr_name=constr_name):
1586+
constr = getattr(base_obj, constr_name)
1587+
1588+
dt = constr(*constr_args)
1589+
1590+
# Test that it creates the right subclass
1591+
self.assertIsInstance(dt, DateSubclass)
1592+
1593+
# Test that it's equal to the base object
1594+
self.assertEqual(dt, base_d)
1595+
1596+
# Test that it called the constructor
1597+
self.assertEqual(dt.extra, 7)
1598+
15551599
def test_pickling_subclass_date(self):
15561600

15571601
args = 6, 7, 23
@@ -2420,6 +2464,54 @@ def newmeth(self, start):
24202464
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month +
24212465
dt1.second - 7)
24222466

2467+
def test_subclass_alternate_constructors_datetime(self):
2468+
# Test that alternate constructors call the constructor
2469+
class DateTimeSubclass(self.theclass):
2470+
def __new__(cls, *args, **kwargs):
2471+
result = self.theclass.__new__(cls, *args, **kwargs)
2472+
result.extra = 7
2473+
2474+
return result
2475+
2476+
args = (2003, 4, 14, 12, 30, 15, 123456)
2477+
d_isoformat = '2003-04-14T12:30:15.123456' # Equivalent isoformat()
2478+
utc_ts = 1050323415.123456 # UTC timestamp
2479+
2480+
base_d = DateTimeSubclass(*args)
2481+
self.assertIsInstance(base_d, DateTimeSubclass)
2482+
self.assertEqual(base_d.extra, 7)
2483+
2484+
# Timestamp depends on time zone, so we'll calculate the equivalent here
2485+
ts = base_d.timestamp()
2486+
2487+
test_cases = [
2488+
('fromtimestamp', (ts,)),
2489+
# See https://bugs.python.org/issue32417
2490+
# ('fromtimestamp', (ts, timezone.utc)),
2491+
('utcfromtimestamp', (utc_ts,)),
2492+
('fromisoformat', (d_isoformat,)),
2493+
('strptime', (d_isoformat, '%Y-%m-%dT%H:%M:%S.%f')),
2494+
('combine', (date(*args[0:3]), time(*args[3:]))),
2495+
]
2496+
2497+
for constr_name, constr_args in test_cases:
2498+
for base_obj in (DateTimeSubclass, base_d):
2499+
# Test both the classmethod and method
2500+
with self.subTest(base_obj_type=type(base_obj),
2501+
constr_name=constr_name):
2502+
constr = getattr(base_obj, constr_name)
2503+
2504+
dt = constr(*constr_args)
2505+
2506+
# Test that it creates the right subclass
2507+
self.assertIsInstance(dt, DateTimeSubclass)
2508+
2509+
# Test that it's equal to the base object
2510+
self.assertEqual(dt, base_d.replace(tzinfo=None))
2511+
2512+
# Test that it called the constructor
2513+
self.assertEqual(dt.extra, 7)
2514+
24232515
def test_fromisoformat_datetime(self):
24242516
# Test that isoformat() is reversible
24252517
base_dates = [
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improved speed of :class:`datetime.date` and :class:`datetime.datetime`
2+
alternate constructors.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix bug where :meth:`datetime.datetime.fromtimestamp` did not call __new__
2+
in :class:`datetime.datetime` subclasses.

Modules/_datetimemodule.c

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,27 @@ new_date_ex(int year, int month, int day, PyTypeObject *type)
847847
#define new_date(year, month, day) \
848848
new_date_ex(year, month, day, &PyDateTime_DateType)
849849

850+
// Forward declaration
851+
static PyObject * new_datetime_ex(int, int, int, int, int, int, int,
852+
PyObject*, PyTypeObject*);
853+
854+
/* Create date instance with no range checking, or call subclass constructor */
855+
static PyObject *
856+
new_date_subclass_ex(int year, int month, int day, PyObject *cls) {
857+
PyObject *result;
858+
// We have "fast path" constructors for two subclasses: date and datetime
859+
if ((PyTypeObject *)cls == &PyDateTime_DateType) {
860+
result = new_date_ex(year, month, day, (PyTypeObject *)cls);
861+
} else if ((PyTypeObject *)cls == &PyDateTime_DateTimeType) {
862+
result = new_datetime_ex(year, month, day, 0, 0, 0, 0, Py_None,
863+
(PyTypeObject *)cls);
864+
} else {
865+
result = PyObject_CallFunction(cls, "iii", year, month, day);
866+
}
867+
868+
return result;
869+
}
870+
850871
/* Create a datetime instance with no range checking. */
851872
static PyObject *
852873
new_datetime_ex2(int year, int month, int day, int hour, int minute,
@@ -894,6 +915,40 @@ new_datetime_ex(int year, int month, int day, int hour, int minute,
894915
new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \
895916
&PyDateTime_DateTimeType)
896917

918+
static PyObject *
919+
new_datetime_subclass_fold_ex(int year, int month, int day, int hour, int minute,
920+
int second, int usecond, PyObject *tzinfo,
921+
int fold, PyObject *cls) {
922+
PyObject* dt;
923+
if ((PyTypeObject*)cls == &PyDateTime_DateTimeType) {
924+
// Use the fast path constructor
925+
dt = new_datetime(year, month, day, hour, minute, second, usecond,
926+
tzinfo, fold);
927+
} else {
928+
// Subclass
929+
dt = PyObject_CallFunction(cls, "iiiiiiiO",
930+
year,
931+
month,
932+
day,
933+
hour,
934+
minute,
935+
second,
936+
usecond,
937+
tzinfo);
938+
}
939+
940+
return dt;
941+
}
942+
943+
static PyObject *
944+
new_datetime_subclass_ex(int year, int month, int day, int hour, int minute,
945+
int second, int usecond, PyObject *tzinfo,
946+
PyObject *cls) {
947+
return new_datetime_subclass_fold_ex(year, month, day, hour, minute,
948+
second, usecond, tzinfo, 0,
949+
cls);
950+
}
951+
897952
/* Create a time instance with no range checking. */
898953
static PyObject *
899954
new_time_ex2(int hour, int minute, int second, int usecond,
@@ -2743,10 +2798,10 @@ date_local_from_object(PyObject *cls, PyObject *obj)
27432798
if (_PyTime_localtime(t, &tm) != 0)
27442799
return NULL;
27452800

2746-
return PyObject_CallFunction(cls, "iii",
2747-
tm.tm_year + 1900,
2748-
tm.tm_mon + 1,
2749-
tm.tm_mday);
2801+
return new_date_subclass_ex(tm.tm_year + 1900,
2802+
tm.tm_mon + 1,
2803+
tm.tm_mday,
2804+
cls);
27502805
}
27512806

27522807
/* Return new date from current time.
@@ -2809,8 +2864,7 @@ date_fromordinal(PyObject *cls, PyObject *args)
28092864
">= 1");
28102865
else {
28112866
ord_to_ymd(ordinal, &year, &month, &day);
2812-
result = PyObject_CallFunction(cls, "iii",
2813-
year, month, day);
2867+
result = new_date_subclass_ex(year, month, day, cls);
28142868
}
28152869
}
28162870
return result;
@@ -2845,14 +2899,7 @@ date_fromisoformat(PyObject *cls, PyObject *dtstr) {
28452899
return NULL;
28462900
}
28472901

2848-
PyObject *result;
2849-
if ( (PyTypeObject*)cls == &PyDateTime_DateType ) {
2850-
result = new_date_ex(year, month, day, (PyTypeObject*)cls);
2851-
} else {
2852-
result = PyObject_CallFunction(cls, "iii", year, month, day);
2853-
}
2854-
2855-
return result;
2902+
return new_date_subclass_ex(year, month, day, cls);
28562903
}
28572904

28582905

@@ -4596,9 +4643,8 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
45964643
fold = 1;
45974644
}
45984645
}
4599-
return new_datetime_ex2(year, month, day, hour,
4600-
minute, second, us, tzinfo, fold,
4601-
(PyTypeObject *)cls);
4646+
return new_datetime_subclass_fold_ex(year, month, day, hour, minute,
4647+
second, us, tzinfo, fold, cls);
46024648
}
46034649

46044650
/* Internal helper.
@@ -4764,17 +4810,16 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw)
47644810
else
47654811
tzinfo = Py_None;
47664812
}
4767-
result = PyObject_CallFunction(cls, "iiiiiiiO",
4768-
GET_YEAR(date),
4769-
GET_MONTH(date),
4770-
GET_DAY(date),
4771-
TIME_GET_HOUR(time),
4772-
TIME_GET_MINUTE(time),
4773-
TIME_GET_SECOND(time),
4774-
TIME_GET_MICROSECOND(time),
4775-
tzinfo);
4776-
if (result)
4777-
DATE_SET_FOLD(result, TIME_GET_FOLD(time));
4813+
result = new_datetime_subclass_fold_ex(GET_YEAR(date),
4814+
GET_MONTH(date),
4815+
GET_DAY(date),
4816+
TIME_GET_HOUR(time),
4817+
TIME_GET_MINUTE(time),
4818+
TIME_GET_SECOND(time),
4819+
TIME_GET_MICROSECOND(time),
4820+
tzinfo,
4821+
TIME_GET_FOLD(time),
4822+
cls);
47784823
}
47794824
return result;
47804825
}
@@ -4832,23 +4877,8 @@ datetime_fromisoformat(PyObject* cls, PyObject *dtstr) {
48324877
return NULL;
48334878
}
48344879

4835-
PyObject* dt;
4836-
if ( (PyTypeObject*)cls == &PyDateTime_DateTimeType ) {
4837-
// Use the fast path constructor
4838-
dt = new_datetime(year, month, day, hour, minute, second, microsecond,
4839-
tzinfo, 0);
4840-
} else {
4841-
// Subclass
4842-
dt = PyObject_CallFunction(cls, "iiiiiiiO",
4843-
year,
4844-
month,
4845-
day,
4846-
hour,
4847-
minute,
4848-
second,
4849-
microsecond,
4850-
tzinfo);
4851-
}
4880+
PyObject *dt = new_datetime_subclass_ex(year, month, day, hour, minute,
4881+
second, microsecond, tzinfo, cls);
48524882

48534883
Py_DECREF(tzinfo);
48544884
return dt;

0 commit comments

Comments
 (0)