Skip to content

Commit 6f6b8b4

Browse files
[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 46190d9) Co-authored-by: Eugene Toder <eltoder@users.noreply.github.com>
1 parent e4fe4d9 commit 6f6b8b4

File tree

3 files changed

+112
-44
lines changed

3 files changed

+112
-44
lines changed

Lib/test/datetimetester.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,10 +1723,17 @@ def test_replace(self):
17231723

17241724
def test_subclass_replace(self):
17251725
class DateSubclass(self.theclass):
1726-
pass
1726+
def __new__(cls, *args, **kwargs):
1727+
result = self.theclass.__new__(cls, *args, **kwargs)
1728+
result.extra = 7
1729+
return result
17271730

17281731
dt = DateSubclass(2012, 1, 1)
1729-
self.assertIs(type(dt.replace(year=2013)), DateSubclass)
1732+
res = dt.replace(year=2013)
1733+
self.assertIs(type(res), DateSubclass)
1734+
self.assertEqual(res.year, 2013)
1735+
self.assertEqual(res.month, 1)
1736+
self.assertEqual(res.extra, 7)
17301737

17311738
def test_subclass_date(self):
17321739

@@ -3048,6 +3055,24 @@ def __new__(cls, *args, **kwargs):
30483055
self.assertIsInstance(dt, DateTimeSubclass)
30493056
self.assertEqual(dt.extra, 7)
30503057

3058+
def test_subclass_replace_fold(self):
3059+
class DateTimeSubclass(self.theclass):
3060+
pass
3061+
3062+
dt = DateTimeSubclass(2012, 1, 1)
3063+
dt2 = DateTimeSubclass(2012, 1, 1, fold=1)
3064+
3065+
test_cases = [
3066+
('self.replace', dt.replace(year=2013), 0),
3067+
('self.replace', dt2.replace(year=2013), 1),
3068+
]
3069+
3070+
for name, res, fold in test_cases:
3071+
with self.subTest(name, fold=fold):
3072+
self.assertIs(type(res), DateTimeSubclass)
3073+
self.assertEqual(res.year, 2013)
3074+
self.assertEqual(res.fold, fold)
3075+
30513076
def test_fromisoformat_datetime(self):
30523077
# Test that isoformat() is reversible
30533078
base_dates = [
@@ -3754,10 +3779,26 @@ def test_replace(self):
37543779

37553780
def test_subclass_replace(self):
37563781
class TimeSubclass(self.theclass):
3757-
pass
3782+
def __new__(cls, *args, **kwargs):
3783+
result = self.theclass.__new__(cls, *args, **kwargs)
3784+
result.extra = 7
3785+
return result
37583786

37593787
ctime = TimeSubclass(12, 30)
3760-
self.assertIs(type(ctime.replace(hour=10)), TimeSubclass)
3788+
ctime2 = TimeSubclass(12, 30, fold=1)
3789+
3790+
test_cases = [
3791+
('self.replace', ctime.replace(hour=10), 0),
3792+
('self.replace', ctime2.replace(hour=10), 1),
3793+
]
3794+
3795+
for name, res, fold in test_cases:
3796+
with self.subTest(name, fold=fold):
3797+
self.assertIs(type(res), TimeSubclass)
3798+
self.assertEqual(res.hour, 10)
3799+
self.assertEqual(res.minute, 30)
3800+
self.assertEqual(res.extra, 7)
3801+
self.assertEqual(res.fold, fold)
37613802

37623803
def test_subclass_time(self):
37633804

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
When replace() method is called on a subclass of datetime, date or time,
2+
properly call derived constructor. Previously, only the base class's
3+
constructor was called.
4+
5+
Also, make sure to pass non-zero fold values when creating subclasses in
6+
various methods. Previously, fold was silently ignored.

Modules/_datetimemodule.c

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,40 @@ new_datetime_ex(int year, int month, int day, int hour, int minute,
10271027
new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \
10281028
&PyDateTime_DateTimeType)
10291029

1030+
static PyObject *
1031+
call_subclass_fold(PyObject *cls, int fold, const char *format, ...)
1032+
{
1033+
PyObject *kwargs = NULL, *res = NULL;
1034+
va_list va;
1035+
1036+
va_start(va, format);
1037+
PyObject *args = Py_VaBuildValue(format, va);
1038+
va_end(va);
1039+
if (args == NULL) {
1040+
return NULL;
1041+
}
1042+
if (fold) {
1043+
kwargs = PyDict_New();
1044+
if (kwargs == NULL) {
1045+
goto Done;
1046+
}
1047+
PyObject *obj = PyLong_FromLong(fold);
1048+
if (obj == NULL) {
1049+
goto Done;
1050+
}
1051+
int err = PyDict_SetItemString(kwargs, "fold", obj);
1052+
Py_DECREF(obj);
1053+
if (err < 0) {
1054+
goto Done;
1055+
}
1056+
}
1057+
res = PyObject_Call(cls, args, kwargs);
1058+
Done:
1059+
Py_DECREF(args);
1060+
Py_XDECREF(kwargs);
1061+
return res;
1062+
}
1063+
10301064
static PyObject *
10311065
new_datetime_subclass_fold_ex(int year, int month, int day, int hour, int minute,
10321066
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
10361070
// Use the fast path constructor
10371071
dt = new_datetime(year, month, day, hour, minute, second, usecond,
10381072
tzinfo, fold);
1039-
} else {
1073+
}
1074+
else {
10401075
// Subclass
1041-
dt = PyObject_CallFunction(cls, "iiiiiiiO",
1042-
year,
1043-
month,
1044-
day,
1045-
hour,
1046-
minute,
1047-
second,
1048-
usecond,
1049-
tzinfo);
1076+
dt = call_subclass_fold(cls, fold, "iiiiiiiO", year, month, day,
1077+
hour, minute, second, usecond, tzinfo);
10501078
}
10511079

10521080
return dt;
@@ -1102,6 +1130,24 @@ new_time_ex(int hour, int minute, int second, int usecond,
11021130
#define new_time(hh, mm, ss, us, tzinfo, fold) \
11031131
new_time_ex2(hh, mm, ss, us, tzinfo, fold, &PyDateTime_TimeType)
11041132

1133+
static PyObject *
1134+
new_time_subclass_fold_ex(int hour, int minute, int second, int usecond,
1135+
PyObject *tzinfo, int fold, PyObject *cls)
1136+
{
1137+
PyObject *t;
1138+
if ((PyTypeObject*)cls == &PyDateTime_TimeType) {
1139+
// Use the fast path constructor
1140+
t = new_time(hour, minute, second, usecond, tzinfo, fold);
1141+
}
1142+
else {
1143+
// Subclass
1144+
t = call_subclass_fold(cls, fold, "iiiiO", hour, minute, second,
1145+
usecond, tzinfo);
1146+
}
1147+
1148+
return t;
1149+
}
1150+
11051151
/* Create a timedelta instance. Normalize the members iff normalize is
11061152
* true. Passing false is a speed optimization, if you know for sure
11071153
* that seconds and microseconds are already in their proper ranges. In any
@@ -3430,21 +3476,14 @@ date_timetuple(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored))
34303476
static PyObject *
34313477
date_replace(PyDateTime_Date *self, PyObject *args, PyObject *kw)
34323478
{
3433-
PyObject *clone;
3434-
PyObject *tuple;
34353479
int year = GET_YEAR(self);
34363480
int month = GET_MONTH(self);
34373481
int day = GET_DAY(self);
34383482

34393483
if (! PyArg_ParseTupleAndKeywords(args, kw, "|iii:replace", date_kws,
34403484
&year, &month, &day))
34413485
return NULL;
3442-
tuple = Py_BuildValue("iii", year, month, day);
3443-
if (tuple == NULL)
3444-
return NULL;
3445-
clone = date_new(Py_TYPE(self), tuple, NULL);
3446-
Py_DECREF(tuple);
3447-
return clone;
3486+
return new_date_subclass_ex(year, month, day, (PyObject *)Py_TYPE(self));
34483487
}
34493488

34503489
static Py_hash_t
@@ -4533,8 +4572,6 @@ time_hash(PyDateTime_Time *self)
45334572
static PyObject *
45344573
time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw)
45354574
{
4536-
PyObject *clone;
4537-
PyObject *tuple;
45384575
int hh = TIME_GET_HOUR(self);
45394576
int mm = TIME_GET_MINUTE(self);
45404577
int ss = TIME_GET_SECOND(self);
@@ -4551,15 +4588,8 @@ time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw)
45514588
"fold must be either 0 or 1");
45524589
return NULL;
45534590
}
4554-
tuple = Py_BuildValue("iiiiO", hh, mm, ss, us, tzinfo);
4555-
if (tuple == NULL)
4556-
return NULL;
4557-
clone = time_new(Py_TYPE(self), tuple, NULL);
4558-
if (clone != NULL) {
4559-
TIME_SET_FOLD(clone, fold);
4560-
}
4561-
Py_DECREF(tuple);
4562-
return clone;
4591+
return new_time_subclass_fold_ex(hh, mm, ss, us, tzinfo, fold,
4592+
(PyObject *)Py_TYPE(self));
45634593
}
45644594

45654595
static PyObject *
@@ -6003,8 +6033,6 @@ datetime_hash(PyDateTime_DateTime *self)
60036033
static PyObject *
60046034
datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
60056035
{
6006-
PyObject *clone;
6007-
PyObject *tuple;
60086036
int y = GET_YEAR(self);
60096037
int m = GET_MONTH(self);
60106038
int d = GET_DAY(self);
@@ -6025,15 +6053,8 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
60256053
"fold must be either 0 or 1");
60266054
return NULL;
60276055
}
6028-
tuple = Py_BuildValue("iiiiiiiO", y, m, d, hh, mm, ss, us, tzinfo);
6029-
if (tuple == NULL)
6030-
return NULL;
6031-
clone = datetime_new(Py_TYPE(self), tuple, NULL);
6032-
if (clone != NULL) {
6033-
DATE_SET_FOLD(clone, fold);
6034-
}
6035-
Py_DECREF(tuple);
6036-
return clone;
6056+
return new_datetime_subclass_fold_ex(y, m, d, hh, mm, ss, us, tzinfo, fold,
6057+
(PyObject *)Py_TYPE(self));
60376058
}
60386059

60396060
static PyObject *

0 commit comments

Comments
 (0)