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

Reply via email to