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.
This commit is contained in:
Paul Ganssle 2018-01-16 13:06:31 -05:00 committed by Alexander Belopolsky
parent 6b5a27975a
commit 9f1b7b93f5
4 changed files with 171 additions and 45 deletions

View File

@ -1552,6 +1552,50 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
self.assertEqual(dt1.toordinal(), dt2.toordinal()) self.assertEqual(dt1.toordinal(), dt2.toordinal())
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month - 7) self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month - 7)
def test_subclass_alternate_constructors(self):
# Test that alternate constructors call the constructor
class DateSubclass(self.theclass):
def __new__(cls, *args, **kwargs):
result = self.theclass.__new__(cls, *args, **kwargs)
result.extra = 7
return result
args = (2003, 4, 14)
d_ord = 731319 # Equivalent ordinal date
d_isoformat = '2003-04-14' # Equivalent isoformat()
base_d = DateSubclass(*args)
self.assertIsInstance(base_d, DateSubclass)
self.assertEqual(base_d.extra, 7)
# Timestamp depends on time zone, so we'll calculate the equivalent here
ts = datetime.combine(base_d, time(0)).timestamp()
test_cases = [
('fromordinal', (d_ord,)),
('fromtimestamp', (ts,)),
('fromisoformat', (d_isoformat,)),
]
for constr_name, constr_args in test_cases:
for base_obj in (DateSubclass, base_d):
# Test both the classmethod and method
with self.subTest(base_obj_type=type(base_obj),
constr_name=constr_name):
constr = getattr(base_obj, constr_name)
dt = constr(*constr_args)
# Test that it creates the right subclass
self.assertIsInstance(dt, DateSubclass)
# Test that it's equal to the base object
self.assertEqual(dt, base_d)
# Test that it called the constructor
self.assertEqual(dt.extra, 7)
def test_pickling_subclass_date(self): def test_pickling_subclass_date(self):
args = 6, 7, 23 args = 6, 7, 23
@ -2420,6 +2464,54 @@ class TestDateTime(TestDate):
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month + self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month +
dt1.second - 7) dt1.second - 7)
def test_subclass_alternate_constructors_datetime(self):
# Test that alternate constructors call the constructor
class DateTimeSubclass(self.theclass):
def __new__(cls, *args, **kwargs):
result = self.theclass.__new__(cls, *args, **kwargs)
result.extra = 7
return result
args = (2003, 4, 14, 12, 30, 15, 123456)
d_isoformat = '2003-04-14T12:30:15.123456' # Equivalent isoformat()
utc_ts = 1050323415.123456 # UTC timestamp
base_d = DateTimeSubclass(*args)
self.assertIsInstance(base_d, DateTimeSubclass)
self.assertEqual(base_d.extra, 7)
# Timestamp depends on time zone, so we'll calculate the equivalent here
ts = base_d.timestamp()
test_cases = [
('fromtimestamp', (ts,)),
# See https://bugs.python.org/issue32417
# ('fromtimestamp', (ts, timezone.utc)),
('utcfromtimestamp', (utc_ts,)),
('fromisoformat', (d_isoformat,)),
('strptime', (d_isoformat, '%Y-%m-%dT%H:%M:%S.%f')),
('combine', (date(*args[0:3]), time(*args[3:]))),
]
for constr_name, constr_args in test_cases:
for base_obj in (DateTimeSubclass, base_d):
# Test both the classmethod and method
with self.subTest(base_obj_type=type(base_obj),
constr_name=constr_name):
constr = getattr(base_obj, constr_name)
dt = constr(*constr_args)
# Test that it creates the right subclass
self.assertIsInstance(dt, DateTimeSubclass)
# Test that it's equal to the base object
self.assertEqual(dt, base_d.replace(tzinfo=None))
# Test that it called the constructor
self.assertEqual(dt.extra, 7)
def test_fromisoformat_datetime(self): def test_fromisoformat_datetime(self):
# Test that isoformat() is reversible # Test that isoformat() is reversible
base_dates = [ base_dates = [

View File

@ -0,0 +1,2 @@
Improved speed of :class:`datetime.date` and :class:`datetime.datetime`
alternate constructors.

View File

@ -0,0 +1,2 @@
Fix bug where :meth:`datetime.datetime.fromtimestamp` did not call __new__
in :class:`datetime.datetime` subclasses.

View File

@ -847,6 +847,27 @@ new_date_ex(int year, int month, int day, PyTypeObject *type)
#define new_date(year, month, day) \ #define new_date(year, month, day) \
new_date_ex(year, month, day, &PyDateTime_DateType) new_date_ex(year, month, day, &PyDateTime_DateType)
// Forward declaration
static PyObject * new_datetime_ex(int, int, int, int, int, int, int,
PyObject*, PyTypeObject*);
/* Create date instance with no range checking, or call subclass constructor */
static PyObject *
new_date_subclass_ex(int year, int month, int day, PyObject *cls) {
PyObject *result;
// We have "fast path" constructors for two subclasses: date and datetime
if ((PyTypeObject *)cls == &PyDateTime_DateType) {
result = new_date_ex(year, month, day, (PyTypeObject *)cls);
} else if ((PyTypeObject *)cls == &PyDateTime_DateTimeType) {
result = new_datetime_ex(year, month, day, 0, 0, 0, 0, Py_None,
(PyTypeObject *)cls);
} else {
result = PyObject_CallFunction(cls, "iii", year, month, day);
}
return result;
}
/* Create a datetime instance with no range checking. */ /* Create a datetime instance with no range checking. */
static PyObject * static PyObject *
new_datetime_ex2(int year, int month, int day, int hour, int minute, 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,
new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \ new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \
&PyDateTime_DateTimeType) &PyDateTime_DateTimeType)
static PyObject *
new_datetime_subclass_fold_ex(int year, int month, int day, int hour, int minute,
int second, int usecond, PyObject *tzinfo,
int fold, PyObject *cls) {
PyObject* dt;
if ((PyTypeObject*)cls == &PyDateTime_DateTimeType) {
// Use the fast path constructor
dt = new_datetime(year, month, day, hour, minute, second, usecond,
tzinfo, fold);
} else {
// Subclass
dt = PyObject_CallFunction(cls, "iiiiiiiO",
year,
month,
day,
hour,
minute,
second,
usecond,
tzinfo);
}
return dt;
}
static PyObject *
new_datetime_subclass_ex(int year, int month, int day, int hour, int minute,
int second, int usecond, PyObject *tzinfo,
PyObject *cls) {
return new_datetime_subclass_fold_ex(year, month, day, hour, minute,
second, usecond, tzinfo, 0,
cls);
}
/* Create a time instance with no range checking. */ /* Create a time instance with no range checking. */
static PyObject * static PyObject *
new_time_ex2(int hour, int minute, int second, int usecond, new_time_ex2(int hour, int minute, int second, int usecond,
@ -2743,10 +2798,10 @@ date_local_from_object(PyObject *cls, PyObject *obj)
if (_PyTime_localtime(t, &tm) != 0) if (_PyTime_localtime(t, &tm) != 0)
return NULL; return NULL;
return PyObject_CallFunction(cls, "iii", return new_date_subclass_ex(tm.tm_year + 1900,
tm.tm_year + 1900, tm.tm_mon + 1,
tm.tm_mon + 1, tm.tm_mday,
tm.tm_mday); cls);
} }
/* Return new date from current time. /* Return new date from current time.
@ -2809,8 +2864,7 @@ date_fromordinal(PyObject *cls, PyObject *args)
">= 1"); ">= 1");
else { else {
ord_to_ymd(ordinal, &year, &month, &day); ord_to_ymd(ordinal, &year, &month, &day);
result = PyObject_CallFunction(cls, "iii", result = new_date_subclass_ex(year, month, day, cls);
year, month, day);
} }
} }
return result; return result;
@ -2845,14 +2899,7 @@ date_fromisoformat(PyObject *cls, PyObject *dtstr) {
return NULL; return NULL;
} }
PyObject *result; return new_date_subclass_ex(year, month, day, cls);
if ( (PyTypeObject*)cls == &PyDateTime_DateType ) {
result = new_date_ex(year, month, day, (PyTypeObject*)cls);
} else {
result = PyObject_CallFunction(cls, "iii", year, month, day);
}
return result;
} }
@ -4596,9 +4643,8 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
fold = 1; fold = 1;
} }
} }
return new_datetime_ex2(year, month, day, hour, return new_datetime_subclass_fold_ex(year, month, day, hour, minute,
minute, second, us, tzinfo, fold, second, us, tzinfo, fold, cls);
(PyTypeObject *)cls);
} }
/* Internal helper. /* Internal helper.
@ -4764,17 +4810,16 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw)
else else
tzinfo = Py_None; tzinfo = Py_None;
} }
result = PyObject_CallFunction(cls, "iiiiiiiO", result = new_datetime_subclass_fold_ex(GET_YEAR(date),
GET_YEAR(date), GET_MONTH(date),
GET_MONTH(date), GET_DAY(date),
GET_DAY(date), TIME_GET_HOUR(time),
TIME_GET_HOUR(time), TIME_GET_MINUTE(time),
TIME_GET_MINUTE(time), TIME_GET_SECOND(time),
TIME_GET_SECOND(time), TIME_GET_MICROSECOND(time),
TIME_GET_MICROSECOND(time), tzinfo,
tzinfo); TIME_GET_FOLD(time),
if (result) cls);
DATE_SET_FOLD(result, TIME_GET_FOLD(time));
} }
return result; return result;
} }
@ -4832,23 +4877,8 @@ datetime_fromisoformat(PyObject* cls, PyObject *dtstr) {
return NULL; return NULL;
} }
PyObject* dt; PyObject *dt = new_datetime_subclass_ex(year, month, day, hour, minute,
if ( (PyTypeObject*)cls == &PyDateTime_DateTimeType ) { second, microsecond, tzinfo, cls);
// Use the fast path constructor
dt = new_datetime(year, month, day, hour, minute, second, microsecond,
tzinfo, 0);
} else {
// Subclass
dt = PyObject_CallFunction(cls, "iiiiiiiO",
year,
month,
day,
hour,
minute,
second,
microsecond,
tzinfo);
}
Py_DECREF(tzinfo); Py_DECREF(tzinfo);
return dt; return dt;