summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/datetime.rst20
-rw-r--r--Doc/whatsnew/3.9.rst7
-rw-r--r--Lib/datetime.py37
-rw-r--r--Lib/test/datetimetester.py51
-rw-r--r--Misc/NEWS.d/next/Library/2019-09-01-15-17-49.bpo-24416.G8Ww1U.rst3
-rw-r--r--Modules/_datetimemodule.c152
-rw-r--r--Modules/clinic/_datetimemodule.c.h56
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]*/