From 5147013cda578044dfa8eff318a424bb1896bd85 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 21 Dec 2017 16:59:29 -0500 Subject: [PATCH 1/6] Add tests for date subclass alternate constructors --- Lib/test/datetimetester.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 1d0c1c5bd236f6..a7ccacb625c4f1 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1552,6 +1552,54 @@ 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) + ts = 1050292800.0 # Equivalent timestamp + 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) + + test_cases = [ + ('fromordinal', (d_ord,)), + ('fromtimestamp', (ts,)), + ('fromisoformat', (d_isoformat,)), + ] + + for constr_name, constr_args in test_cases: + if (issubclass(self.theclass, datetime) and + constr_name == 'fromtimestamp'): + # Known failure for datetime.fromtimestamp + # See bpo-32404 + continue + + 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 From 587986718781a4156195d413eac1c208a7d64403 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 21 Dec 2017 17:00:19 -0500 Subject: [PATCH 2/6] Switch over alternate date constructors to fast path --- Modules/_datetimemodule.c | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 624196702b60a2..cdaf3faafa4c75 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -844,6 +844,19 @@ new_date_ex(int year, int month, int day, PyTypeObject *type) return (PyObject *) self; } +/* 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; + 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; +} + #define new_date(year, month, day) \ new_date_ex(year, month, day, &PyDateTime_DateType) @@ -2743,10 +2756,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. @@ -2809,8 +2822,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; @@ -2845,14 +2857,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); } From 2e47897470eab878649f0675df0d4588f7589e65 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 23 Dec 2017 13:27:44 -0500 Subject: [PATCH 3/6] Switch datetime constructors to fastpath, fix bpo-32404 --- Lib/test/datetimetester.py | 58 ++++++++++++++++++++++++---- Modules/_datetimemodule.c | 79 +++++++++++++++++++++++--------------- 2 files changed, 100 insertions(+), 37 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a7ccacb625c4f1..e8ed79e8b320f8 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1562,7 +1562,6 @@ def __new__(cls, *args, **kwargs): return result args = (2003, 4, 14) - ts = 1050292800.0 # Equivalent timestamp d_ord = 731319 # Equivalent ordinal date d_isoformat = '2003-04-14' # Equivalent isoformat() @@ -1570,6 +1569,9 @@ def __new__(cls, *args, **kwargs): 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,)), @@ -1577,12 +1579,6 @@ def __new__(cls, *args, **kwargs): ] for constr_name, constr_args in test_cases: - if (issubclass(self.theclass, datetime) and - constr_name == 'fromtimestamp'): - # Known failure for datetime.fromtimestamp - # See bpo-32404 - continue - for base_obj in (DateSubclass, base_d): # Test both the classmethod and method with self.subTest(base_obj_type=type(base_obj), @@ -2468,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 = [ diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cdaf3faafa4c75..98f3117aa20edf 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -848,7 +848,7 @@ new_date_ex(int year, int month, int day, PyTypeObject *type) static PyObject * new_date_subclass_ex(int year, int month, int day, PyObject *cls) { PyObject *result; - if ( (PyTypeObject *)cls == & PyDateTime_DateType ) { + if ((PyTypeObject *)cls == &PyDateTime_DateType) { result = new_date_ex(year, month, day, (PyTypeObject *)cls); } else { result = PyObject_CallFunction(cls, "iii", year, month, day); @@ -907,6 +907,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, @@ -4601,9 +4635,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. @@ -4769,15 +4802,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); + result = new_datetime_subclass_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, + cls); + if (result) DATE_SET_FOLD(result, TIME_GET_FOLD(time)); } @@ -4837,23 +4871,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; From a64e9675abf35d07bb38f314b53ddce17aa2b91d Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 23 Dec 2017 14:07:14 -0500 Subject: [PATCH 4/6] Add fast path for datetime in date subclass constructor --- Modules/_datetimemodule.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 98f3117aa20edf..e6849bd3023f32 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -844,12 +844,23 @@ new_date_ex(int year, int month, int day, PyTypeObject *type) return (PyObject *) self; } +#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); } @@ -857,9 +868,6 @@ new_date_subclass_ex(int year, int month, int day, PyObject *cls) { return result; } -#define new_date(year, month, day) \ - new_date_ex(year, month, day, &PyDateTime_DateType) - /* Create a datetime instance with no range checking. */ static PyObject * new_datetime_ex2(int year, int month, int day, int hour, int minute, From 62b52f6215e18b4d0f3122f4be6af358c099dfac Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 23 Dec 2017 14:56:37 -0500 Subject: [PATCH 5/6] Set fold in constructor in datetime.combine --- Modules/_datetimemodule.c | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index e6849bd3023f32..fe29ba7067ad1a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4810,18 +4810,16 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw) else tzinfo = Py_None; } - result = new_datetime_subclass_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, - cls); - - 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; } From b438cbec71ae203c3ed889f7cd930174ed11090a Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 23 Dec 2017 14:56:55 -0500 Subject: [PATCH 6/6] Add news entries. --- .../next/Library/2017-12-23-14-51-46.bpo-32403.CVFapH.rst | 2 ++ .../next/Library/2017-12-23-14-54-05.bpo-32404.yJqtlJ.rst | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-12-23-14-51-46.bpo-32403.CVFapH.rst create mode 100644 Misc/NEWS.d/next/Library/2017-12-23-14-54-05.bpo-32404.yJqtlJ.rst diff --git a/Misc/NEWS.d/next/Library/2017-12-23-14-51-46.bpo-32403.CVFapH.rst b/Misc/NEWS.d/next/Library/2017-12-23-14-51-46.bpo-32403.CVFapH.rst new file mode 100644 index 00000000000000..f05d346948a5a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-12-23-14-51-46.bpo-32403.CVFapH.rst @@ -0,0 +1,2 @@ +Improved speed of :class:`datetime.date` and :class:`datetime.datetime` +alternate constructors. diff --git a/Misc/NEWS.d/next/Library/2017-12-23-14-54-05.bpo-32404.yJqtlJ.rst b/Misc/NEWS.d/next/Library/2017-12-23-14-54-05.bpo-32404.yJqtlJ.rst new file mode 100644 index 00000000000000..5299820429bb04 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-12-23-14-54-05.bpo-32404.yJqtlJ.rst @@ -0,0 +1,2 @@ +Fix bug where :meth:`datetime.datetime.fromtimestamp` did not call __new__ +in :class:`datetime.datetime` subclasses.