From 99db36df82a3a6507cbed8def92dc39a1903e488 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Sun, 1 Sep 2019 14:41:29 +0900 Subject: [PATCH] bpo-24416: Return a namedtuple from date.isocalendar() --- Doc/library/datetime.rst | 20 +++-- Doc/whatsnew/3.9.rst | 7 ++ Include/structseq.h | 1 + Lib/datetime.py | 37 ++++++++- Lib/test/datetimetester.py | 51 +++++++++---- .../2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 3 + Modules/_datetimemodule.c | 64 +++++++++++++++- Objects/structseq.c | 76 +++++++++++++++++++ 8 files changed, 231 insertions(+), 28 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..ea0d3c0d2d9592 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -670,7 +670,8 @@ Instance methods: .. method:: date.isocalendar() - Return a 3-tuple, (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. [#]_ @@ -682,11 +683,14 @@ 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`. .. method:: date.isoformat() @@ -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 :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/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 7cf49bfbb93f94..2f70ef69e52fab 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -145,6 +145,13 @@ 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`. +(Contributed by Dong-hee Na in :issue:`24416`.) + fcntl ----- diff --git a/Include/structseq.h b/Include/structseq.h index 8f51c89163a4e1..b81761c19f9a8d 100644 --- a/Include/structseq.h +++ b/Include/structseq.h @@ -26,6 +26,7 @@ PyAPI_FUNC(void) PyStructSequence_InitType(PyTypeObject *type, PyStructSequence_Desc *desc); PyAPI_FUNC(int) PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc); +PyAPI_FUNC(int) IsoCalendarDateType_InitType(PyTypeObject *type, PyStructSequence_Desc *desc); #endif PyAPI_FUNC(PyTypeObject*) PyStructSequence_NewType(PyStructSequence_Desc *desc); diff --git a/Lib/datetime.py b/Lib/datetime.py index 67555191d02c18..b39cc0d9703ad8 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1095,7 +1095,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 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 +1120,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. @@ -1210,6 +1210,36 @@ def __reduce__(self): else: return (self.__class__, args, state) + +class IsoCalendarDate(tuple): + + def __new__(cls, year, week, weekday): + return super().__new__(cls, (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 __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]})') + + +_IsoCalendarDate = IsoCalendarDate +del IsoCalendarDate _tzinfo_class = tzinfo class time: @@ -1559,6 +1589,7 @@ def __reduce__(self): time.max = time(23, 59, 59, 999999) time.resolution = timedelta(microseconds=1) + class datetime(date): """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) @@ -2514,7 +2545,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 42e2cecaeb724e..a9741d6d4062f4 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 @@ -1355,19 +1356,43 @@ 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)) + 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_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) + 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): # 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 new file mode 100644 index 00000000000000..ee9af990f079db --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst @@ -0,0 +1,3 @@ +The ``isocalendar()`` methods of :class:`datetime.date` and +:class:`datetime.datetime` now return a :term:`named tuple` +instead of a :class:`tuple`. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c1b24073436e63..b66f95dee2d9d5 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3225,6 +3225,45 @@ 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__, +"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 +}; + +static int isocalendardate_initialized; +static PyTypeObject StructIsoCalendarDateType; + +/* Method to add our custom reduce type to the IsoCalendarDate type */ +static int +_initialize_isocalendardate() { + if (isocalendardate_initialized) { + return 0; + } + + if (IsoCalendarDateType_InitType(&StructIsoCalendarDateType, + &struct_iso_calendar_date_desc) < 0) { + return -1; + } + + isocalendardate_initialized = 1; + return 0; +} + static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) { @@ -3244,10 +3283,22 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) ++year; week = 0; } - return Py_BuildValue("iii", year, week + 1, day + 1); -} -/* Miscellaneous methods. */ + 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_DECREF(v); + return NULL; + } + return v; +} static PyObject * date_richcompare(PyObject *self, PyObject *other, int op) @@ -3383,7 +3434,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 named tuple containing ISO year, week number, and " "weekday.")}, {"isoformat", (PyCFunction)date_isoformat, METH_NOARGS, @@ -6516,6 +6567,11 @@ PyInit__datetime(void) PyModule_AddIntMacro(m, MINYEAR); PyModule_AddIntMacro(m, MAXYEAR); + /* IsoCalendarDate */ + if (_initialize_isocalendardate()) { + return NULL; + } + Py_INCREF(&PyDateTime_DateType); PyModule_AddObject(m, "date", (PyObject *) &PyDateTime_DateType); diff --git a/Objects/structseq.c b/Objects/structseq.c index c86fbe50b972c6..5f09aca4ccff87 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -442,6 +442,82 @@ PyStructSequence_InitType(PyTypeObject *type, PyStructSequence_Desc *desc) (void)PyStructSequence_InitType2(type, desc); } +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 PyMethodDef iso_calendar_date_methods[] = { + {"__reduce__", (PyCFunction)isocalendardate_reduce, METH_NOARGS, + PyDoc_STR("__reduce__() -> (cls, state)")}, + {NULL, NULL}, +}; + +int +IsoCalendarDateType_InitType(PyTypeObject *type, PyStructSequence_Desc *desc) +{ + PyMemberDef *members; + Py_ssize_t n_members, n_unnamed_members; + +#ifdef Py_TRACE_REFS + /* if the type object was chained, unchain it first + before overwriting its storage */ + if (type->ob_base.ob_base._ob_next) { + _Py_ForgetReference((PyObject *)type); + } +#endif + + /* PyTypeObject has already been initialized */ + if (Py_REFCNT(type) != 0) { + PyErr_BadInternalCall(); + return -1; + } + + type->tp_name = desc->name; + type->tp_basicsize = sizeof(PyStructSequence) - sizeof(PyObject *); + type->tp_itemsize = sizeof(PyObject *); + type->tp_dealloc = (destructor)structseq_dealloc; + type->tp_repr = (reprfunc)structseq_repr; + type->tp_doc = desc->doc; + type->tp_base = &PyTuple_Type; + type->tp_methods = iso_calendar_date_methods; + type->tp_new = structseq_new; + type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC; + type->tp_traverse = (traverseproc) structseq_traverse; + + n_members = count_members(desc, &n_unnamed_members); + members = PyMem_NEW(PyMemberDef, n_members - n_unnamed_members + 1); + if (members == NULL) { + PyErr_NoMemory(); + return -1; + } + initialize_members(desc, members, n_members); + type->tp_members = members; + + if (PyType_Ready(type) < 0) { + PyMem_FREE(members); + return -1; + } + Py_INCREF(type); + + if (initialize_structseq_dict( + desc, type->tp_dict, n_members, n_unnamed_members) < 0) { + PyMem_FREE(members); + Py_DECREF(type); + return -1; + } + + return 0; +} + PyTypeObject * PyStructSequence_NewType(PyStructSequence_Desc *desc) {