Skip to content

bpo-32403: Faster date and datetime constructors #4993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,50 @@ def newmeth(self, start):
self.assertEqual(dt1.toordinal(), dt2.toordinal())
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):

args = 6, 7, 23
Expand Down Expand Up @@ -2420,6 +2464,54 @@ def newmeth(self, start):
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month +
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):
# Test that isoformat() is reversible
base_dates = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improved speed of :class:`datetime.date` and :class:`datetime.datetime`
alternate constructors.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix bug where :meth:`datetime.datetime.fromtimestamp` did not call __new__
in :class:`datetime.datetime` subclasses.
120 changes: 75 additions & 45 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,27 @@ new_date_ex(int year, int month, int day, PyTypeObject *type)
#define new_date(year, month, day) \
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. */
static PyObject *
new_datetime_ex2(int year, int month, int day, int hour, int minute,
Expand Down Expand Up @@ -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, \
&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. */
static PyObject *
new_time_ex2(int hour, int minute, int second, int usecond,
Expand Down Expand Up @@ -2743,10 +2798,10 @@ date_local_from_object(PyObject *cls, PyObject *obj)
if (_PyTime_localtime(t, &tm) != 0)
return NULL;

return PyObject_CallFunction(cls, "iii",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday);
return new_date_subclass_ex(tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
cls);
}

/* Return new date from current time.
Expand Down Expand Up @@ -2809,8 +2864,7 @@ date_fromordinal(PyObject *cls, PyObject *args)
">= 1");
else {
ord_to_ymd(ordinal, &year, &month, &day);
result = PyObject_CallFunction(cls, "iii",
year, month, day);
result = new_date_subclass_ex(year, month, day, cls);
}
}
return result;
Expand Down Expand Up @@ -2845,14 +2899,7 @@ date_fromisoformat(PyObject *cls, PyObject *dtstr) {
return NULL;
}

PyObject *result;
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;
return new_date_subclass_ex(year, month, day, cls);
}


Expand Down Expand Up @@ -4596,9 +4643,8 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
fold = 1;
}
}
return new_datetime_ex2(year, month, day, hour,
minute, second, us, tzinfo, fold,
(PyTypeObject *)cls);
return new_datetime_subclass_fold_ex(year, month, day, hour, minute,
second, us, tzinfo, fold, cls);
}

/* Internal helper.
Expand Down Expand Up @@ -4764,17 +4810,16 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw)
else
tzinfo = Py_None;
}
result = PyObject_CallFunction(cls, "iiiiiiiO",
GET_YEAR(date),
GET_MONTH(date),
GET_DAY(date),
TIME_GET_HOUR(time),
TIME_GET_MINUTE(time),
TIME_GET_SECOND(time),
TIME_GET_MICROSECOND(time),
tzinfo);
if (result)
DATE_SET_FOLD(result, TIME_GET_FOLD(time));
result = new_datetime_subclass_fold_ex(GET_YEAR(date),
GET_MONTH(date),
GET_DAY(date),
TIME_GET_HOUR(time),
TIME_GET_MINUTE(time),
TIME_GET_SECOND(time),
TIME_GET_MICROSECOND(time),
tzinfo,
TIME_GET_FOLD(time),
cls);
}
return result;
}
Expand Down Expand Up @@ -4832,23 +4877,8 @@ datetime_fromisoformat(PyObject* cls, PyObject *dtstr) {
return NULL;
}

PyObject* dt;
if ( (PyTypeObject*)cls == &PyDateTime_DateTimeType ) {
// 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);
}
PyObject *dt = new_datetime_subclass_ex(year, month, day, hour, minute,
second, microsecond, tzinfo, cls);

Py_DECREF(tzinfo);
return dt;
Expand Down