From 654af5a90399a127667e1b322f8b05c1c4b9e70e Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 19 Jun 2024 20:42:56 +0200 Subject: [PATCH 01/17] Python implementation --- Lib/_pydatetime.py | 16 +++++++++++++++- Lib/_strptime.py | 27 +++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 34ccb2da13d0f3..11880008099d3d 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -908,6 +908,7 @@ class date: fromtimestamp() today() fromordinal() + strptime() Operators: @@ -1008,6 +1009,12 @@ def fromisocalendar(cls, year, week, day): This is the inverse of the date.isocalendar() function""" return cls(*_isoweek_to_gregorian(year, week, day)) + @classmethod + def strptime(cls, date_string, format): + 'string, format -> new date parsed from a string (like time.strptime()).' + import _strptime + return _strptime._strptime_datetime_date(cls, date_string, format) + # Conversions to string def __repr__(self): @@ -1328,6 +1335,7 @@ class time: Constructors: __new__() + strptime() Operators: @@ -1386,6 +1394,12 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold self._fold = fold return self + @classmethod + def strptime(cls, date_string, format): + 'string, format -> new time parsed from a string (like time.strptime()).' + import _strptime + return _strptime._strptime_datetime_time(cls, date_string, format) + # Read-only field accessors @property def hour(self): @@ -2092,7 +2106,7 @@ def __str__(self): def strptime(cls, date_string, format): 'string, format -> new datetime parsed from a string (like time.strptime()).' import _strptime - return _strptime._strptime_datetime(cls, date_string, format) + return _strptime._strptime_datetime_datetime(cls, date_string, format) def utcoffset(self): """Return the timezone offset as timedelta positive east of UTC (negative west of diff --git a/Lib/_strptime.py b/Lib/_strptime.py index e42af75af74bf5..8006655174c158 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -567,8 +567,31 @@ def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): tt = _strptime(data_string, format)[0] return time.struct_time(tt[:time._STRUCT_TM_ITEMS]) -def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): - """Return a class cls instance based on the input string and the +def _strptime_datetime_date(cls, data_string, format="%a %b %d %Y"): + """Return a date instance based on the input string and the + format string.""" + tt, _, _ = _strptime(data_string, format) + args = tt[:3] + return cls(*args) + +def _strptime_datetime_time(cls, data_string, format="%H:%M:%S"): + """Return a time instance based on the input string and the + format string.""" + tt, fraction, gmtoff_fraction = _strptime(data_string, format) + tzname, gmtoff = tt[-2:] + args = tt[3:6] + (fraction,) + if gmtoff is not None: + tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction) + if tzname: + tz = datetime_timezone(tzdelta, tzname) + else: + tz = datetime_timezone(tzdelta) + args += (tz,) + + return cls(*args) + +def _strptime_datetime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): + """Return a datetime instance based on the input string and the format string.""" tt, fraction, gmtoff_fraction = _strptime(data_string, format) tzname, gmtoff = tt[-2:] From 8ba282f5375ddee5010277e70d68a4800fda7042 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 19 Jun 2024 21:03:16 +0200 Subject: [PATCH 02/17] C implementation --- .../pycore_global_objects_fini_generated.h | 4 +- Include/internal/pycore_global_strings.h | 4 +- .../internal/pycore_runtime_init_generated.h | 4 +- .../internal/pycore_unicodeobject_generated.h | 8 ++- Lib/test/datetimetester.py | 6 +- Modules/_datetimemodule.c | 59 ++++++++++++++++++- 6 files changed, 76 insertions(+), 9 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index bc94930b85f098..2d6eaaf5f323ba 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -778,7 +778,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_shutdown)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_slotnames)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_strptime)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_strptime_datetime)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_strptime_datetime_date)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_strptime_datetime_datetime)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_strptime_datetime_time)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_swappedbytes_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_type_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_uninitialized_submodules)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 998be2ec490dd9..1e10d37fda2262 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -267,7 +267,9 @@ struct _Py_global_strings { STRUCT_FOR_ID(_shutdown) STRUCT_FOR_ID(_slotnames) STRUCT_FOR_ID(_strptime) - STRUCT_FOR_ID(_strptime_datetime) + STRUCT_FOR_ID(_strptime_datetime_date) + STRUCT_FOR_ID(_strptime_datetime_datetime) + STRUCT_FOR_ID(_strptime_datetime_time) STRUCT_FOR_ID(_swappedbytes_) STRUCT_FOR_ID(_type_) STRUCT_FOR_ID(_uninitialized_submodules) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index bd79a7dff42f89..9d9452b072a033 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -776,7 +776,9 @@ extern "C" { INIT_ID(_shutdown), \ INIT_ID(_slotnames), \ INIT_ID(_strptime), \ - INIT_ID(_strptime_datetime), \ + INIT_ID(_strptime_datetime_date), \ + INIT_ID(_strptime_datetime_datetime), \ + INIT_ID(_strptime_datetime_time), \ INIT_ID(_swappedbytes_), \ INIT_ID(_type_), \ INIT_ID(_uninitialized_submodules), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 7284aeb592d7ec..3654678eb4f0c4 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -639,7 +639,13 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(_strptime); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); - string = &_Py_ID(_strptime_datetime); + string = &_Py_ID(_strptime_datetime_date); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(_strptime_datetime_datetime); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(_strptime_datetime_time); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(_swappedbytes_); diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e55b738eb4a975..146219285f4aff 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2718,7 +2718,7 @@ def test_utcnow(self): def test_strptime(self): string = '2004-12-01 13:02:47.197' format = '%Y-%m-%d %H:%M:%S.%f' - expected = _strptime._strptime_datetime(self.theclass, string, format) + expected = _strptime._strptime_datetime_datetime(self.theclass, string, format) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) self.assertIs(type(expected), self.theclass) @@ -2732,8 +2732,8 @@ def test_strptime(self): ] for string, format in inputs: with self.subTest(string=string, format=format): - expected = _strptime._strptime_datetime(self.theclass, string, - format) + expected = _strptime._strptime_datetime_datetime(self.theclass, string, + format) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 31bf641152d803..91f56cefa7141c 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3381,6 +3381,26 @@ date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw) return new_date_subclass_ex(year, month, day, cls); } +/* Return new date from _strptime.strptime_datetime_date(). */ +static PyObject * +date_strptime(PyObject *cls, PyObject *args) +{ + PyObject *string, *format, *result; + + if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) + return NULL; + + PyObject *module = PyImport_Import(&_Py_ID(_strptime)); + if (module == NULL) { + return NULL; + } + result = PyObject_CallMethodObjArgs(module, + &_Py_ID(_strptime_datetime_date), cls, + string, format, NULL); + Py_DECREF(module); + return result; +} + /* * Date arithmetic. @@ -3846,6 +3866,11 @@ static PyMethodDef date_methods[] = { "number and weekday.\n\n" "This is the inverse of the date.isocalendar() function")}, + {"strptime", (PyCFunction)date_strptime, + METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new date parsed from a string " + "(like time.strptime()).")}, + {"today", (PyCFunction)date_today, METH_NOARGS | METH_CLASS, PyDoc_STR("Current date or datetime: same as " "self.__class__.fromtimestamp(time.time()).")}, @@ -4580,6 +4605,26 @@ time_new(PyTypeObject *type, PyObject *args, PyObject *kw) return self; } +/* Return new time from _strptime.strptime_datetime_time(). */ +static PyObject * +time_strptime(PyObject *cls, PyObject *args) +{ + PyObject *string, *format, *result; + + if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) + return NULL; + + PyObject *module = PyImport_Import(&_Py_ID(_strptime)); + if (module == NULL) { + return NULL; + } + result = PyObject_CallMethodObjArgs(module, + &_Py_ID(_strptime_datetime_time), cls, + string, format, NULL); + Py_DECREF(module); + return result; +} + /* * Destructor. */ @@ -5003,6 +5048,15 @@ time_reduce(PyDateTime_Time *self, PyObject *arg) static PyMethodDef time_methods[] = { + /* Class method: */ + + {"strptime", (PyCFunction)time_strptime, + METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new time parsed from a string " + "(like time.strptime()).")}, + + /* Instance methods: */ + {"isoformat", _PyCFunction_CAST(time_isoformat), METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]" "[+HH:MM].\n\n" @@ -5510,7 +5564,7 @@ datetime_utcfromtimestamp(PyObject *cls, PyObject *args) return result; } -/* Return new datetime from _strptime.strptime_datetime(). */ +/* Return new datetime from _strptime.strptime_datetime_datetime(). */ static PyObject * datetime_strptime(PyObject *cls, PyObject *args) { @@ -5523,7 +5577,8 @@ datetime_strptime(PyObject *cls, PyObject *args) if (module == NULL) { return NULL; } - result = PyObject_CallMethodObjArgs(module, &_Py_ID(_strptime_datetime), + result = PyObject_CallMethodObjArgs(module, + &_Py_ID(_strptime_datetime_datetime), cls, string, format, NULL); Py_DECREF(module); return result; From 24ca6e4935cc3fe4f9d32ce7bb1f70edb777e18d Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 19 Jun 2024 21:27:30 +0200 Subject: [PATCH 03/17] Test `date.strptime` --- Lib/test/datetimetester.py | 67 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 146219285f4aff..5a46916c0c8c79 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1109,6 +1109,66 @@ def test_delta_non_days_ignored(self): dt2 = dt - delta self.assertEqual(dt2, dt - days) + def test_strptime(self): + string = '2004-12-01' + format = '%Y-%m-%d' + expected = _strptime._strptime_datetime_date(date, string, format) + got = date.strptime(string, format) + self.assertEqual(expected, got) + self.assertIs(type(expected), date) + self.assertIs(type(got), date) + + # bpo-34482: Check that surrogates are handled properly. + inputs = [ + ('2004-12\ud80001', '%Y-%m\ud800%d'), + ('2004\ud80012-01', '%Y\ud800%m-%d'), + ] + for string, format in inputs: + with self.subTest(string=string, format=format): + expected = _strptime._strptime_datetime_date(date, string, + format) + got = date.strptime(string, format) + self.assertEqual(expected, got) + + def test_strptime_single_digit(self): + # bpo-34903: Check that single digit dates are allowed. + strptime = date.strptime + with self.assertRaises(ValueError): + # %y does require two digits. + newdate = strptime('01/02/3', '%d/%m/%y') + + d1 = date(2003, 2, 1) + d2 = date(2003, 1, 2) + d3 = date(2003, 2, 1) + d4 = date(2003, 1, 25) + inputs = [ + ('%d', '1/02/03', '%d/%m/%y', d1), + ('%m', '01/2/03', '%d/%m/%y', d1), + ('%j', '2/03', '%j/%y', d2), + ('%w', '6/04/03', '%w/%U/%y', d3), + # %u requires a single digit. + ('%W', '6/4/2003', '%u/%W/%Y', d3), + ('%V', '6/4/2003', '%u/%V/%G', d4), + ] + for reason, string, format, target in inputs: + reason = 'test single digit ' + reason + with self.subTest(reason=reason, + string=string, + format=format, + target=target): + newdate = strptime(string, format) + self.assertEqual(newdate, target, msg=reason) + + @warnings_helper.ignore_warnings(category=DeprecationWarning) + def test_strptime_leap_year(self): + # GH-70647: warns if parsing a format with a day and no year. + with self.assertRaises(ValueError): + # The existing behavior that GH-70647 seeks to change. + date.strptime('02-29', '%m-%d') + with self._assertNotWarns(DeprecationWarning): + date.strptime('20-03-14', '%y-%m-%d') + date.strptime('02-29,2024', '%m-%d,%Y') + class SubclassDate(date): sub_var = 1 @@ -2718,7 +2778,8 @@ def test_utcnow(self): def test_strptime(self): string = '2004-12-01 13:02:47.197' format = '%Y-%m-%d %H:%M:%S.%f' - expected = _strptime._strptime_datetime_datetime(self.theclass, string, format) + expected = _strptime._strptime_datetime_datetime(self.theclass, string, + format) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) self.assertIs(type(expected), self.theclass) @@ -2732,8 +2793,8 @@ def test_strptime(self): ] for string, format in inputs: with self.subTest(string=string, format=format): - expected = _strptime._strptime_datetime_datetime(self.theclass, string, - format) + expected = _strptime._strptime_datetime_datetime(self.theclass, + string, format) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) From 21a103284613c530205c29ae738c7098c2bfcd3e Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 19 Jun 2024 21:47:05 +0200 Subject: [PATCH 04/17] Test `time.strptime` --- Lib/test/datetimetester.py | 86 +++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5a46916c0c8c79..4b9b246ca8a208 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1139,16 +1139,15 @@ def test_strptime_single_digit(self): d1 = date(2003, 2, 1) d2 = date(2003, 1, 2) - d3 = date(2003, 2, 1) - d4 = date(2003, 1, 25) + d3 = date(2003, 1, 25) inputs = [ ('%d', '1/02/03', '%d/%m/%y', d1), ('%m', '01/2/03', '%d/%m/%y', d1), ('%j', '2/03', '%j/%y', d2), - ('%w', '6/04/03', '%w/%U/%y', d3), + ('%w', '6/04/03', '%w/%U/%y', d1), # %u requires a single digit. - ('%W', '6/4/2003', '%u/%W/%Y', d3), - ('%V', '6/4/2003', '%u/%V/%G', d4), + ('%W', '6/4/2003', '%u/%W/%Y', d1), + ('%V', '6/4/2003', '%u/%V/%G', d3), ] for reason, string, format, target in inputs: reason = 'test single digit ' + reason @@ -3787,6 +3786,83 @@ def test_compat_unpickle(self): derived = loads(data, encoding='latin1') self.assertEqual(derived, expected) + def test_strptime(self): + string = '13:02:47.197' + format = '%H:%M:%S.%f' + expected = _strptime._strptime_datetime_time(self.theclass, string, + format) + got = self.theclass.strptime(string, format) + self.assertEqual(expected, got) + self.assertIs(type(expected), self.theclass) + self.assertIs(type(got), self.theclass) + + # bpo-34482: Check that surrogates are handled properly. + inputs = [ + ('13:02\ud80047.197', '%H:%M\ud800%S.%f'), + ('13\ud80002:47.197', '%H\ud800%M:%S.%f'), + ] + for string, format in inputs: + with self.subTest(string=string, format=format): + expected = _strptime._strptime_datetime_time(self.theclass, + string, format) + got = self.theclass.strptime(string, format) + self.assertEqual(expected, got) + + strptime = self.theclass.strptime + self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) + self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) + self.assertEqual( + strptime("-00:02:01.000003", "%z").utcoffset(), + -timedelta(minutes=2, seconds=1, microseconds=3) + ) + # Only local timezone and UTC are supported + for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'), + (-_time.timezone, _time.tzname[0])): + if tzseconds < 0: + sign = '-' + seconds = -tzseconds + else: + sign ='+' + seconds = tzseconds + hours, minutes = divmod(seconds//60, 60) + tstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname) + t = strptime(tstr, "%z %Z") + self.assertEqual(t.utcoffset(), timedelta(seconds=tzseconds)) + self.assertEqual(t.tzname(), tzname) + + # Can produce inconsistent time + tstr, fmt = "+1234 UTC", "%z %Z" + t = strptime(tstr, fmt) + self.assertEqual(t.utcoffset(), 12 * HOUR + 34 * MINUTE) + self.assertEqual(t.tzname(), 'UTC') + # yet will roundtrip + self.assertEqual(t.strftime(fmt), tstr) + + # Produce naive time if no %z is provided + self.assertEqual(strptime("UTC", "%Z").tzinfo, None) + + with self.assertRaises(ValueError): strptime("-2400", "%z") + with self.assertRaises(ValueError): strptime("-000", "%z") + with self.assertRaises(ValueError): strptime("z", "%z") + + def test_strptime_single_digit(self): + # bpo-34903: Check that single digit times are allowed. + t = self.theclass(4, 5, 6) + inputs = [ + ('%H', '4:05:06', '%H:%M:%S', t), + ('%M', '04:5:06', '%H:%M:%S', t), + ('%S', '04:05:6', '%H:%M:%S', t), + ('%I', '4am:05:06', '%I%p:%M:%S', t), + ] + for reason, string, format, target in inputs: + reason = 'test single digit ' + reason + with self.subTest(reason=reason, + string=string, + format=format, + target=target): + newdate = self.theclass.strptime(string, format) + self.assertEqual(newdate, target, msg=reason) + def test_bool(self): # time is always True. cls = self.theclass From a06468ba36596c5fb9f332f028220917b1939b2e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:53:43 +0000 Subject: [PATCH 05/17] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst diff --git a/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst new file mode 100644 index 00000000000000..925defaab13287 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst @@ -0,0 +1,2 @@ +Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. + (Contributed by Wannes Boeykens in :gh:`120752`.) From 34d5b516bed686065db771c38637ad0af9f359d8 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 19 Jun 2024 21:59:22 +0200 Subject: [PATCH 06/17] Update whatsnew --- Doc/whatsnew/3.14.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 804d39ab64646d..e1753a3a02a909 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -92,6 +92,12 @@ ast Added :func:`ast.compare` for comparing two ASTs. (Contributed by Batuhan Taskaya and Jeremy Hylton in :issue:`15987`.) +datetime +-------- + +Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. +(Contributed by Wannes Boeykens in :gh:`120752`.) + os -- From 23b194faadcce4ca28dd8140c7f71e2173750fd9 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Wed, 19 Jun 2024 22:27:55 +0200 Subject: [PATCH 07/17] Update documentation --- Doc/library/datetime.rst | 58 +++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index b6d8e6e6df07fa..1631b6c0a67e5a 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -542,6 +542,20 @@ Other constructors, all class methods: .. versionadded:: 3.8 +.. classmethod:: date.strptime(date_string, format) + + Return a :class:`.date` corresponding to *date_string*, parsed according to + *format*. This is equivalent to:: + + date(*(time.strptime(date_string, format)[0:3])) + + :exc:`ValueError` is raised if the date_string and format + can't be parsed by :func:`time.strptime` or if it returns a value which isn't a + time tuple. See also :ref:`strftime-strptime-behavior` and + :meth:`date.fromisoformat`. + + .. versionadded:: 3.14 + Class attributes: @@ -1821,7 +1835,7 @@ In Boolean contexts, a :class:`.time` object is always considered to be true. details. -Other constructor: +Other constructors: .. classmethod:: time.fromisoformat(time_string) @@ -1863,6 +1877,22 @@ Other constructor: Previously, this method only supported formats that could be emitted by :meth:`time.isoformat()`. +.. classmethod:: time.strptime(date_string, format) + + Return a :class:`.time` corresponding to *date_string*, parsed according to + *format*. + + If *format* does not contain microseconds or timezone information, this is equivalent to:: + + time(*(time.strptime(date_string, format)[3:6])) + + :exc:`ValueError` is raised if the date_string and format + can't be parsed by :func:`time.strptime` or if it returns a value which isn't a + time tuple. See also :ref:`strftime-strptime-behavior` and + :meth:`time.fromisoformat`. + + .. versionadded:: 3.14 + Instance methods: @@ -2361,24 +2391,22 @@ Class attributes: ``strftime(format)`` method, to create a string representing the time under the control of an explicit format string. -Conversely, the :meth:`datetime.strptime` class method creates a -:class:`.datetime` object from a string representing a date and time and a -corresponding format string. +Conversely, the :meth:`date.strptime`, :meth:`datetime.strptime` and +:meth:`time.strptime`, class methods create an object from a string +representing the time and a corresponding format string. The table below provides a high-level comparison of :meth:`~.datetime.strftime` versus :meth:`~.datetime.strptime`: -+----------------+--------------------------------------------------------+------------------------------------------------------------------------------+ -| | ``strftime`` | ``strptime`` | -+================+========================================================+==============================================================================+ -| Usage | Convert object to a string according to a given format | Parse a string into a :class:`.datetime` object given a corresponding format | -+----------------+--------------------------------------------------------+------------------------------------------------------------------------------+ -| Type of method | Instance method | Class method | -+----------------+--------------------------------------------------------+------------------------------------------------------------------------------+ -| Method of | :class:`date`; :class:`.datetime`; :class:`.time` | :class:`.datetime` | -+----------------+--------------------------------------------------------+------------------------------------------------------------------------------+ -| Signature | ``strftime(format)`` | ``strptime(date_string, format)`` | -+----------------+--------------------------------------------------------+------------------------------------------------------------------------------+ ++----------------+--------------------------------------------------------+------------------------------------------------------------+ +| | ``strftime`` | ``strptime`` | ++================+========================================================+============================================================+ +| Usage | Convert object to a string according to a given format | Parse a string into an object given a corresponding format | ++----------------+--------------------------------------------------------+------------------------------------------------------------+ +| Type of method | Instance method | Class method | ++----------------+--------------------------------------------------------+------------------------------------------------------------+ +| Signature | ``strftime(format)`` | ``strptime(date_string, format)`` | ++----------------+--------------------------------------------------------+------------------------------------------------------------+ .. _format-codes: From 648f414395c2f8a3905f29e1b281c14e024a967a Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Wed, 19 Jun 2024 22:43:56 +0200 Subject: [PATCH 08/17] Add leap year note --- Doc/library/datetime.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1631b6c0a67e5a..4bdabd84e2c090 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -554,6 +554,25 @@ Other constructors, all class methods: time tuple. See also :ref:`strftime-strptime-behavior` and :meth:`date.fromisoformat`. + .. note + + If *format* specifies a day of month without a year a + :exc:`DeprecationWarning` is emitted. This is to avoid a quadrennial + leap year bug in code seeking to parse only a month and day as the + default year used in absence of one in the format is not a leap year. + Such *format* values may raise an error as of Python 3.15. The + workaround is to always include a year in your *format*. If parsing + *date_string* values that do not have a year, explicitly add a year that + is a leap year before parsing: + + .. doctest:: + + >>> from datetime import date + >>> date_string = "02/29" + >>> when = date.strptime(f"{date_string};1984", "%m/%d;%Y") # Avoids leap year bug. + >>> when.strftime("%B %d") # doctest: +SKIP + 'February 29' + .. versionadded:: 3.14 From c4cbef0700efc6a03e875073a89440a8a4cfb4bd Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 09:30:39 +0200 Subject: [PATCH 09/17] Update 2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst --- .../next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst index 925defaab13287..a166b2bf3e996c 100644 --- a/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst +++ b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst @@ -1,2 +1,2 @@ Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. - (Contributed by Wannes Boeykens in :gh:`120752`.) + (Contributed by Wannes Boeykens.) From a6a2083c9216d14129eb0f81b8941f680da29421 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 09:39:48 +0200 Subject: [PATCH 10/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/datetime.rst | 6 +++--- Lib/_pydatetime.py | 4 ++-- Modules/_datetimemodule.c | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 4bdabd84e2c090..91950ceadc35fa 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1905,8 +1905,8 @@ Other constructors: time(*(time.strptime(date_string, format)[3:6])) - :exc:`ValueError` is raised if the date_string and format - can't be parsed by :func:`time.strptime` or if it returns a value which isn't a + :exc:`ValueError` is raised if the *date_string* and *format* + cannot be parsed by :func:`time.strptime` or if it returns a value which is not a time tuple. See also :ref:`strftime-strptime-behavior` and :meth:`time.fromisoformat`. @@ -2411,7 +2411,7 @@ Class attributes: control of an explicit format string. Conversely, the :meth:`date.strptime`, :meth:`datetime.strptime` and -:meth:`time.strptime`, class methods create an object from a string +:meth:`time.strptime` class methods create an object from a string representing the time and a corresponding format string. The table below provides a high-level comparison of :meth:`~.datetime.strftime` diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 11880008099d3d..91f51e7ca8aa2f 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1011,7 +1011,7 @@ def fromisocalendar(cls, year, week, day): @classmethod def strptime(cls, date_string, format): - 'string, format -> new date parsed from a string (like time.strptime()).' + """Parse a date string according to the given format (like time.strptime()).""" import _strptime return _strptime._strptime_datetime_date(cls, date_string, format) @@ -1396,7 +1396,7 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold @classmethod def strptime(cls, date_string, format): - 'string, format -> new time parsed from a string (like time.strptime()).' + """string, format -> new time parsed from a string (like time.strptime()).""" import _strptime return _strptime._strptime_datetime_time(cls, date_string, format) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 91f56cefa7141c..c6c4343a65aa38 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3387,8 +3387,9 @@ date_strptime(PyObject *cls, PyObject *args) { PyObject *string, *format, *result; - if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) + if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) { return NULL; + } PyObject *module = PyImport_Import(&_Py_ID(_strptime)); if (module == NULL) { @@ -4611,7 +4612,9 @@ time_strptime(PyObject *cls, PyObject *args) { PyObject *string, *format, *result; - if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) + if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) { + return NULL; + } return NULL; PyObject *module = PyImport_Import(&_Py_ID(_strptime)); From d7525b4addcd4aea12c9c9dafcc2bc6e8709a20f Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 09:42:23 +0200 Subject: [PATCH 11/17] Remove parentheses --- .../next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst index a166b2bf3e996c..18e3506a60c455 100644 --- a/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst +++ b/Misc/NEWS.d/next/Library/2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst @@ -1,2 +1,2 @@ Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. - (Contributed by Wannes Boeykens.) +Contributed by Wannes Boeykens. From a5486de61e6159bae6bde2eddf710ef4fedc7755 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 09:53:41 +0200 Subject: [PATCH 12/17] Use helper function --- Lib/_strptime.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 8006655174c158..a8bf129033c261 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -574,21 +574,24 @@ def _strptime_datetime_date(cls, data_string, format="%a %b %d %Y"): args = tt[:3] return cls(*args) +def _parse_tz(tzname, gmtoff, gmtoff_fraction): + tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction) + if tzname: + return datetime_timezone(tzdelta, tzname) + else: + return datetime_timezone(tzdelta) + def _strptime_datetime_time(cls, data_string, format="%H:%M:%S"): """Return a time instance based on the input string and the format string.""" tt, fraction, gmtoff_fraction = _strptime(data_string, format) tzname, gmtoff = tt[-2:] args = tt[3:6] + (fraction,) - if gmtoff is not None: - tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction) - if tzname: - tz = datetime_timezone(tzdelta, tzname) - else: - tz = datetime_timezone(tzdelta) - args += (tz,) - - return cls(*args) + if gmtoff is None: + return cls(*args) + else: + tz = _parse_tz(tzname, gmtoff, gmtoff_fraction) + return cls(*args, tz) def _strptime_datetime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): """Return a datetime instance based on the input string and the @@ -596,12 +599,8 @@ def _strptime_datetime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y") tt, fraction, gmtoff_fraction = _strptime(data_string, format) tzname, gmtoff = tt[-2:] args = tt[:6] + (fraction,) - if gmtoff is not None: - tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction) - if tzname: - tz = datetime_timezone(tzdelta, tzname) - else: - tz = datetime_timezone(tzdelta) - args += (tz,) - - return cls(*args) + if gmtoff is None: + return cls(*args) + else: + tz = _parse_tz(tzname, gmtoff, gmtoff_fraction) + return cls(*args, tz) From f8bcdd3635122acc4d35d93d53d1ee7865eb565e Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 09:57:11 +0200 Subject: [PATCH 13/17] Remove bad return --- Modules/_datetimemodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c6c4343a65aa38..704e7018315da7 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4615,7 +4615,6 @@ time_strptime(PyObject *cls, PyObject *args) if (!PyArg_ParseTuple(args, "UU:strptime", &string, &format)) { return NULL; } - return NULL; PyObject *module = PyImport_Import(&_Py_ID(_strptime)); if (module == NULL) { From ff80f600f7c3ade77cf7fa8d5a7e68171890f141 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Mon, 24 Jun 2024 10:35:08 +0200 Subject: [PATCH 14/17] Link to github issue --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index e1753a3a02a909..face0b2a76e843 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -96,7 +96,7 @@ datetime -------- Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. -(Contributed by Wannes Boeykens in :gh:`120752`.) +(Contributed by Wannes Boeykens in :gh:`41431`.) os -- From 3f6b7295495c4ea8a8cdbfa5eb5ab466e67eb3a9 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Tue, 24 Sep 2024 09:59:07 +0200 Subject: [PATCH 15/17] Fix directive --- Doc/library/datetime.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 70097f574e1d49..59e2dbd6847538 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -560,7 +560,7 @@ Other constructors, all class methods: time tuple. See also :ref:`strftime-strptime-behavior` and :meth:`date.fromisoformat`. - .. note + .. note:: If *format* specifies a day of month without a year a :exc:`DeprecationWarning` is emitted. This is to avoid a quadrennial From 6cabc24d2094bbe780d030ced4df4cb133e8b828 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Wed, 25 Sep 2024 07:46:30 +0200 Subject: [PATCH 16/17] Apply suggestions from code review Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com> --- Lib/test/datetimetester.py | 84 ++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5ce1928d8b43fe..2aa7e7cf6d1a40 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1107,25 +1107,45 @@ def test_delta_non_days_ignored(self): self.assertEqual(dt2, dt - days) def test_strptime(self): - string = '2004-12-01' - format = '%Y-%m-%d' - expected = _strptime._strptime_datetime_date(date, string, format) - got = date.strptime(string, format) - self.assertEqual(expected, got) - self.assertIs(type(expected), date) - self.assertIs(type(got), date) - - # bpo-34482: Check that surrogates are handled properly. inputs = [ - ('2004-12\ud80001', '%Y-%m\ud800%d'), - ('2004\ud80012-01', '%Y\ud800%m-%d'), + # Basic valid cases + (date(1998, 2, 3), '1998-02-03', '%Y-%m-%d'), + (date(2004, 12, 2), '2004-12-02', '%Y-%m-%d'), + + # Edge cases: Leap year + (date(2020, 2, 29), '2020-02-29', '%Y-%m-%d'), # Valid leap year date + + # bpo-34482: Handle surrogate pairs + (date(2004, 12, 2), '2004-12\ud80002', '%Y-%m\ud800%d'), + (date(2004, 12, 2), '2004\ud80012-02', '%Y\ud800%m-%d'), + + # Month/day variations + (date(2004, 2, 1), '2004-02', '%Y-%m'), # No day provided + (date(2004, 2, 1), '02-2004', '%m-%Y'), # Month and year swapped + + # Different day-month-year formats + (date(2004, 12, 2), '01/12/2004', '%d/%m/%Y'), # Day/Month/Year + (date(2004, 12, 2), '12/01/2004', '%m/%d/%Y'), # Month/Day/Year + + # Different separators + (date(2023, 9, 24), '24.09.2023', '%d.%m.%Y'), # Dots as separators + (date(2023, 9, 24), '24-09-2023', '%d-%m-%Y'), # Dashes + (date(2023, 9, 24), '2023/09/24', '%Y/%m/%d'), # Slashes + + # Handling years with fewer digits + (date(127, 2, 3), '0127-02-03', '%Y-%m-%d'), + (date(99, 2, 3), '0099-02-03', '%Y-%m-%d'), + (date(5, 2, 3), '0005-02-03', '%Y-%m-%d'), + + # Variations on ISO 8601 format + (date(2023, 9, 24), '2023-W39-1', '%G-W%V-%u'), # ISO week date (Week 39, Monday) + (date(2023, 9, 24), '2023-267', '%Y-%j'), # Year and day of the year (Julian) ] - for string, format in inputs: + for expected, string, format in inputs: with self.subTest(string=string, format=format): - expected = _strptime._strptime_datetime_date(date, string, - format) got = date.strptime(string, format) self.assertEqual(expected, got) + self.assertIs(type(got), date) def test_strptime_single_digit(self): # bpo-34903: Check that single digit dates are allowed. @@ -3801,27 +3821,18 @@ def test_compat_unpickle(self): self.assertEqual(derived, expected) def test_strptime(self): - string = '13:02:47.197' - format = '%H:%M:%S.%f' - expected = _strptime._strptime_datetime_time(self.theclass, string, - format) - got = self.theclass.strptime(string, format) - self.assertEqual(expected, got) - self.assertIs(type(expected), self.theclass) - self.assertIs(type(got), self.theclass) - # bpo-34482: Check that surrogates are handled properly. inputs = [ - ('13:02\ud80047.197', '%H:%M\ud800%S.%f'), - ('13\ud80002:47.197', '%H\ud800%M:%S.%f'), + (self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%f'), + (self.theclass(13, 2, 47, 197000), '13:02\ud80047.197', '%H:%M\ud800%S.%f'), + (self.theclass(13, 2, 47, 197000), '13\ud80002:47.197', '%H\ud800%M:%S.%f'), ] - for string, format in inputs: + for expected, string, format in inputs: with self.subTest(string=string, format=format): - expected = _strptime._strptime_datetime_time(self.theclass, - string, format) - got = self.theclass.strptime(string, format) self.assertEqual(expected, got) + self.assertIs(type(got), self.theclass) + def test_strptime_tz(self): strptime = self.theclass.strptime self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) @@ -3840,9 +3851,11 @@ def test_strptime(self): seconds = tzseconds hours, minutes = divmod(seconds//60, 60) tstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname) - t = strptime(tstr, "%z %Z") - self.assertEqual(t.utcoffset(), timedelta(seconds=tzseconds)) - self.assertEqual(t.tzname(), tzname) + with self.subTest(tstr=tstr): + t = strptime(tstr, "%z %Z") + self.assertEqual(t.utcoffset(), timedelta(seconds=tzseconds)) + self.assertEqual(t.tzname(), tzname) + self.assertIs(type(t), self.theclass) # Can produce inconsistent time tstr, fmt = "+1234 UTC", "%z %Z" @@ -3855,9 +3868,10 @@ def test_strptime(self): # Produce naive time if no %z is provided self.assertEqual(strptime("UTC", "%Z").tzinfo, None) - with self.assertRaises(ValueError): strptime("-2400", "%z") - with self.assertRaises(ValueError): strptime("-000", "%z") - with self.assertRaises(ValueError): strptime("z", "%z") + def test_strptime_errors(self): + for tzstr in ("-2400", "-000", "z"): + with self.assertRaises(ValueError): + self.theclass.strptime(tzstr, "%z") def test_strptime_single_digit(self): # bpo-34903: Check that single digit times are allowed. From f6829850fb15f80394f5529c2b9053281acf5d09 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 25 Sep 2024 10:36:56 +0200 Subject: [PATCH 17/17] Fix test cases --- Lib/test/datetimetester.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2aa7e7cf6d1a40..61c32d5de0a6a6 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1124,8 +1124,8 @@ def test_strptime(self): (date(2004, 2, 1), '02-2004', '%m-%Y'), # Month and year swapped # Different day-month-year formats - (date(2004, 12, 2), '01/12/2004', '%d/%m/%Y'), # Day/Month/Year - (date(2004, 12, 2), '12/01/2004', '%m/%d/%Y'), # Month/Day/Year + (date(2004, 12, 2), '02/12/2004', '%d/%m/%Y'), # Day/Month/Year + (date(2004, 12, 2), '12/02/2004', '%m/%d/%Y'), # Month/Day/Year # Different separators (date(2023, 9, 24), '24.09.2023', '%d.%m.%Y'), # Dots as separators @@ -1138,8 +1138,8 @@ def test_strptime(self): (date(5, 2, 3), '0005-02-03', '%Y-%m-%d'), # Variations on ISO 8601 format - (date(2023, 9, 24), '2023-W39-1', '%G-W%V-%u'), # ISO week date (Week 39, Monday) - (date(2023, 9, 24), '2023-267', '%Y-%j'), # Year and day of the year (Julian) + (date(2023, 9, 25), '2023-W39-1', '%G-W%V-%u'), # ISO week date (Week 39, Monday) + (date(2023, 9, 25), '2023-268', '%Y-%j'), # Year and day of the year (Julian) ] for expected, string, format in inputs: with self.subTest(string=string, format=format): @@ -3829,6 +3829,7 @@ def test_strptime(self): ] for expected, string, format in inputs: with self.subTest(string=string, format=format): + got = self.theclass.strptime(string, format) self.assertEqual(expected, got) self.assertIs(type(got), self.theclass)