diff options
-rw-r--r-- | Doc/library/datetime.rst | 20 | ||||
-rw-r--r-- | Doc/whatsnew/3.9.rst | 7 | ||||
-rw-r--r-- | Lib/datetime.py | 37 | ||||
-rw-r--r-- | Lib/test/datetimetester.py | 51 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst | 3 | ||||
-rw-r--r-- | Modules/_datetimemodule.c | 152 | ||||
-rw-r--r-- | Modules/clinic/_datetimemodule.c.h | 56 |
7 files changed, 297 insertions, 29 deletions
diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 22ecbb5..4daf5df 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() @@ -1397,8 +1401,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 cbddbb4..bddb710 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -281,6 +281,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/Lib/datetime.py b/Lib/datetime.py index 6755519..952aebf 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1095,7 +1095,7 @@ class date: 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 @@ class date: 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 @@ class tzinfo: 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 @@ time.min = time(0, 0, 0) 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 @@ else: _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 42e2cec..a9741d6 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 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): 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 0000000..ee9af99 --- /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 9bdc52e..7a5efd2 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; @@ -3224,6 +3226,136 @@ date_isoweekday(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) return PyLong_FromLong(dow + 1); } +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."); + +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_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) +{ + 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 PyMethodDef iso_calendar_date_methods[] = { + {"__reduce__", (PyCFunction)iso_calendar_date_reduce, METH_NOARGS, + PyDoc_STR("__reduce__() -> (cls, state)")}, + {NULL, NULL}, +}; + +static PyTypeObject PyDateTime_IsoCalendarDateType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "datetime.IsoCalendarDate", + .tp_basicsize = sizeof(PyDateTime_IsoCalendarDate), + .tp_repr = (reprfunc) iso_calendar_date_repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = iso_calendar_date__doc__, + .tp_methods = iso_calendar_date_methods, + .tp_getset = iso_calendar_date_getset, + .tp_base = &PyTuple_Type, + .tp_new = iso_calendar_date_new, +}; + +/*[clinic input] +@classmethod +datetime.IsoCalendarDate.__new__ as iso_calendar_date_new + year: int + week: int + weekday: int +[clinic start generated code]*/ + +static PyObject * +iso_calendar_date_new_impl(PyTypeObject *type, int year, int week, + int weekday) +/*[clinic end generated code: output=383d33d8dc7183a2 input=4f2c663c9d19c4ee]*/ + +{ + PyDateTime_IsoCalendarDate *self; + self = (PyDateTime_IsoCalendarDate *) type->tp_alloc(type, 3); + if (self == NULL) { + return NULL; + } + + 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; +} + static PyObject * date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) { @@ -3243,7 +3375,13 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored)) ++year; week = 0; } - return Py_BuildValue("iii", year, week + 1, day + 1); + + PyObject* v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType, + year, week + 1, day + 1); + if (v == NULL) { + return NULL; + } + return v; } /* Miscellaneous methods. */ @@ -3382,7 +3520,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, @@ -6386,13 +6524,14 @@ PyInit__datetime(void) if (m == NULL) return NULL; + PyTypeObject *types[] = { &PyDateTime_DateType, &PyDateTime_DateTimeType, &PyDateTime_TimeType, &PyDateTime_DeltaType, &PyDateTime_TZInfoType, - &PyDateTime_TimeZoneType + &PyDateTime_TimeZoneType, }; for (size_t i = 0; i < Py_ARRAY_LENGTH(types); i++) { @@ -6401,6 +6540,11 @@ PyInit__datetime(void) } } + if (PyType_Ready(&PyDateTime_IsoCalendarDateType) < 0) { + return NULL; + } + Py_INCREF(&PyDateTime_IsoCalendarDateType); + /* timedelta values */ d = PyDateTime_DeltaType.tp_dict; diff --git a/Modules/clinic/_datetimemodule.c.h b/Modules/clinic/_datetimemodule.c.h index 447036c..973a4ea 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -14,6 +14,60 @@ 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, 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[] = {"year", "week", "weekday", NULL}; + static _PyArg_Parser _parser = {NULL, _keywords, "IsoCalendarDate", 0}; + PyObject *argsbuf[3]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + int year; + int week; + int weekday; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 3, 3, 0, argsbuf); + if (!fastargs) { + goto exit; + } + 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; +} + PyDoc_STRVAR(datetime_datetime_now__doc__, "now($type, /, tz=None)\n" "--\n" @@ -55,4 +109,4 @@ skip_optional_pos: exit: return return_value; } -/*[clinic end generated code: output=aae916ab728ca85b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5e17549f29a439a5 input=a9049054013a1b77]*/ |