From e1c5b001577e938e7419d444bdf089c4c4187790 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Sun, 1 Sep 2019 14:41:29 +0900 Subject: [PATCH 01/20] bpo-24416: Return a namedtuple from date.isocalendar() --- Doc/library/datetime.rst | 10 ++-- Doc/whatsnew/3.9.rst | 8 ++++ Lib/datetime.py | 10 +++- Lib/test/datetimetester.py | 10 +++- .../2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 4 ++ Modules/_datetimemodule.c | 47 ++++++++++++++++++- 6 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 2bd25cc4362b26..6292770d4e6427 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,7 +670,7 @@ Instance methods: .. method:: date.isocalendar() - Return a 3-tuple, (ISO year, ISO week number, ISO weekday). + Return a 3-namedtuple, (ISO year, ISO week number, ISO weekday). The ISO calendar is a widely used variant of the Gregorian calendar. [#]_ @@ -688,6 +688,10 @@ Instance methods: >>> date(2004, 1, 4).isocalendar() (2004, 1, 7) + .. versionchanged:: 3.9 + Result changed from a tuple to a namedtuple. + It can't be unpickled in previous versions. + .. method:: date.isoformat() Return a string representing the date in ISO 8601 format, ``YYYY-MM-DD``:: @@ -1399,8 +1403,8 @@ Instance methods: .. method:: datetime.isocalendar() - Return a 3-tuple, (ISO year, ISO week number, ISO weekday). The same as - ``self.date().isocalendar()``. + Return a 3-namedtuple, (ISO year, ISO week number, ISO weekday). + The same as ``self.date().isocalendar()``. .. method:: datetime.isoformat(sep='T', timespec='auto') diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 7cf49bfbb93f94..706e171f53b979 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -145,6 +145,14 @@ Add :func:`curses.get_escdelay`, :func:`curses.set_escdelay`, :func:`curses.get_tabsize`, and :func:`curses.set_tabsize` functions. (Contributed by Anthony Sottile in :issue:`38312`.) +datetime +-------- +The :meth:`~datetime.date.isocalendar()` of :class:`datetime.date` +and :meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime` +methods now returns a :func:`~collections.namedtuple` instead of a :class:`tuple`. +It can't be unpickled in previous versions. +(Contributed by Dong-hee Na in :issue:`24416`.) + fcntl ----- diff --git a/Lib/datetime.py b/Lib/datetime.py index 67555191d02c18..dc402a150ea534 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -12,6 +12,9 @@ import math as _math import sys +from collections import namedtuple + + def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -1095,7 +1098,7 @@ def isoweekday(self): return self.toordinal() % 7 or 7 def isocalendar(self): - """Return a 3-tuple containing ISO year, week number, and weekday. + """Return a 3-named tuple containing ISO year, week number, and weekday. The first ISO week of the year is the (Mon-Sun) week containing the year's first Thursday; everything else derives @@ -1120,7 +1123,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return year, week+1, day+1 + return IsoCalendarDate(year, week+1, day+1) # Pickle support. @@ -1559,6 +1562,9 @@ def __reduce__(self): time.max = time(23, 59, 59, 999999) time.resolution = timedelta(microseconds=1) +IsoCalendarDate = namedtuple('IsoCalendarDate', 'year week weekday') + + class datetime(date): """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 42e2cecaeb724e..b57f8a32322262 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -76,7 +76,8 @@ def test_name_cleanup(self): names = set(name for name in dir(datetime) if not name.startswith('__') and not name.endswith('__')) allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime', - 'datetime_CAPI', 'time', 'timedelta', 'timezone', + 'datetime_CAPI', 'IsoCalendarDate', 'namedtuple', + 'time', 'timedelta', 'timezone', 'tzinfo', 'sys']) self.assertEqual(names - allowed, set([])) @@ -1369,6 +1370,13 @@ def test_isocalendar(self): d = self.theclass(2010, 1, 4+i) self.assertEqual(d.isocalendar(), (2010, 1, i+1)) + def test_isocalendar_named(self): + d = self.theclass(2008, 12, 3) + t = d.isocalendar() + self.assertEqual(t.year, 2008) + self.assertEqual(t.week, 49) + self.assertEqual(t.weekday, 3) + def test_iso_long_years(self): # Calculate long ISO years and compare to table from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm diff --git a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst new file mode 100644 index 00000000000000..7a0d8ae8558a1c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst @@ -0,0 +1,4 @@ +The :meth:`~datetime.date.isocalendar()` of :class:`datetime.date` and +:meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime` +methods now returns a :func:`~collections.namedtuple` instead of a +:class:`tuple`. It can't be unpickled in previous versions. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c1b24073436e63..3f5cbb90d15581 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3225,6 +3225,26 @@ date_isoweekday(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) return PyLong_FromLong(dow + 1); } +static PyStructSequence_Field struct_iso_calendar_date_fields[] = { + {"year", "year, for example, 1993"}, + {"week", "week, range [1, 53]"}, + {"weekday", "week day, range [1, 7]"}, + {0} +}; + +static PyStructSequence_Desc struct_iso_calendar_date_desc = { + "datetime.IsoCalendarDate", + "The result of date.isocalendar() or datetime.isocalendar().\n\n" + "This object may be accessed either as a tuple of\n" + " (year,week, weekday)\n" + "or via the object attributes as named in the above tuple.", + struct_iso_calendar_date_fields, + 3 +}; + +static int initialized; +static PyTypeObject StructIsoCalendarDateType; + static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) { @@ -3234,6 +3254,9 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) int week; int day; + PyObject* v = PyStructSequence_New(&StructIsoCalendarDateType); + if (!v) return NULL; + week = divmod(today - week1_monday, 7, &day); if (week < 0) { --year; @@ -3244,7 +3267,16 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) ++year; week = 0; } - return Py_BuildValue("iii", year, week + 1, day + 1); + + PyStructSequence_SET_ITEM(v, 0, PyLong_FromLong(year)); + PyStructSequence_SET_ITEM(v, 1, PyLong_FromLong(week + 1)); + PyStructSequence_SET_ITEM(v, 2, PyLong_FromLong(day + 1)); + + if (PyErr_Occurred()) { + Py_XDECREF(v); + return NULL; + } + return v; } /* Miscellaneous methods. */ @@ -3383,7 +3415,7 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Return time tuple, compatible with time.localtime().")}, {"isocalendar", (PyCFunction)date_isocalendar, METH_NOARGS, - PyDoc_STR("Return a 3-tuple containing ISO year, week number, and " + PyDoc_STR("Return a 3-named tuple containing ISO year, week number, and " "weekday.")}, {"isoformat", (PyCFunction)date_isoformat, METH_NOARGS, @@ -6516,6 +6548,17 @@ PyInit__datetime(void) PyModule_AddIntMacro(m, MINYEAR); PyModule_AddIntMacro(m, MAXYEAR); + /* IsoCalendarDate */ + if (!initialized) { + if (PyStructSequence_InitType2(&StructIsoCalendarDateType, + &struct_iso_calendar_date_desc) < 0) { + return NULL; + } + initialized = 1; + } + Py_INCREF((PyObject *) &StructIsoCalendarDateType); + PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); + Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); From 309b1f150b2e77a8d2c3ed1645b9013d1a486c52 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Sun, 1 Sep 2019 22:28:33 +0900 Subject: [PATCH 02/20] bpo-24416: Apply code review --- Doc/library/datetime.rst | 4 ++-- Lib/datetime.py | 7 +++--- Lib/test/datetimetester.py | 1 + .../2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 5 ++-- Modules/_datetimemodule.c | 23 +++++++++++-------- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 6292770d4e6427..a73c93529ede5d 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,7 +670,7 @@ Instance methods: .. method:: date.isocalendar() - Return a 3-namedtuple, (ISO year, ISO week number, ISO weekday). + Return a named tuple, (ISO year, ISO week number, ISO weekday). The ISO calendar is a widely used variant of the Gregorian calendar. [#]_ @@ -1403,7 +1403,7 @@ Instance methods: .. method:: datetime.isocalendar() - Return a 3-namedtuple, (ISO year, ISO week number, ISO weekday). + Return a named tuple, (ISO year, ISO week number, ISO weekday). The same as ``self.date().isocalendar()``. diff --git a/Lib/datetime.py b/Lib/datetime.py index dc402a150ea534..571903897c4940 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -15,6 +15,9 @@ from collections import namedtuple +IsoCalendarDate = namedtuple('IsoCalendarDate', 'year week weekday') + + def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -1098,7 +1101,7 @@ def isoweekday(self): return self.toordinal() % 7 or 7 def isocalendar(self): - """Return a 3-named tuple containing ISO year, week number, and weekday. + """Return a named tuple containing ISO year, week number, and weekday. The first ISO week of the year is the (Mon-Sun) week containing the year's first Thursday; everything else derives @@ -1562,8 +1565,6 @@ def __reduce__(self): time.max = time(23, 59, 59, 999999) time.resolution = timedelta(microseconds=1) -IsoCalendarDate = namedtuple('IsoCalendarDate', 'year week weekday') - class datetime(date): """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index b57f8a32322262..42dd56206758f3 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1376,6 +1376,7 @@ def test_isocalendar_named(self): self.assertEqual(t.year, 2008) self.assertEqual(t.week, 49) self.assertEqual(t.weekday, 3) + self.assertEqual((2008, 49, 3), t) def test_iso_long_years(self): # Calculate long ISO years and compare to table from diff --git a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst index 7a0d8ae8558a1c..4a95fb13ac7d43 100644 --- a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst +++ b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst @@ -1,4 +1,3 @@ -The :meth:`~datetime.date.isocalendar()` of :class:`datetime.date` and -:meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime` -methods now returns a :func:`~collections.namedtuple` instead of a +The ``isocalendar()`` methods of :class:`datetime.date` and +:class:`datetime.datetime` now return a :term:`named tuple` instead of a :class:`tuple`. It can't be unpickled in previous versions. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 3f5cbb90d15581..068043ff89f8ee 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3232,12 +3232,15 @@ static PyStructSequence_Field struct_iso_calendar_date_fields[] = { {0} }; +PyDoc_STRVAR(struct_iso_calendar_date__doc__, +"The result of date.isocalendar() or datetime.isocalendar()\n\n\ +This object may be accessed either as a tuple of\n\ + ((year, week, weekday)\n\ +or via the object attributes as named in the above tuple."); + static PyStructSequence_Desc struct_iso_calendar_date_desc = { "datetime.IsoCalendarDate", - "The result of date.isocalendar() or datetime.isocalendar().\n\n" - "This object may be accessed either as a tuple of\n" - " (year,week, weekday)\n" - "or via the object attributes as named in the above tuple.", + struct_iso_calendar_date__doc__, struct_iso_calendar_date_fields, 3 }; @@ -3254,9 +3257,6 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) int week; int day; - PyObject* v = PyStructSequence_New(&StructIsoCalendarDateType); - if (!v) return NULL; - week = divmod(today - week1_monday, 7, &day); if (week < 0) { --year; @@ -3268,12 +3268,17 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) week = 0; } + PyObject *v = PyStructSequence_New(&StructIsoCalendarDateType); + if (v == NULL) { + return NULL; + } + PyStructSequence_SET_ITEM(v, 0, PyLong_FromLong(year)); PyStructSequence_SET_ITEM(v, 1, PyLong_FromLong(week + 1)); PyStructSequence_SET_ITEM(v, 2, PyLong_FromLong(day + 1)); if (PyErr_Occurred()) { - Py_XDECREF(v); + Py_DECREF(v); return NULL; } return v; @@ -3415,7 +3420,7 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Return time tuple, compatible with time.localtime().")}, {"isocalendar", (PyCFunction)date_isocalendar, METH_NOARGS, - PyDoc_STR("Return a 3-named tuple containing ISO year, week number, and " + PyDoc_STR("Return a named tuple containing ISO year, week number, and " "weekday.")}, {"isoformat", (PyCFunction)date_isoformat, METH_NOARGS, From bf168c935b2ce177f46bfc300a74035ba17dae2b Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 00:46:50 +0900 Subject: [PATCH 03/20] bpo-24416: Apply code review --- Doc/library/datetime.rst | 6 +-- Lib/datetime.py | 25 ++++++++-- Lib/test/datetimetester.py | 49 ++++++++++--------- .../2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 4 +- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index a73c93529ede5d..f977b145d5ad73 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,7 +670,7 @@ Instance methods: .. method:: date.isocalendar() - Return a named tuple, (ISO year, ISO week number, ISO weekday). + Return a :class:`IsoCalendarDate`, (ISO year, ISO week number, ISO weekday). The ISO calendar is a widely used variant of the Gregorian calendar. [#]_ @@ -689,7 +689,7 @@ Instance methods: (2004, 1, 7) .. versionchanged:: 3.9 - Result changed from a tuple to a namedtuple. + Result changed from a tuple to a :class:`IsoCalendarDate`. It can't be unpickled in previous versions. .. method:: date.isoformat() @@ -1403,7 +1403,7 @@ Instance methods: .. method:: datetime.isocalendar() - Return a named tuple, (ISO year, ISO week number, ISO weekday). + Return a :class:`IsoCalendarDate`, (ISO year, ISO week number, ISO weekday). The same as ``self.date().isocalendar()``. diff --git a/Lib/datetime.py b/Lib/datetime.py index 571903897c4940..0f227f7211aa64 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -12,10 +12,29 @@ import math as _math import sys -from collections import namedtuple +class IsoCalendarDate(tuple): + def __new__(cls, seq): + if len(seq) != 3: + raise TypeError(f'{cls.__name__}() takes a 3-sequence ' + f'({len(seq)}-sequence given)') + return tuple.__new__(cls, seq) -IsoCalendarDate = namedtuple('IsoCalendarDate', 'year week weekday') + @property + def year(self): + return self[0] + + @property + def week(self): + return self[1] + + @property + def weekday(self): + return self[2] + + def __repr__(self): + return (f'{self.__class__.__name__}' + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') def _cmp(x, y): @@ -1126,7 +1145,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return IsoCalendarDate(year, week+1, day+1) + return IsoCalendarDate((year, week+1, day+1)) # Pickle support. diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 42dd56206758f3..e05098650f0e9a 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -76,7 +76,7 @@ def test_name_cleanup(self): names = set(name for name in dir(datetime) if not name.startswith('__') and not name.endswith('__')) allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime', - 'datetime_CAPI', 'IsoCalendarDate', 'namedtuple', + 'datetime_CAPI', 'IsoCalendarDate', 'time', 'timedelta', 'timezone', 'tzinfo', 'sys']) self.assertEqual(names - allowed, set([])) @@ -1356,27 +1356,32 @@ def test_weekday(self): def test_isocalendar(self): # Check examples from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm - for i in range(7): - d = self.theclass(2003, 12, 22+i) - self.assertEqual(d.isocalendar(), (2003, 52, i+1)) - d = self.theclass(2003, 12, 29) + timedelta(i) - self.assertEqual(d.isocalendar(), (2004, 1, i+1)) - d = self.theclass(2004, 1, 5+i) - self.assertEqual(d.isocalendar(), (2004, 2, i+1)) - d = self.theclass(2009, 12, 21+i) - self.assertEqual(d.isocalendar(), (2009, 52, i+1)) - d = self.theclass(2009, 12, 28) + timedelta(i) - self.assertEqual(d.isocalendar(), (2009, 53, i+1)) - d = self.theclass(2010, 1, 4+i) - self.assertEqual(d.isocalendar(), (2010, 1, i+1)) - - def test_isocalendar_named(self): - d = self.theclass(2008, 12, 3) - t = d.isocalendar() - self.assertEqual(t.year, 2008) - self.assertEqual(t.week, 49) - self.assertEqual(t.weekday, 3) - self.assertEqual((2008, 49, 3), t) + week_mondays = [ + ((2003, 12, 22), (2003, 52, 1)), + ((2003, 12, 29), (2004, 1, 1)), + ((2004, 1, 5), (2004, 2, 1)), + ((2009, 12, 21), (2009, 52, 1)), + ((2009, 12, 28), (2009, 53, 1)), + ((2010, 1, 4), (2010, 1, 1)), + ] + + test_cases = [] + for cal_date, iso_date in week_mondays: + base_date = self.theclass(*cal_date) + # Adds one test case for every day of the specified weeks + for i in range(7): + new_date = base_date + timedelta(i) + new_iso = iso_date[0:2] + (iso_date[2] + i,) + test_cases.append((new_date, new_iso)) + + for d, exp_iso in test_cases: + with self.subTest(d=d, comparison="tuple"): + self.assertEqual(d.isocalendar(), exp_iso) + + # Check that the tuple contents are accessible by field name + with self.subTest(d=d, comparison="fields"): + t = d.isocalendar() + self.assertEqual((t.year, t.week, t.weekday), exp_iso) def test_iso_long_years(self): # Calculate long ISO years and compare to table from diff --git a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst index 4a95fb13ac7d43..66ac3992d5f8a9 100644 --- a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst +++ b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst @@ -1,3 +1,3 @@ The ``isocalendar()`` methods of :class:`datetime.date` and -:class:`datetime.datetime` now return a :term:`named tuple` instead of a -:class:`tuple`. It can't be unpickled in previous versions. +:class:`datetime.datetime` now return a :class:`IsoCalendarDate` +instead of a :class:`tuple`. It can't be unpickled in previous versions. From f3c02ada6c72122d0a6c69ccbd26e11f068580e0 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 00:53:28 +0900 Subject: [PATCH 04/20] bpo-24416: Update docstrings --- Lib/datetime.py | 2 +- Modules/_datetimemodule.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index 0f227f7211aa64..28fbb76647cde5 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1120,7 +1120,7 @@ def isoweekday(self): return self.toordinal() % 7 or 7 def isocalendar(self): - """Return a named tuple containing ISO year, week number, and weekday. + """Return a IsoCalendarDate containing ISO year, week number, and weekday. The first ISO week of the year is the (Mon-Sun) week containing the year's first Thursday; everything else derives diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 068043ff89f8ee..3b3427d9e97bb9 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3420,7 +3420,7 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Return time tuple, compatible with time.localtime().")}, {"isocalendar", (PyCFunction)date_isocalendar, METH_NOARGS, - PyDoc_STR("Return a named tuple containing ISO year, week number, and " + PyDoc_STR("Return a IsoCalendarDate containing ISO year, week number, and " "weekday.")}, {"isoformat", (PyCFunction)date_isoformat, METH_NOARGS, From 9ae18863cdc7760e5a667a0e60352b29e9e75e60 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 09:30:07 +0900 Subject: [PATCH 05/20] bpo-24416: Apply third's codereview --- Doc/library/datetime.rst | 9 +++--- Lib/datetime.py | 58 +++++++++++++++++++++----------------- Lib/test/datetimetester.py | 7 ++--- Modules/_datetimemodule.c | 5 ++-- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index f977b145d5ad73..46a12e6c77050c 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,7 +670,8 @@ Instance methods: .. method:: date.isocalendar() - Return a :class:`IsoCalendarDate`, (ISO year, ISO week number, ISO weekday). + Return a :term:`named tuple` object with three components: `year`, `week` + and `weekday`. The ISO calendar is a widely used variant of the Gregorian calendar. [#]_ @@ -689,8 +690,7 @@ Instance methods: (2004, 1, 7) .. versionchanged:: 3.9 - Result changed from a tuple to a :class:`IsoCalendarDate`. - It can't be unpickled in previous versions. + Result changed from a tuple to a :term:`named tuple`. .. method:: date.isoformat() @@ -1403,7 +1403,8 @@ Instance methods: .. method:: datetime.isocalendar() - Return a :class:`IsoCalendarDate`, (ISO year, ISO week number, ISO weekday). + Return a :term:`named tuple` object with three components: `year`, `week` + and `weekday`. The same as ``self.date().isocalendar()``. diff --git a/Lib/datetime.py b/Lib/datetime.py index 28fbb76647cde5..9ef5f0fd158243 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -13,30 +13,6 @@ import sys -class IsoCalendarDate(tuple): - def __new__(cls, seq): - if len(seq) != 3: - raise TypeError(f'{cls.__name__}() takes a 3-sequence ' - f'({len(seq)}-sequence given)') - return tuple.__new__(cls, seq) - - @property - def year(self): - return self[0] - - @property - def week(self): - return self[1] - - @property - def weekday(self): - return self[2] - - def __repr__(self): - return (f'{self.__class__.__name__}' - f'(year={self[0]}, week={self[1]}, weekday={self[2]})') - - def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -1145,7 +1121,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return IsoCalendarDate((year, week+1, day+1)) + return _IsoCalendarDate((year, week+1, day+1)) # Pickle support. @@ -1235,6 +1211,36 @@ def __reduce__(self): else: return (self.__class__, args, state) + +class _IsoCalendarDate(tuple): + + def __new__(cls, seq): + if len(seq) != 3: + raise TypeError(f'{cls.__name__}() takes a 3-sequence ' + f'({len(seq)}-sequence given)') + return tuple.__new__(cls, seq) + + @property + def year(self): + return self[0] + + @property + def week(self): + return self[1] + + @property + def weekday(self): + return self[2] + + def __repr__(self): + return ('datetime.IsoCalendarDate' + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') + + +_IsoCalendarDate.__name__ = "IsoCalendarDate" +_IsoCalendarDate.__qualname__ = "IsoCalendarDate" + + _tzinfo_class = tzinfo class time: @@ -2540,7 +2546,7 @@ def _name_from_offset(delta): _format_time, _format_offset, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff) + _parse_hh_mm_ss_ff, _IsoCalendarDate) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e05098650f0e9a..9339b6111ae0ad 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1375,13 +1375,10 @@ def test_isocalendar(self): test_cases.append((new_date, new_iso)) for d, exp_iso in test_cases: - with self.subTest(d=d, comparison="tuple"): - self.assertEqual(d.isocalendar(), exp_iso) - - # Check that the tuple contents are accessible by field name - with self.subTest(d=d, comparison="fields"): + with self.subTest(d=d, comparison="fields and tuple"): t = d.isocalendar() self.assertEqual((t.year, t.week, t.weekday), exp_iso) + self.assertEqual(d.isocalendar(), exp_iso) def test_iso_long_years(self): # Calculate long ISO years and compare to table from diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 3b3427d9e97bb9..1a788fd175c801 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3420,7 +3420,7 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Return time tuple, compatible with time.localtime().")}, {"isocalendar", (PyCFunction)date_isocalendar, METH_NOARGS, - PyDoc_STR("Return a IsoCalendarDate containing ISO year, week number, and " + PyDoc_STR("Return a named tuple containing ISO year, week number, and " "weekday.")}, {"isoformat", (PyCFunction)date_isoformat, METH_NOARGS, @@ -6561,8 +6561,7 @@ PyInit__datetime(void) } initialized = 1; } - Py_INCREF((PyObject *) &StructIsoCalendarDateType); - PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); + Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); From c592159c697167f1200e9fa4ea0a930c21c96a71 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 13:24:55 +0900 Subject: [PATCH 06/20] bpo-24416: Revert to support pickle --- Doc/library/datetime.rst | 9 ++++----- Lib/datetime.py | 14 +++++--------- Modules/_datetimemodule.c | 3 ++- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 46a12e6c77050c..0f2951a3ee037d 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,8 +670,8 @@ Instance methods: .. method:: date.isocalendar() - Return a :term:`named tuple` object with three components: `year`, `week` - and `weekday`. + Return a :term:`named tuple` object with three components: ``year``, + ``week`` and ``weekday``. The ISO calendar is a widely used variant of the Gregorian calendar. [#]_ @@ -1403,9 +1403,8 @@ Instance methods: .. method:: datetime.isocalendar() - Return a :term:`named tuple` object with three components: `year`, `week` - and `weekday`. - The same as ``self.date().isocalendar()``. + Return a :term:`named tuple` with three components: ``year``, ``week`` + and ``weekday``. The same as ``self.date().isocalendar()``. .. method:: datetime.isoformat(sep='T', timespec='auto') diff --git a/Lib/datetime.py b/Lib/datetime.py index 9ef5f0fd158243..fa3c46bdd65890 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1096,7 +1096,7 @@ def isoweekday(self): return self.toordinal() % 7 or 7 def isocalendar(self): - """Return a IsoCalendarDate containing ISO year, week number, and weekday. + """Return a named tuple containing ISO year, week number, and weekday. The first ISO week of the year is the (Mon-Sun) week containing the year's first Thursday; everything else derives @@ -1121,7 +1121,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return _IsoCalendarDate((year, week+1, day+1)) + return IsoCalendarDate((year, week+1, day+1)) # Pickle support. @@ -1212,7 +1212,7 @@ def __reduce__(self): return (self.__class__, args, state) -class _IsoCalendarDate(tuple): +class IsoCalendarDate(tuple): def __new__(cls, seq): if len(seq) != 3: @@ -1233,14 +1233,10 @@ def weekday(self): return self[2] def __repr__(self): - return ('datetime.IsoCalendarDate' + return (f'{self.__class__.__name__}' f'(year={self[0]}, week={self[1]}, weekday={self[2]})') -_IsoCalendarDate.__name__ = "IsoCalendarDate" -_IsoCalendarDate.__qualname__ = "IsoCalendarDate" - - _tzinfo_class = tzinfo class time: @@ -2546,7 +2542,7 @@ def _name_from_offset(delta): _format_time, _format_offset, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff, _IsoCalendarDate) + _parse_hh_mm_ss_ff) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 1a788fd175c801..068043ff89f8ee 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6561,7 +6561,8 @@ PyInit__datetime(void) } initialized = 1; } - + Py_INCREF((PyObject *) &StructIsoCalendarDateType); + PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); From 87ad62c7aa234780863640750fa6a92dc658c1d1 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 13:26:50 +0900 Subject: [PATCH 07/20] bpo-24416: Update misc/news --- .../next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst index 66ac3992d5f8a9..ee9af990f079db 100644 --- a/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst +++ b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst @@ -1,3 +1,3 @@ The ``isocalendar()`` methods of :class:`datetime.date` and -:class:`datetime.datetime` now return a :class:`IsoCalendarDate` -instead of a :class:`tuple`. It can't be unpickled in previous versions. +:class:`datetime.datetime` now return a :term:`named tuple` +instead of a :class:`tuple`. From 63d1ed9a4d48cd32408e1044ab6b952a6ad93d09 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 15:56:41 +0900 Subject: [PATCH 08/20] bpo-24416: Update __new__ --- Lib/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index fa3c46bdd65890..eae63c4a668c92 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1218,7 +1218,7 @@ def __new__(cls, seq): if len(seq) != 3: raise TypeError(f'{cls.__name__}() takes a 3-sequence ' f'({len(seq)}-sequence given)') - return tuple.__new__(cls, seq) + return super().__new__(cls, seq) @property def year(self): From 752aad8d77db1ad5c7464b50b19f6ceb090f9e2a Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Wed, 11 Sep 2019 12:43:04 +0100 Subject: [PATCH 09/20] Change pickling behavior of isocalendar() objects In order to leave IsocalendarDate as a private class and to improve what backwards compatibility is offered for pickling the result of a datetime.isocalendar() call, add a __reduce__ method to the named tuples that reduces them to plain tuples. Also add a test to this effect. --- Lib/datetime.py | 5 +++- Lib/test/datetimetester.py | 15 +++++++++++ Modules/_datetimemodule.c | 55 ++++++++++++++++++++++++++++++++------ 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index eae63c4a668c92..19f0754b7feb36 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1232,9 +1232,12 @@ def week(self): def weekday(self): return self[2] + def __reduce__(self): + return (tuple, (tuple(self),)) + def __repr__(self): return (f'{self.__class__.__name__}' - f'(year={self[0]}, week={self[1]}, weekday={self[2]})') + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') _tzinfo_class = tzinfo diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 9339b6111ae0ad..4ef93df7aa355d 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2,6 +2,7 @@ See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ +import io import itertools import bisect import copy @@ -1380,6 +1381,20 @@ def test_isocalendar(self): self.assertEqual((t.year, t.week, t.weekday), exp_iso) self.assertEqual(d.isocalendar(), exp_iso) + def test_isocalendar_pickling(self): + """Test that the result of datetime.isocalendar() can be pickled. + + The result of a round trip should be a plain tuple. + """ + d = self.theclass(2019, 1, 1) + f = io.BytesIO() + pickle.dump(d.isocalendar(), f) + f.seek(0) + + res = pickle.load(f) + + self.assertEqual(res, (2019, 1, 2)) + def test_iso_long_years(self): # Calculate long ISO years and compare to table from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 068043ff89f8ee..27971317e2dadb 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3245,9 +3245,53 @@ static PyStructSequence_Desc struct_iso_calendar_date_desc = { 3 }; -static int initialized; +static int isocalendardate_initialized; static PyTypeObject StructIsoCalendarDateType; +// Required for pickling as a tuple +static PyObject * +isocalendardate_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + // Construct the tuple that this reduces to + PyObject * reduce_tuple = Py_BuildValue( + "O((OOO))", &PyTuple_Type, + PyStructSequence_GET_ITEM(self, 0), + PyStructSequence_GET_ITEM(self, 1), + PyStructSequence_GET_ITEM(self, 2) + ); + + return reduce_tuple; +} + +/* Method to add our custom reduce type to the IsoCalendarDate type */ +static int +_initialize_isocalendardate() { + if (isocalendardate_initialized) { + return 0; + } + + if (PyStructSequence_InitType2(&StructIsoCalendarDateType, + &struct_iso_calendar_date_desc) < 0) { + return -1; + } + + // Add our custom __reduce__ + PyMethodDef *method = StructIsoCalendarDateType.tp_methods; + int reduce_missing = 1; + while (method != NULL) { + if (strcmp("__reduce__", method->ml_name) == 0) { + reduce_missing = 0; + method->ml_meth = (PyCFunction)isocalendardate_reduce; + method->ml_doc = "Reduce IsoCalendarDate to a simple tuple"; + + break; + } + } + + assert(!reduce_missing); + return 0; +} + static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) { @@ -6554,15 +6598,10 @@ PyInit__datetime(void) PyModule_AddIntMacro(m, MAXYEAR); /* IsoCalendarDate */ - if (!initialized) { - if (PyStructSequence_InitType2(&StructIsoCalendarDateType, - &struct_iso_calendar_date_desc) < 0) { - return NULL; - } - initialized = 1; + if (_initialize_isocalendardate()) { + return NULL; } Py_INCREF((PyObject *) &StructIsoCalendarDateType); - PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); From 4b2dc8b373f9acebcd020db86e468feeb92b0faa Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Wed, 11 Sep 2019 12:47:15 +0100 Subject: [PATCH 10/20] Rename IsoCalendarDate to _IsoCalendarDate --- Lib/datetime.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index 19f0754b7feb36..b806ee099a7e32 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1121,7 +1121,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return IsoCalendarDate((year, week+1, day+1)) + return _IsoCalendarDate((year, week+1, day+1)) # Pickle support. @@ -1239,6 +1239,9 @@ def __repr__(self): return (f'{self.__class__.__name__}' f'(year={self[0]}, week={self[1]}, weekday={self[2]})') +_IsoCalendarDate = IsoCalendarDate +del IsoCalendarDate + _tzinfo_class = tzinfo @@ -2545,7 +2548,7 @@ def _name_from_offset(delta): _format_time, _format_offset, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff) + _parse_hh_mm_ss_ff, _IsoCalendarDate) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and From 205b6c54aeef1532d8dbfc14d869b264fb63c7cb Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 21:52:58 +0900 Subject: [PATCH 11/20] bpo-24416: Update datetime.rst --- Doc/library/datetime.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 0f2951a3ee037d..ea0d3c0d2d9592 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -683,11 +683,11 @@ Instance methods: For example, 2004 begins on a Thursday, so the first week of ISO year 2004 begins on Monday, 29 Dec 2003 and ends on Sunday, 4 Jan 2004:: - >>> from datetime import date - >>> date(2003, 12, 29).isocalendar() - (2004, 1, 1) - >>> date(2004, 1, 4).isocalendar() - (2004, 1, 7) + >>> from datetime import date + >>> date(2003, 12, 29).isocalendar() + datetime.IsoCalendarDate(year=2004, week=1, weekday=1) + >>> date(2004, 1, 4).isocalendar() + datetime.IsoCalendarDate(year=2004, week=1, weekday=7) .. versionchanged:: 3.9 Result changed from a tuple to a :term:`named tuple`. From 32cc8c183873bb01a3dfc7f35f394be2de403077 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 11 Sep 2019 21:57:37 +0900 Subject: [PATCH 12/20] bpo-24416: Update unittest --- Lib/datetime.py | 1 - Lib/test/datetimetester.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index b806ee099a7e32..0fefb4262d2838 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -12,7 +12,6 @@ import math as _math import sys - def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 4ef93df7aa355d..ccbb31c239bcfb 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1376,10 +1376,13 @@ def test_isocalendar(self): test_cases.append((new_date, new_iso)) for d, exp_iso in test_cases: - with self.subTest(d=d, comparison="fields and tuple"): + with self.subTest(d=d, comparison="tuple"): + self.assertEqual(d.isocalendar(), exp_iso) + + # Check that the tuple contents are accessible by field name + with self.subTest(d=d, comparison="fields"): t = d.isocalendar() self.assertEqual((t.year, t.week, t.weekday), exp_iso) - self.assertEqual(d.isocalendar(), exp_iso) def test_isocalendar_pickling(self): """Test that the result of datetime.isocalendar() can be pickled. From 41057ff40714cc0d8d30f83dab8531e481a25de6 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Wed, 11 Sep 2019 15:03:55 +0100 Subject: [PATCH 13/20] Revert "Rename IsoCalendarDate to _IsoCalendarDate" This reverts commit 448a2109dfde1558a082de78171d17580995a950. --- Lib/datetime.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index 0fefb4262d2838..fb44f211a77487 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1120,7 +1120,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return _IsoCalendarDate((year, week+1, day+1)) + return IsoCalendarDate((year, week+1, day+1)) # Pickle support. @@ -1238,9 +1238,6 @@ def __repr__(self): return (f'{self.__class__.__name__}' f'(year={self[0]}, week={self[1]}, weekday={self[2]})') -_IsoCalendarDate = IsoCalendarDate -del IsoCalendarDate - _tzinfo_class = tzinfo @@ -2547,7 +2544,7 @@ def _name_from_offset(delta): _format_time, _format_offset, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff, _IsoCalendarDate) + _parse_hh_mm_ss_ff) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and From 9c98a61ad2d127c1883ef9cabca6c77c72f76649 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Wed, 11 Sep 2019 15:03:57 +0100 Subject: [PATCH 14/20] Revert "Change pickling behavior of isocalendar() objects" This reverts commit 104264121ea49795da87fc803a270bdda8fa21d2. --- Lib/datetime.py | 5 +--- Lib/test/datetimetester.py | 15 ----------- Modules/_datetimemodule.c | 55 ++++++-------------------------------- 3 files changed, 9 insertions(+), 66 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index fb44f211a77487..dc55013d1cf860 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1231,12 +1231,9 @@ def week(self): def weekday(self): return self[2] - def __reduce__(self): - return (tuple, (tuple(self),)) - def __repr__(self): return (f'{self.__class__.__name__}' - f'(year={self[0]}, week={self[1]}, weekday={self[2]})') + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') _tzinfo_class = tzinfo diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index ccbb31c239bcfb..e05098650f0e9a 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2,7 +2,6 @@ See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ -import io import itertools import bisect import copy @@ -1384,20 +1383,6 @@ def test_isocalendar(self): t = d.isocalendar() self.assertEqual((t.year, t.week, t.weekday), exp_iso) - def test_isocalendar_pickling(self): - """Test that the result of datetime.isocalendar() can be pickled. - - The result of a round trip should be a plain tuple. - """ - d = self.theclass(2019, 1, 1) - f = io.BytesIO() - pickle.dump(d.isocalendar(), f) - f.seek(0) - - res = pickle.load(f) - - self.assertEqual(res, (2019, 1, 2)) - def test_iso_long_years(self): # Calculate long ISO years and compare to table from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 27971317e2dadb..068043ff89f8ee 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3245,53 +3245,9 @@ static PyStructSequence_Desc struct_iso_calendar_date_desc = { 3 }; -static int isocalendardate_initialized; +static int initialized; static PyTypeObject StructIsoCalendarDateType; -// Required for pickling as a tuple -static PyObject * -isocalendardate_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) -{ - // Construct the tuple that this reduces to - PyObject * reduce_tuple = Py_BuildValue( - "O((OOO))", &PyTuple_Type, - PyStructSequence_GET_ITEM(self, 0), - PyStructSequence_GET_ITEM(self, 1), - PyStructSequence_GET_ITEM(self, 2) - ); - - return reduce_tuple; -} - -/* Method to add our custom reduce type to the IsoCalendarDate type */ -static int -_initialize_isocalendardate() { - if (isocalendardate_initialized) { - return 0; - } - - if (PyStructSequence_InitType2(&StructIsoCalendarDateType, - &struct_iso_calendar_date_desc) < 0) { - return -1; - } - - // Add our custom __reduce__ - PyMethodDef *method = StructIsoCalendarDateType.tp_methods; - int reduce_missing = 1; - while (method != NULL) { - if (strcmp("__reduce__", method->ml_name) == 0) { - reduce_missing = 0; - method->ml_meth = (PyCFunction)isocalendardate_reduce; - method->ml_doc = "Reduce IsoCalendarDate to a simple tuple"; - - break; - } - } - - assert(!reduce_missing); - return 0; -} - static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) { @@ -6598,10 +6554,15 @@ PyInit__datetime(void) PyModule_AddIntMacro(m, MAXYEAR); /* IsoCalendarDate */ - if (_initialize_isocalendardate()) { - return NULL; + if (!initialized) { + if (PyStructSequence_InitType2(&StructIsoCalendarDateType, + &struct_iso_calendar_date_desc) < 0) { + return NULL; + } + initialized = 1; } Py_INCREF((PyObject *) &StructIsoCalendarDateType); + PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); From d4ce1e77c73bdf56b4f957b3e11641897bc6e9b0 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Thu, 12 Sep 2019 02:45:27 +0900 Subject: [PATCH 15/20] bpo-24416: Add pickle test --- Lib/test/datetimetester.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e05098650f0e9a..990ff504bade72 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2,6 +2,7 @@ See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ +import io import itertools import bisect import copy @@ -1383,6 +1384,19 @@ def test_isocalendar(self): t = d.isocalendar() self.assertEqual((t.year, t.week, t.weekday), exp_iso) + def test_isocalendar_pickling(self): + """Test that the result of datetime.isocalendar() can be pickled. + + The result of a round trip should be a plain tuple. + """ + d = self.theclass(2019, 1, 1) + f = io.BytesIO() + pickle.dump(d.isocalendar(), f) + f.seek(0) + res = pickle.load(f) + + self.assertEqual(res, (2019, 1, 2)) + def test_iso_long_years(self): # Calculate long ISO years and compare to table from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm From 2987fdc791e9df44c133a9ee76d6c1553a8a6eb1 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Sat, 21 Sep 2019 22:14:52 +0900 Subject: [PATCH 16/20] bpo-24416: Update C API to subclass tuple --- Modules/_datetimemodule.c | 189 +++++++++++++++++++++++------ Modules/clinic/_datetimemodule.c.h | 27 ++++- 2 files changed, 180 insertions(+), 36 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 068043ff89f8ee..64c2c8b7b11821 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -38,8 +38,9 @@ module datetime class datetime.datetime "PyDateTime_DateTime *" "&PyDateTime_DateTimeType" class datetime.date "PyDateTime_Date *" "&PyDateTime_DateType" +class datetime.IsoCalendarDate "PyDateTime_IsoCalendarDate *" "&PyDateTime_IsoCalendarDateType" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=25138ad6a696b785]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=81bec0fa19837f63]*/ #include "clinic/_datetimemodule.c.h" @@ -131,6 +132,7 @@ class datetime.date "PyDateTime_Date *" "&PyDateTime_DateType" static PyTypeObject PyDateTime_DateType; static PyTypeObject PyDateTime_DateTimeType; static PyTypeObject PyDateTime_DeltaType; +static PyTypeObject PyDateTime_IsoCalendarDateType; static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; static PyTypeObject PyDateTime_TimeZoneType; @@ -3225,28 +3227,153 @@ date_isoweekday(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) return PyLong_FromLong(dow + 1); } -static PyStructSequence_Field struct_iso_calendar_date_fields[] = { - {"year", "year, for example, 1993"}, - {"week", "week, range [1, 53]"}, - {"weekday", "week day, range [1, 7]"}, - {0} -}; - -PyDoc_STRVAR(struct_iso_calendar_date__doc__, +PyDoc_STRVAR(iso_calendar_date__doc__, "The result of date.isocalendar() or datetime.isocalendar()\n\n\ This object may be accessed either as a tuple of\n\ ((year, week, weekday)\n\ or via the object attributes as named in the above tuple."); -static PyStructSequence_Desc struct_iso_calendar_date_desc = { - "datetime.IsoCalendarDate", - struct_iso_calendar_date__doc__, - struct_iso_calendar_date_fields, - 3 +typedef struct { + PyTupleObject tuple; +} PyDateTime_IsoCalendarDate; + +static PyObject * +iso_calendar_date_repr(PyDateTime_IsoCalendarDate *self) +{ + PyObject* year = PyTuple_GetItem((PyObject *)self, 0); + if (year == NULL) { + return NULL; + } + PyObject* week = PyTuple_GetItem((PyObject *)self, 1); + if (week == NULL) { + return NULL; + } + PyObject* weekday = PyTuple_GetItem((PyObject *)self, 2); + if (weekday == NULL) { + return NULL; + } + + return PyUnicode_FromFormat("%.200s(year=%S, week=%S, weekday=%S)", + Py_TYPE(self)->tp_name, year, week, weekday); +} + +static PyObject * +iso_calendar_date_year(PyDateTime_IsoCalendarDate *self, void *unused) +{ + PyObject* year = PyTuple_GetItem((PyObject *)self, 0); + if(year == NULL) { + return NULL; + } + Py_INCREF(year); + return year; +} + +static PyObject * +iso_calendar_date_week(PyDateTime_IsoCalendarDate *self, void *unused) +{ + PyObject* week = PyTuple_GetItem((PyObject *)self, 1); + if (week == NULL) { + return NULL; + } + Py_INCREF(week); + return week; +} + +static PyObject * +iso_calendar_date_weekday(PyDateTime_IsoCalendarDate *self, void *unused) +{ + PyObject* weekday = PyTuple_GetItem((PyObject *)self, 2); + if(weekday == NULL) { + return NULL; + } + Py_INCREF(weekday); + return weekday; +} + +static PyGetSetDef iso_calendar_date_getset[] = { + {"year", (getter)iso_calendar_date_year}, + {"week", (getter)iso_calendar_date_week}, + {"weekday", (getter)iso_calendar_date_weekday}, + {NULL} }; -static int initialized; -static PyTypeObject StructIsoCalendarDateType; +static PyTypeObject PyDateTime_IsoCalendarDateType = { + PyVarObject_HEAD_INIT(NULL, 0) + "datetime.IsoCalendarDate", /* tp_name */ + sizeof(PyDateTime_IsoCalendarDate), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_vectorcall_offset */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc) iso_calendar_date_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + iso_calendar_date__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + iso_calendar_date_getset, /* tp_getset */ + &PyTuple_Type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + iso_calendar_date_new, /* tp_new */ +}; + +/*[clinic input] +@classmethod +datetime.IsoCalendarDate.__new__ as iso_calendar_date_new + sequence as seq: object +[clinic start generated code]*/ + +static PyObject * +iso_calendar_date_new_impl(PyTypeObject *type, PyObject *seq) +/*[clinic end generated code: output=bf2e1982e7012dae input=f9bb6626d4947edc]*/ +{ + PyDateTime_IsoCalendarDate* self; + Py_ssize_t len = PyObject_Length(seq); + if (len == -1) { + return NULL; + } + + if (len != 3) { + PyErr_Format(PyExc_TypeError, + "%.500s() takes a 3-sequence (%zd-sequence given)", + type->tp_name, len); + return NULL; + } + + self = (PyDateTime_IsoCalendarDate *) type->tp_alloc(type, len); + if (self == NULL) { + return NULL; + } + + for (int i = 0; i < len; ++i) { + PyObject* v = PyTuple_GetItem(seq, i); + Py_INCREF(v); + PyTuple_SET_ITEM(self, i, v); + } + + return (PyObject *)self; +} static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) @@ -3268,19 +3395,17 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) week = 0; } - PyObject *v = PyStructSequence_New(&StructIsoCalendarDateType); - if (v == NULL) { + PyObject *seq = Py_BuildValue("iii", year, week + 1, day + 1); + if (seq == NULL) { return NULL; } - PyStructSequence_SET_ITEM(v, 0, PyLong_FromLong(year)); - PyStructSequence_SET_ITEM(v, 1, PyLong_FromLong(week + 1)); - PyStructSequence_SET_ITEM(v, 2, PyLong_FromLong(day + 1)); - - if (PyErr_Occurred()) { - Py_DECREF(v); + PyObject* v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType, seq); + if(v == NULL) { + Py_DECREF(seq); return NULL; } + Py_DECREF(seq); return v; } @@ -6430,6 +6555,8 @@ PyInit__datetime(void) return NULL; if (PyType_Ready(&PyDateTime_DeltaType) < 0) return NULL; + if (PyType_Ready(&PyDateTime_IsoCalendarDateType) < 0) + return NULL; if (PyType_Ready(&PyDateTime_TimeType) < 0) return NULL; if (PyType_Ready(&PyDateTime_TZInfoType) < 0) @@ -6553,17 +6680,6 @@ PyInit__datetime(void) PyModule_AddIntMacro(m, MINYEAR); PyModule_AddIntMacro(m, MAXYEAR); - /* IsoCalendarDate */ - if (!initialized) { - if (PyStructSequence_InitType2(&StructIsoCalendarDateType, - &struct_iso_calendar_date_desc) < 0) { - return NULL; - } - initialized = 1; - } - Py_INCREF((PyObject *) &StructIsoCalendarDateType); - PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &StructIsoCalendarDateType); - Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); @@ -6571,6 +6687,9 @@ PyInit__datetime(void) PyModule_AddObject(m, "datetime", (PyObject *)&PyDateTime_DateTimeType); + Py_INCREF((PyObject *) &PyDateTime_IsoCalendarDateType); + PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &PyDateTime_IsoCalendarDateType); + Py_INCREF(&PyDateTime_TimeType); PyModule_AddObject(m, "time", (PyObject *) &PyDateTime_TimeType); diff --git a/Modules/clinic/_datetimemodule.c.h b/Modules/clinic/_datetimemodule.c.h index 447036ca03814c..6f121ad72d5b88 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -14,6 +14,31 @@ PyDoc_STRVAR(datetime_date_fromtimestamp__doc__, #define DATETIME_DATE_FROMTIMESTAMP_METHODDEF \ {"fromtimestamp", (PyCFunction)datetime_date_fromtimestamp, METH_O|METH_CLASS, datetime_date_fromtimestamp__doc__}, +static PyObject * +iso_calendar_date_new_impl(PyTypeObject *type, PyObject *seq); + +static PyObject * +iso_calendar_date_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + static const char * const _keywords[] = {"sequence", NULL}; + static _PyArg_Parser _parser = {NULL, _keywords, "IsoCalendarDate", 0}; + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + PyObject *seq; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 1, 0, argsbuf); + if (!fastargs) { + goto exit; + } + seq = fastargs[0]; + return_value = iso_calendar_date_new_impl(type, seq); + +exit: + return return_value; +} + PyDoc_STRVAR(datetime_datetime_now__doc__, "now($type, /, tz=None)\n" "--\n" @@ -55,4 +80,4 @@ datetime_datetime_now(PyTypeObject *type, PyObject *const *args, Py_ssize_t narg exit: return return_value; } -/*[clinic end generated code: output=aae916ab728ca85b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=48b1aa60aca63a57 input=a9049054013a1b77]*/ From 8584bf69ae41b8ad8fd1c4578f2f6a19b9977311 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Sun, 22 Sep 2019 01:02:50 +0900 Subject: [PATCH 17/20] bpo-24416: Change pickling behavior of isocalendar() objects --- Lib/datetime.py | 9 +++++++-- Lib/test/datetimetester.py | 3 +-- Modules/_datetimemodule.c | 25 +++++++++++++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index dc55013d1cf860..c5bf4c74609144 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1120,7 +1120,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return IsoCalendarDate((year, week+1, day+1)) + return _IsoCalendarDate((year, week+1, day+1)) # Pickle support. @@ -1231,11 +1231,16 @@ def week(self): def weekday(self): return self[2] + def __reduce__(self): + return (tuple, (tuple(self),)) + def __repr__(self): return (f'{self.__class__.__name__}' f'(year={self[0]}, week={self[1]}, weekday={self[2]})') +_IsoCalendarDate = IsoCalendarDate +del IsoCalendarDate _tzinfo_class = tzinfo class time: @@ -2541,7 +2546,7 @@ def _name_from_offset(delta): _format_time, _format_offset, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff) + _parse_hh_mm_ss_ff, _IsoCalendarDate) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 990ff504bade72..959fc88b183433 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -77,8 +77,7 @@ def test_name_cleanup(self): names = set(name for name in dir(datetime) if not name.startswith('__') and not name.endswith('__')) allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime', - 'datetime_CAPI', 'IsoCalendarDate', - 'time', 'timedelta', 'timezone', + 'datetime_CAPI', 'time', 'timedelta', 'timezone', 'tzinfo', 'sys']) self.assertEqual(names - allowed, set([])) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 64c2c8b7b11821..e9d6a5084fe83c 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3257,6 +3257,20 @@ iso_calendar_date_repr(PyDateTime_IsoCalendarDate *self) Py_TYPE(self)->tp_name, year, week, weekday); } +static PyObject * +isocalendardate_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + // Construct the tuple that this reduces to + PyObject * reduce_tuple = Py_BuildValue( + "O((OOO))", &PyTuple_Type, + PyTuple_GET_ITEM(self, 0), + PyTuple_GET_ITEM(self, 1), + PyTuple_GET_ITEM(self, 2) + ); + + return reduce_tuple; +} + static PyObject * iso_calendar_date_year(PyDateTime_IsoCalendarDate *self, void *unused) { @@ -3297,6 +3311,12 @@ static PyGetSetDef iso_calendar_date_getset[] = { {NULL} }; +static PyMethodDef iso_calendar_date_methods[] = { + {"__reduce__", (PyCFunction)isocalendardate_reduce, METH_NOARGS, + PyDoc_STR("__reduce__() -> (cls, state)")}, + {NULL, NULL}, +}; + static PyTypeObject PyDateTime_IsoCalendarDateType = { PyVarObject_HEAD_INIT(NULL, 0) "datetime.IsoCalendarDate", /* tp_name */ @@ -3325,7 +3345,7 @@ static PyTypeObject PyDateTime_IsoCalendarDateType = { 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ - 0, /* tp_methods */ + iso_calendar_date_methods, /* tp_methods */ 0, /* tp_members */ iso_calendar_date_getset, /* tp_getset */ &PyTuple_Type, /* tp_base */ @@ -6687,9 +6707,6 @@ PyInit__datetime(void) PyModule_AddObject(m, "datetime", (PyObject *)&PyDateTime_DateTimeType); - Py_INCREF((PyObject *) &PyDateTime_IsoCalendarDateType); - PyModule_AddObject(m, "IsoCalendarDate", (PyObject *) &PyDateTime_IsoCalendarDateType); - Py_INCREF(&PyDateTime_TimeType); PyModule_AddObject(m, "time", (PyObject *) &PyDateTime_TimeType); From 17104d5d94aa099a3cf3e20cb6be3146db2e293a Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Mon, 23 Sep 2019 02:42:10 +0900 Subject: [PATCH 18/20] bpo-24416: Update __new__ --- Lib/datetime.py | 9 ++--- Modules/_datetimemodule.c | 54 +++++++++++------------------- Modules/clinic/_datetimemodule.c.h | 45 ++++++++++++++++++++----- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index c5bf4c74609144..c4feec1ab83fe7 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1120,7 +1120,7 @@ def isocalendar(self): if today >= _isoweek1monday(year+1): year += 1 week = 0 - return _IsoCalendarDate((year, week+1, day+1)) + return _IsoCalendarDate(year, week+1, day+1) # Pickle support. @@ -1213,11 +1213,8 @@ def __reduce__(self): class IsoCalendarDate(tuple): - def __new__(cls, seq): - if len(seq) != 3: - raise TypeError(f'{cls.__name__}() takes a 3-sequence ' - f'({len(seq)}-sequence given)') - return super().__new__(cls, seq) + def __new__(cls, year, week, weekday): + return super().__new__(cls, (year, week, weekday)) @property def year(self): diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index e9d6a5084fe83c..0ad61b3df1120e 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3274,8 +3274,8 @@ isocalendardate_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) static PyObject * iso_calendar_date_year(PyDateTime_IsoCalendarDate *self, void *unused) { - PyObject* year = PyTuple_GetItem((PyObject *)self, 0); - if(year == NULL) { + PyObject *year = PyTuple_GetItem((PyObject *)self, 0); + if (year == NULL) { return NULL; } Py_INCREF(year); @@ -3285,7 +3285,7 @@ iso_calendar_date_year(PyDateTime_IsoCalendarDate *self, void *unused) static PyObject * iso_calendar_date_week(PyDateTime_IsoCalendarDate *self, void *unused) { - PyObject* week = PyTuple_GetItem((PyObject *)self, 1); + PyObject *week = PyTuple_GetItem((PyObject *)self, 1); if (week == NULL) { return NULL; } @@ -3296,8 +3296,8 @@ iso_calendar_date_week(PyDateTime_IsoCalendarDate *self, void *unused) static PyObject * iso_calendar_date_weekday(PyDateTime_IsoCalendarDate *self, void *unused) { - PyObject* weekday = PyTuple_GetItem((PyObject *)self, 2); - if(weekday == NULL) { + PyObject *weekday = PyTuple_GetItem((PyObject *)self, 2); + if (weekday == NULL) { return NULL; } Py_INCREF(weekday); @@ -3361,36 +3361,26 @@ static PyTypeObject PyDateTime_IsoCalendarDateType = { /*[clinic input] @classmethod datetime.IsoCalendarDate.__new__ as iso_calendar_date_new - sequence as seq: object + year: int + week: int + weekday: int [clinic start generated code]*/ static PyObject * -iso_calendar_date_new_impl(PyTypeObject *type, PyObject *seq) -/*[clinic end generated code: output=bf2e1982e7012dae input=f9bb6626d4947edc]*/ -{ - PyDateTime_IsoCalendarDate* self; - Py_ssize_t len = PyObject_Length(seq); - if (len == -1) { - return NULL; - } - - if (len != 3) { - PyErr_Format(PyExc_TypeError, - "%.500s() takes a 3-sequence (%zd-sequence given)", - type->tp_name, len); - return NULL; - } +iso_calendar_date_new_impl(PyTypeObject *type, int year, int week, + int weekday) +/*[clinic end generated code: output=383d33d8dc7183a2 input=4f2c663c9d19c4ee]*/ - self = (PyDateTime_IsoCalendarDate *) type->tp_alloc(type, len); +{ + PyDateTime_IsoCalendarDate *self; + self = (PyDateTime_IsoCalendarDate *) type->tp_alloc(type, 3); if (self == NULL) { return NULL; } - for (int i = 0; i < len; ++i) { - PyObject* v = PyTuple_GetItem(seq, i); - Py_INCREF(v); - PyTuple_SET_ITEM(self, i, v); - } + PyTuple_SET_ITEM(self, 0, PyLong_FromLong(year)); + PyTuple_SET_ITEM(self, 1, PyLong_FromLong(week)); + PyTuple_SET_ITEM(self, 2, PyLong_FromLong(weekday)); return (PyObject *)self; } @@ -3415,17 +3405,11 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) week = 0; } - PyObject *seq = Py_BuildValue("iii", year, week + 1, day + 1); - if (seq == NULL) { - return NULL; - } - - PyObject* v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType, seq); + PyObject* v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType, + year, week + 1, day + 1); if(v == NULL) { - Py_DECREF(seq); return NULL; } - Py_DECREF(seq); return v; } diff --git a/Modules/clinic/_datetimemodule.c.h b/Modules/clinic/_datetimemodule.c.h index 6f121ad72d5b88..973a4ea025347f 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -15,25 +15,54 @@ PyDoc_STRVAR(datetime_date_fromtimestamp__doc__, {"fromtimestamp", (PyCFunction)datetime_date_fromtimestamp, METH_O|METH_CLASS, datetime_date_fromtimestamp__doc__}, static PyObject * -iso_calendar_date_new_impl(PyTypeObject *type, PyObject *seq); +iso_calendar_date_new_impl(PyTypeObject *type, int year, int week, + int weekday); static PyObject * iso_calendar_date_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { PyObject *return_value = NULL; - static const char * const _keywords[] = {"sequence", NULL}; + static const char * const _keywords[] = {"year", "week", "weekday", NULL}; static _PyArg_Parser _parser = {NULL, _keywords, "IsoCalendarDate", 0}; - PyObject *argsbuf[1]; + PyObject *argsbuf[3]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); - PyObject *seq; + int year; + int week; + int weekday; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 1, 0, argsbuf); + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 3, 3, 0, argsbuf); if (!fastargs) { goto exit; } - seq = fastargs[0]; - return_value = iso_calendar_date_new_impl(type, seq); + if (PyFloat_Check(fastargs[0])) { + PyErr_SetString(PyExc_TypeError, + "integer argument expected, got float" ); + goto exit; + } + year = _PyLong_AsInt(fastargs[0]); + if (year == -1 && PyErr_Occurred()) { + goto exit; + } + if (PyFloat_Check(fastargs[1])) { + PyErr_SetString(PyExc_TypeError, + "integer argument expected, got float" ); + goto exit; + } + week = _PyLong_AsInt(fastargs[1]); + if (week == -1 && PyErr_Occurred()) { + goto exit; + } + if (PyFloat_Check(fastargs[2])) { + PyErr_SetString(PyExc_TypeError, + "integer argument expected, got float" ); + goto exit; + } + weekday = _PyLong_AsInt(fastargs[2]); + if (weekday == -1 && PyErr_Occurred()) { + goto exit; + } + return_value = iso_calendar_date_new_impl(type, year, week, weekday); exit: return return_value; @@ -80,4 +109,4 @@ datetime_datetime_now(PyTypeObject *type, PyObject *const *args, Py_ssize_t narg exit: return return_value; } -/*[clinic end generated code: output=48b1aa60aca63a57 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5e17549f29a439a5 input=a9049054013a1b77]*/ From f04b6bcaf1bcef5d0cac8a919f171bbe34a3588e Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Mon, 23 Sep 2019 03:00:54 +0900 Subject: [PATCH 19/20] bpo-24416: Update whatsnew --- Doc/whatsnew/3.9.rst | 1 - Modules/_datetimemodule.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 706e171f53b979..2f70ef69e52fab 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -150,7 +150,6 @@ datetime The :meth:`~datetime.date.isocalendar()` of :class:`datetime.date` and :meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime` methods now returns a :func:`~collections.namedtuple` instead of a :class:`tuple`. -It can't be unpickled in previous versions. (Contributed by Dong-hee Na in :issue:`24416`.) fcntl diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 0ad61b3df1120e..a617fe278aff9c 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3407,7 +3407,7 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) PyObject* v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType, year, week + 1, day + 1); - if(v == NULL) { + if (v == NULL) { return NULL; } return v; From 0b04d4584cc374b381d869b14372abd5ddc74d22 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Mon, 16 Dec 2019 17:15:29 +0900 Subject: [PATCH 20/20] bpo-24416: Apply vstinner's review --- Lib/datetime.py | 4 +++- Lib/test/datetimetester.py | 8 +++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index c4feec1ab83fe7..b39cc0d9703ad8 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1229,11 +1229,13 @@ def weekday(self): return self[2] def __reduce__(self): + # This code is intended to pickle the object without making the + # class public. See https://bugs.python.org/msg352381 return (tuple, (tuple(self),)) def __repr__(self): return (f'{self.__class__.__name__}' - f'(year={self[0]}, week={self[1]}, weekday={self[2]})') + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') _IsoCalendarDate = IsoCalendarDate diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 959fc88b183433..a9741d6d4062f4 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1389,11 +1389,9 @@ def test_isocalendar_pickling(self): The result of a round trip should be a plain tuple. """ d = self.theclass(2019, 1, 1) - f = io.BytesIO() - pickle.dump(d.isocalendar(), f) - f.seek(0) - res = pickle.load(f) - + p = pickle.dumps(d.isocalendar()) + res = pickle.loads(p) + self.assertEqual(type(res), tuple) self.assertEqual(res, (2019, 1, 2)) def test_iso_long_years(self):