https://github.com/python/cpython/commit/6f6b8b4ac95ac1921acc912ac28c5ddca83b927e commit: 6f6b8b4ac95ac1921acc912ac28c5ddca83b927e branch: 3.12 author: Serhiy Storchaka <storch...@gmail.com> committer: serhiy-storchaka <storch...@gmail.com> date: 2025-04-02T20:35:16+03:00 summary:
[3.12] gh-89039: Call subclass constructors in datetime.*.replace (GH-114780) (GH-131239) When replace() method is called on a subclass of datetime, date or time, properly call derived constructor. Previously, only the base class's constructor was called. Also, make sure to pass non-zero fold values when creating subclasses in various methods. Previously, fold was silently ignored. (cherry picked from commit 46190d9ea8a878a03d95b4e1bdcdc9ed576cf3fa) Co-authored-by: Eugene Toder <elto...@users.noreply.github.com> files: A Misc/NEWS.d/next/Library/2023-12-18-20-10-50.gh-issue-89039.gqFdtU.rst M Lib/test/datetimetester.py M Modules/_datetimemodule.c diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 42a505be1d93f0..b2d5750f64f213 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1723,10 +1723,17 @@ def test_replace(self): def test_subclass_replace(self): class DateSubclass(self.theclass): - pass + def __new__(cls, *args, **kwargs): + result = self.theclass.__new__(cls, *args, **kwargs) + result.extra = 7 + return result dt = DateSubclass(2012, 1, 1) - self.assertIs(type(dt.replace(year=2013)), DateSubclass) + res = dt.replace(year=2013) + self.assertIs(type(res), DateSubclass) + self.assertEqual(res.year, 2013) + self.assertEqual(res.month, 1) + self.assertEqual(res.extra, 7) def test_subclass_date(self): @@ -3048,6 +3055,24 @@ def __new__(cls, *args, **kwargs): self.assertIsInstance(dt, DateTimeSubclass) self.assertEqual(dt.extra, 7) + def test_subclass_replace_fold(self): + class DateTimeSubclass(self.theclass): + pass + + dt = DateTimeSubclass(2012, 1, 1) + dt2 = DateTimeSubclass(2012, 1, 1, fold=1) + + test_cases = [ + ('self.replace', dt.replace(year=2013), 0), + ('self.replace', dt2.replace(year=2013), 1), + ] + + for name, res, fold in test_cases: + with self.subTest(name, fold=fold): + self.assertIs(type(res), DateTimeSubclass) + self.assertEqual(res.year, 2013) + self.assertEqual(res.fold, fold) + def test_fromisoformat_datetime(self): # Test that isoformat() is reversible base_dates = [ @@ -3754,10 +3779,26 @@ def test_replace(self): def test_subclass_replace(self): class TimeSubclass(self.theclass): - pass + def __new__(cls, *args, **kwargs): + result = self.theclass.__new__(cls, *args, **kwargs) + result.extra = 7 + return result ctime = TimeSubclass(12, 30) - self.assertIs(type(ctime.replace(hour=10)), TimeSubclass) + ctime2 = TimeSubclass(12, 30, fold=1) + + test_cases = [ + ('self.replace', ctime.replace(hour=10), 0), + ('self.replace', ctime2.replace(hour=10), 1), + ] + + for name, res, fold in test_cases: + with self.subTest(name, fold=fold): + self.assertIs(type(res), TimeSubclass) + self.assertEqual(res.hour, 10) + self.assertEqual(res.minute, 30) + self.assertEqual(res.extra, 7) + self.assertEqual(res.fold, fold) def test_subclass_time(self): diff --git a/Misc/NEWS.d/next/Library/2023-12-18-20-10-50.gh-issue-89039.gqFdtU.rst b/Misc/NEWS.d/next/Library/2023-12-18-20-10-50.gh-issue-89039.gqFdtU.rst new file mode 100644 index 00000000000000..d1998d75e9fd76 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-12-18-20-10-50.gh-issue-89039.gqFdtU.rst @@ -0,0 +1,6 @@ +When replace() method is called on a subclass of datetime, date or time, +properly call derived constructor. Previously, only the base class's +constructor was called. + +Also, make sure to pass non-zero fold values when creating subclasses in +various methods. Previously, fold was silently ignored. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 8535811a61b9e6..eb144cfdecfd5b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1027,6 +1027,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 * +call_subclass_fold(PyObject *cls, int fold, const char *format, ...) +{ + PyObject *kwargs = NULL, *res = NULL; + va_list va; + + va_start(va, format); + PyObject *args = Py_VaBuildValue(format, va); + va_end(va); + if (args == NULL) { + return NULL; + } + if (fold) { + kwargs = PyDict_New(); + if (kwargs == NULL) { + goto Done; + } + PyObject *obj = PyLong_FromLong(fold); + if (obj == NULL) { + goto Done; + } + int err = PyDict_SetItemString(kwargs, "fold", obj); + Py_DECREF(obj); + if (err < 0) { + goto Done; + } + } + res = PyObject_Call(cls, args, kwargs); +Done: + Py_DECREF(args); + Py_XDECREF(kwargs); + return res; +} + static PyObject * new_datetime_subclass_fold_ex(int year, int month, int day, int hour, int minute, int second, int usecond, PyObject *tzinfo, @@ -1036,17 +1070,11 @@ new_datetime_subclass_fold_ex(int year, int month, int day, int hour, int minute // Use the fast path constructor dt = new_datetime(year, month, day, hour, minute, second, usecond, tzinfo, fold); - } else { + } + else { // Subclass - dt = PyObject_CallFunction(cls, "iiiiiiiO", - year, - month, - day, - hour, - minute, - second, - usecond, - tzinfo); + dt = call_subclass_fold(cls, fold, "iiiiiiiO", year, month, day, + hour, minute, second, usecond, tzinfo); } return dt; @@ -1102,6 +1130,24 @@ new_time_ex(int hour, int minute, int second, int usecond, #define new_time(hh, mm, ss, us, tzinfo, fold) \ new_time_ex2(hh, mm, ss, us, tzinfo, fold, &PyDateTime_TimeType) +static PyObject * +new_time_subclass_fold_ex(int hour, int minute, int second, int usecond, + PyObject *tzinfo, int fold, PyObject *cls) +{ + PyObject *t; + if ((PyTypeObject*)cls == &PyDateTime_TimeType) { + // Use the fast path constructor + t = new_time(hour, minute, second, usecond, tzinfo, fold); + } + else { + // Subclass + t = call_subclass_fold(cls, fold, "iiiiO", hour, minute, second, + usecond, tzinfo); + } + + return t; +} + /* Create a timedelta instance. Normalize the members iff normalize is * true. Passing false is a speed optimization, if you know for sure * that seconds and microseconds are already in their proper ranges. In any @@ -3430,8 +3476,6 @@ date_timetuple(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) static PyObject * date_replace(PyDateTime_Date *self, PyObject *args, PyObject *kw) { - PyObject *clone; - PyObject *tuple; int year = GET_YEAR(self); int month = GET_MONTH(self); int day = GET_DAY(self); @@ -3439,12 +3483,7 @@ date_replace(PyDateTime_Date *self, PyObject *args, PyObject *kw) if (! PyArg_ParseTupleAndKeywords(args, kw, "|iii:replace", date_kws, &year, &month, &day)) return NULL; - tuple = Py_BuildValue("iii", year, month, day); - if (tuple == NULL) - return NULL; - clone = date_new(Py_TYPE(self), tuple, NULL); - Py_DECREF(tuple); - return clone; + return new_date_subclass_ex(year, month, day, (PyObject *)Py_TYPE(self)); } static Py_hash_t @@ -4533,8 +4572,6 @@ time_hash(PyDateTime_Time *self) static PyObject * time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw) { - PyObject *clone; - PyObject *tuple; int hh = TIME_GET_HOUR(self); int mm = TIME_GET_MINUTE(self); int ss = TIME_GET_SECOND(self); @@ -4551,15 +4588,8 @@ time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw) "fold must be either 0 or 1"); return NULL; } - tuple = Py_BuildValue("iiiiO", hh, mm, ss, us, tzinfo); - if (tuple == NULL) - return NULL; - clone = time_new(Py_TYPE(self), tuple, NULL); - if (clone != NULL) { - TIME_SET_FOLD(clone, fold); - } - Py_DECREF(tuple); - return clone; + return new_time_subclass_fold_ex(hh, mm, ss, us, tzinfo, fold, + (PyObject *)Py_TYPE(self)); } static PyObject * @@ -6003,8 +6033,6 @@ datetime_hash(PyDateTime_DateTime *self) static PyObject * datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) { - PyObject *clone; - PyObject *tuple; int y = GET_YEAR(self); int m = GET_MONTH(self); int d = GET_DAY(self); @@ -6025,15 +6053,8 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) "fold must be either 0 or 1"); return NULL; } - tuple = Py_BuildValue("iiiiiiiO", y, m, d, hh, mm, ss, us, tzinfo); - if (tuple == NULL) - return NULL; - clone = datetime_new(Py_TYPE(self), tuple, NULL); - if (clone != NULL) { - DATE_SET_FOLD(clone, fold); - } - Py_DECREF(tuple); - return clone; + return new_datetime_subclass_fold_ex(y, m, d, hh, mm, ss, us, tzinfo, fold, + (PyObject *)Py_TYPE(self)); } static PyObject * _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com