From 24ee255e166c576008a9f55c82d42f4a1a028fee Mon Sep 17 00:00:00 2001 From: furkanonder Date: Wed, 10 Sep 2025 22:18:13 +0300 Subject: [PATCH 1/7] Enhance shelve serializer validation with descriptive error messages --- Lib/shelve.py | 14 ++++++++ Lib/test/test_shelve.py | 80 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/Lib/shelve.py b/Lib/shelve.py index 1010be1e09d702..dc293deb1b5a8a 100644 --- a/Lib/shelve.py +++ b/Lib/shelve.py @@ -106,6 +106,19 @@ def __init__(self, dict, protocol=None, writeback=False, self.serializer = serializer self.deserializer = deserializer + @staticmethod + def _validate_serialized_value(serialized_value, original_value): + if (serialized_value is None or + not isinstance(serialized_value, (bytes, str))): + if serialized_value is None: + invalid_type = "None" + else: + invalid_type = type(serialized_value).__name__ + msg = (f"Serializer returned {invalid_type} for value " + f"{original_value!r} But database values must be " + f"bytes or str, not {invalid_type}") + raise ShelveError(msg) + def __iter__(self): for k in self.dict.keys(): yield k.decode(self.keyencoding) @@ -135,6 +148,7 @@ def __setitem__(self, key, value): if self.writeback: self.cache[key] = value serialized_value = self.serializer(value, self._protocol) + self._validate_serialized_value(serialized_value, value) self.dict[key.encode(self.keyencoding)] = serialized_value def __delitem__(self, key): diff --git a/Lib/test/test_shelve.py b/Lib/test/test_shelve.py index 64609ab9dd9a62..b0a0dbf3baaf21 100644 --- a/Lib/test/test_shelve.py +++ b/Lib/test/test_shelve.py @@ -173,6 +173,8 @@ def test_custom_serializer_and_deserializer(self): def serializer(obj, protocol): if isinstance(obj, (bytes, bytearray, str)): if protocol == 5: + if isinstance(obj, bytearray): + return bytes(obj) # DBM backends expect bytes return obj return type(obj).__name__ elif isinstance(obj, array.array): @@ -223,11 +225,10 @@ def deserializer(data): ) def test_custom_incomplete_serializer_and_deserializer(self): - dbm_sqlite3 = import_helper.import_module("dbm.sqlite3") os.mkdir(self.dirname) self.addCleanup(os_helper.rmtree, self.dirname) - with self.assertRaises(dbm_sqlite3.error): + with self.assertRaises(shelve.ShelveError): def serializer(obj, protocol=None): pass @@ -430,6 +431,81 @@ def setUp(self): dbm._defaultmod = self.dbm_mod +class TestShelveValidation(unittest.TestCase): + dirname = os_helper.TESTFN + fname = os.path.join(dirname, os_helper.TESTFN) + + def setup_test_dir(self): + os_helper.rmtree(self.dirname) + os.mkdir(self.dirname) + + def setUp(self): + self.addCleanup(setattr, dbm, "_defaultmod", dbm._defaultmod) + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def test_serializer_unsupported_return_type(self): + def int_serializer(obj, protocol=None): + return 3 + + def none_serializer(obj, protocol=None): + return None + + def deserializer(data): + if isinstance(data, bytes): + return data.decode("utf-8") + else: + return data + + for module in dbm_iterator(): + self.setup_test_dir() + dbm._defaultmod = module + with module.open(self.fname, "c"): + pass + self.assertEqual(module.__name__, dbm.whichdb(self.fname)) + + with shelve.open(self.fname, serializer=none_serializer, + deserializer=deserializer) as s: + with self.assertRaises(shelve.ShelveError) as cm: + s["key"] = "value" + self.assertEqual("Serializer returned None for value 'value' " + "But database values must be bytes or str, not None", + f"{cm.exception}") + + with shelve.open(self.fname, serializer=int_serializer, + deserializer=deserializer,) as s: + with self.assertRaises(shelve.ShelveError) as cm: + s["key"] = "value" + self.assertEqual("Serializer returned int for value 'value' " + "But database values must be bytes or str, not int", + f"{cm.exception}") + + def test_shelve_type_compatibility(self): + for module in dbm_iterator(): + self.setup_test_dir() + dbm._defaultmod = module + with shelve.Shelf(module.open(self.fname, "c")) as shelf: + shelf["string"] = "hello" + shelf["bytes"] = b"world" + shelf["number"] = 42 + shelf["list"] = [1, 2, 3] + shelf["dict"] = {"key": "value"} + shelf["set"] = {1, 2, 3} + shelf["tuple"] = (1, 2, 3) + shelf["complex"] = 1 + 2j + shelf["bytearray"] = bytearray(b"test") + shelf["array"] = array.array("i", [1, 2, 3]) + self.assertEqual(shelf["string"], "hello") + self.assertEqual(shelf["bytes"], b"world") + self.assertEqual(shelf["number"], 42) + self.assertEqual(shelf["list"], [1, 2, 3]) + self.assertEqual(shelf["dict"], {"key": "value"}) + self.assertEqual(shelf["set"], {1, 2, 3}) + self.assertEqual(shelf["tuple"], (1, 2, 3)) + self.assertEqual(shelf["complex"], 1 + 2j) + self.assertEqual(shelf["bytearray"], bytearray(b"test")) + self.assertEqual(shelf["array"], array.array("i", [1, 2, 3])) + from test import mapping_tests for proto in range(pickle.HIGHEST_PROTOCOL + 1): From 563e204faf6376e34651b94ffb1422ce8a96b18e Mon Sep 17 00:00:00 2001 From: furkanonder Date: Mon, 22 Sep 2025 23:28:57 +0300 Subject: [PATCH 2/7] Enhance dbm module error messages with descriptive type information --- Modules/_dbmmodule.c | 10 ++++++---- Modules/_gdbmmodule.c | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c index 0cd0f043de453d..68002e05d7ab6f 100644 --- a/Modules/_dbmmodule.c +++ b/Modules/_dbmmodule.c @@ -236,8 +236,9 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) dbmobject *dp = dbmobject_CAST(self); if ( !PyArg_Parse(v, "s#", &krec.dptr, &tmp_size) ) { - PyErr_SetString(PyExc_TypeError, - "dbm mappings have bytes or string keys only"); + PyErr_Format(PyExc_TypeError, + "dbm key returned %.100s for value %R But database keys must be bytes or str, not %.100s", + Py_TYPE(v)->tp_name, v, Py_TYPE(v)->tp_name); return -1; } _dbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); @@ -263,8 +264,9 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) } } else { if ( !PyArg_Parse(w, "s#", &drec.dptr, &tmp_size) ) { - PyErr_SetString(PyExc_TypeError, - "dbm mappings have bytes or string elements only"); + PyErr_Format(PyExc_TypeError, + "dbm value returned %.100s for value %R But database values must be bytes or str, not %.100s", + Py_TYPE(w)->tp_name, w, Py_TYPE(w)->tp_name); return -1; } drec.dsize = tmp_size; diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c index 6a4939512b22fc..40f25e74789b2e 100644 --- a/Modules/_gdbmmodule.c +++ b/Modules/_gdbmmodule.c @@ -248,7 +248,7 @@ parse_datum(PyObject *o, datum *d, const char *failmsg) Py_ssize_t size; if (!PyArg_Parse(o, "s#", &d->dptr, &size)) { if (failmsg != NULL) { - PyErr_SetString(PyExc_TypeError, failmsg); + PyErr_Format(PyExc_TypeError, failmsg, Py_TYPE(o)->tp_name, o, Py_TYPE(o)->tp_name); } return 0; } @@ -324,11 +324,12 @@ static int gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) { datum krec, drec; - const char *failmsg = "gdbm mappings have bytes or string indices only"; + const char *key_failmsg = "dbm key returned %.100s for value %R But database keys must be bytes or str, not %.100s"; + const char *value_failmsg = "dbm value returned %.100s for value %R But database keys must be bytes or str, not %.100s"; gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); - if (!parse_datum(v, &krec, failmsg)) { + if (!parse_datum(v, &krec, key_failmsg)) { return -1; } if (dp->di_dbm == NULL) { @@ -349,7 +350,7 @@ gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) } } else { - if (!parse_datum(w, &drec, failmsg)) { + if (!parse_datum(w, &drec, value_failmsg)) { return -1; } errno = 0; From 594dacac10739597e9681f0877ca5d0999eecef7 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 16 Oct 2025 13:29:55 +0200 Subject: [PATCH 3/7] Reword error messages more --- Lib/shelve.py | 4 ++-- Lib/test/test_shelve.py | 8 ++++---- Modules/_dbmmodule.c | 8 ++++---- Modules/_gdbmmodule.c | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/shelve.py b/Lib/shelve.py index dc293deb1b5a8a..fe238069ad5bfb 100644 --- a/Lib/shelve.py +++ b/Lib/shelve.py @@ -114,8 +114,8 @@ def _validate_serialized_value(serialized_value, original_value): invalid_type = "None" else: invalid_type = type(serialized_value).__name__ - msg = (f"Serializer returned {invalid_type} for value " - f"{original_value!r} But database values must be " + msg = (f"Serializer returned {serialized_value!r} for value " + f"{original_value!r}, but database values must be " f"bytes or str, not {invalid_type}") raise ShelveError(msg) diff --git a/Lib/test/test_shelve.py b/Lib/test/test_shelve.py index b0a0dbf3baaf21..f0b27f1ddcc86d 100644 --- a/Lib/test/test_shelve.py +++ b/Lib/test/test_shelve.py @@ -468,16 +468,16 @@ def deserializer(data): deserializer=deserializer) as s: with self.assertRaises(shelve.ShelveError) as cm: s["key"] = "value" - self.assertEqual("Serializer returned None for value 'value' " - "But database values must be bytes or str, not None", + self.assertEqual("Serializer returned None for value 'value', but " + "database values must be bytes or str, not None", f"{cm.exception}") with shelve.open(self.fname, serializer=int_serializer, deserializer=deserializer,) as s: with self.assertRaises(shelve.ShelveError) as cm: s["key"] = "value" - self.assertEqual("Serializer returned int for value 'value' " - "But database values must be bytes or str, not int", + self.assertEqual("Serializer returned 3 for value 'value', but " + "database values must be bytes or str, not int", f"{cm.exception}") def test_shelve_type_compatibility(self): diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c index e5c75146a0a624..4bf783e6365ece 100644 --- a/Modules/_dbmmodule.c +++ b/Modules/_dbmmodule.c @@ -232,8 +232,8 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) if ( !PyArg_Parse(v, "s#", &krec.dptr, &tmp_size) ) { PyErr_Format(PyExc_TypeError, - "dbm key returned %.100s for value %R But database keys must be bytes or str, not %.100s", - Py_TYPE(v)->tp_name, v, Py_TYPE(v)->tp_name); + "database keys must be bytes or str, not %T", + v); return -1; } _dbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); @@ -260,8 +260,8 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) } else { if ( !PyArg_Parse(w, "s#", &drec.dptr, &tmp_size) ) { PyErr_Format(PyExc_TypeError, - "dbm value returned %.100s for value %R But database values must be bytes or str, not %.100s", - Py_TYPE(w)->tp_name, w, Py_TYPE(w)->tp_name); + "database values must be bytes or str, not %T", + w); return -1; } drec.dsize = tmp_size; diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c index e2fe93c86ff6e4..2d70d9c201cc0c 100644 --- a/Modules/_gdbmmodule.c +++ b/Modules/_gdbmmodule.c @@ -242,7 +242,7 @@ parse_datum(PyObject *o, datum *d, const char *failmsg) Py_ssize_t size; if (!PyArg_Parse(o, "s#", &d->dptr, &size)) { if (failmsg != NULL) { - PyErr_Format(PyExc_TypeError, failmsg, Py_TYPE(o)->tp_name, o, Py_TYPE(o)->tp_name); + PyErr_Format(PyExc_TypeError, failmsg, o); } return 0; } @@ -318,8 +318,8 @@ static int gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) { datum krec, drec; - const char *key_failmsg = "dbm key returned %.100s for value %R But database keys must be bytes or str, not %.100s"; - const char *value_failmsg = "dbm value returned %.100s for value %R But database keys must be bytes or str, not %.100s"; + const char *key_failmsg = "database keys must be bytes or str, not %T"; + const char *value_failmsg = "database values must be bytes or str, not %T"; gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); From efd9cac02209bbf8216866e5b4f3b7e2ffdca77f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 16 Oct 2025 13:41:38 +0200 Subject: [PATCH 4/7] Use literal format string for PyErr_Format --- Modules/_gdbmmodule.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c index 2d70d9c201cc0c..d2020673eb78bc 100644 --- a/Modules/_gdbmmodule.c +++ b/Modules/_gdbmmodule.c @@ -237,13 +237,13 @@ gdbm_bool(PyObject *op) // This function is needed to support PY_SSIZE_T_CLEAN. // Return 1 on success, same to PyArg_Parse(). static int -parse_datum(PyObject *o, datum *d, const char *failmsg) +parse_datum(PyObject *o, datum *d, const char *items_name) { Py_ssize_t size; if (!PyArg_Parse(o, "s#", &d->dptr, &size)) { - if (failmsg != NULL) { - PyErr_Format(PyExc_TypeError, failmsg, o); - } + PyErr_Format(PyExc_TypeError, + "database %s must be bytes or str, not %T", + items_name, o); return 0; } if (INT_MAX < size) { @@ -262,7 +262,7 @@ gdbm_subscript_lock_held(PyObject *op, PyObject *key) gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); - if (!parse_datum(key, &krec, NULL)) { + if (!parse_datum(key, &krec, "keys")) { return NULL; } if (dp->di_dbm == NULL) { @@ -318,12 +318,10 @@ static int gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) { datum krec, drec; - const char *key_failmsg = "database keys must be bytes or str, not %T"; - const char *value_failmsg = "database values must be bytes or str, not %T"; gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); - if (!parse_datum(v, &krec, key_failmsg)) { + if (!parse_datum(v, &krec, "keys")) { return -1; } if (dp->di_dbm == NULL) { @@ -344,7 +342,7 @@ gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) } } else { - if (!parse_datum(w, &drec, value_failmsg)) { + if (!parse_datum(w, &drec, "values")) { return -1; } errno = 0; From 8f548c7e5a38ed8ba5dad94001384c7762e1abe4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 16 Oct 2025 14:00:26 +0200 Subject: [PATCH 5/7] Add tests, and make the messages more similar --- Lib/test/test_dbm_gnu.py | 19 +++++++++++++++++++ Lib/test/test_dbm_ndbm.py | 19 +++++++++++++++++++ Modules/_dbmmodule.c | 7 +++---- Modules/_gdbmmodule.c | 19 ++++++++++--------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_dbm_gnu.py b/Lib/test/test_dbm_gnu.py index 66268c42a300b5..c092357d92a635 100644 --- a/Lib/test/test_dbm_gnu.py +++ b/Lib/test/test_dbm_gnu.py @@ -217,6 +217,25 @@ def test_localized_error(self): create_empty_file(os.path.join(d, 'test')) self.assertRaises(gdbm.error, gdbm.open, filename, 'r') + def test_type_errors(self): + self.g = gdbm.open(filename, 'c') + with self.assertRaisesRegex( + TypeError, "^a bytes-like object is required, not 'int'$", + ): + self.g[123] + with self.assertRaisesRegex( + TypeError, "^gdbm key must be bytes or str, not 'int'$", + ): + 123 in self.g + with self.assertRaisesRegex( + TypeError, "^gdbm key must be bytes or str, not 'NoneType'$", + ): + self.g[None] = 123 + with self.assertRaisesRegex( + TypeError, "^gdbm value must be bytes or str, not 'int'$", + ): + self.g['foo'] = 123 + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_dbm_ndbm.py b/Lib/test/test_dbm_ndbm.py index e0f31c9a9a337d..01da376b599b3d 100644 --- a/Lib/test/test_dbm_ndbm.py +++ b/Lib/test/test_dbm_ndbm.py @@ -160,6 +160,25 @@ def test_clear(self): self.assertNotIn(k, db) self.assertEqual(len(db), 0) + def test_type_errors(self): + with dbm.ndbm.open(self.filename, 'c') as db: + with self.assertRaisesRegex( + TypeError, "^a bytes-like object is required, not 'int'$", + ): + db[123] + with self.assertRaisesRegex( + TypeError, "^dbm key must be bytes or str, not 'int'$", + ): + 123 in db + with self.assertRaisesRegex( + TypeError, "^dbm key must be bytes or str, not 'NoneType'$", + ): + db[None] = 123 + with self.assertRaisesRegex( + TypeError, "^dbm value must be bytes or str, not 'int'$", + ): + db['foo'] = 123 + if __name__ == '__main__': unittest.main() diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c index 4bf783e6365ece..a9fae05e9e97a4 100644 --- a/Modules/_dbmmodule.c +++ b/Modules/_dbmmodule.c @@ -232,7 +232,7 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) if ( !PyArg_Parse(v, "s#", &krec.dptr, &tmp_size) ) { PyErr_Format(PyExc_TypeError, - "database keys must be bytes or str, not %T", + "dbm key must be bytes or str, not '%T'", v); return -1; } @@ -260,7 +260,7 @@ dbm_ass_sub_lock_held(PyObject *self, PyObject *v, PyObject *w) } else { if ( !PyArg_Parse(w, "s#", &drec.dptr, &tmp_size) ) { PyErr_Format(PyExc_TypeError, - "database values must be bytes or str, not %T", + "dbm value must be bytes or str, not '%T'", w); return -1; } @@ -371,8 +371,7 @@ dbm_contains_lock_held(PyObject *self, PyObject *arg) } else if (!PyBytes_Check(arg)) { PyErr_Format(PyExc_TypeError, - "dbm key must be bytes or string, not %.100s", - Py_TYPE(arg)->tp_name); + "dbm key must be bytes or str, not '%T'", arg); return -1; } else { diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c index d2020673eb78bc..96303821661b4e 100644 --- a/Modules/_gdbmmodule.c +++ b/Modules/_gdbmmodule.c @@ -237,13 +237,15 @@ gdbm_bool(PyObject *op) // This function is needed to support PY_SSIZE_T_CLEAN. // Return 1 on success, same to PyArg_Parse(). static int -parse_datum(PyObject *o, datum *d, const char *items_name) +parse_datum(PyObject *o, datum *d, const char *item_name) { Py_ssize_t size; if (!PyArg_Parse(o, "s#", &d->dptr, &size)) { - PyErr_Format(PyExc_TypeError, - "database %s must be bytes or str, not %T", - items_name, o); + if (item_name) { + PyErr_Format(PyExc_TypeError, + "gdbm %s must be bytes or str, not '%T'", + item_name, o); + } return 0; } if (INT_MAX < size) { @@ -262,7 +264,7 @@ gdbm_subscript_lock_held(PyObject *op, PyObject *key) gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); - if (!parse_datum(key, &krec, "keys")) { + if (!parse_datum(key, &krec, NULL)) { return NULL; } if (dp->di_dbm == NULL) { @@ -321,7 +323,7 @@ gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) gdbmobject *dp = _gdbmobject_CAST(op); _gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp)); - if (!parse_datum(v, &krec, "keys")) { + if (!parse_datum(v, &krec, "key")) { return -1; } if (dp->di_dbm == NULL) { @@ -342,7 +344,7 @@ gdbm_ass_sub_lock_held(PyObject *op, PyObject *v, PyObject *w) } } else { - if (!parse_datum(w, &drec, "values")) { + if (!parse_datum(w, &drec, "value")) { return -1; } errno = 0; @@ -490,8 +492,7 @@ gdbm_contains_lock_held(PyObject *self, PyObject *arg) } else if (!PyBytes_Check(arg)) { PyErr_Format(PyExc_TypeError, - "gdbm key must be bytes or string, not %.100s", - Py_TYPE(arg)->tp_name); + "gdbm key must be bytes or str, not '%T'", arg); return -1; } else { From 7a1fc8db43d508a131a6ef96126069205f9e3293 Mon Sep 17 00:00:00 2001 From: furkanonder Date: Mon, 17 Nov 2025 00:19:34 +0900 Subject: [PATCH 6/7] Simplify shelve error message on validation of serialization --- Lib/shelve.py | 4 +--- Lib/test/test_shelve.py | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/shelve.py b/Lib/shelve.py index fe238069ad5bfb..63e4f116b52c4a 100644 --- a/Lib/shelve.py +++ b/Lib/shelve.py @@ -114,9 +114,7 @@ def _validate_serialized_value(serialized_value, original_value): invalid_type = "None" else: invalid_type = type(serialized_value).__name__ - msg = (f"Serializer returned {serialized_value!r} for value " - f"{original_value!r}, but database values must be " - f"bytes or str, not {invalid_type}") + msg = f"Serializer must return bytes or str, not {invalid_type}" raise ShelveError(msg) def __iter__(self): diff --git a/Lib/test/test_shelve.py b/Lib/test/test_shelve.py index f0b27f1ddcc86d..931f1ed8e6147f 100644 --- a/Lib/test/test_shelve.py +++ b/Lib/test/test_shelve.py @@ -468,16 +468,14 @@ def deserializer(data): deserializer=deserializer) as s: with self.assertRaises(shelve.ShelveError) as cm: s["key"] = "value" - self.assertEqual("Serializer returned None for value 'value', but " - "database values must be bytes or str, not None", + self.assertEqual("Serializer must return bytes or str, not None", f"{cm.exception}") with shelve.open(self.fname, serializer=int_serializer, deserializer=deserializer,) as s: with self.assertRaises(shelve.ShelveError) as cm: s["key"] = "value" - self.assertEqual("Serializer returned 3 for value 'value', but " - "database values must be bytes or str, not int", + self.assertEqual("Serializer must return bytes or str, not int", f"{cm.exception}") def test_shelve_type_compatibility(self): From acda993eff6d8b9f770ab4d198b4139eaaa090aa Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:29:59 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-11-16-15-29-49.gh-issue-137899.JnbEmT.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-11-16-15-29-49.gh-issue-137899.JnbEmT.rst diff --git a/Misc/NEWS.d/next/Library/2025-11-16-15-29-49.gh-issue-137899.JnbEmT.rst b/Misc/NEWS.d/next/Library/2025-11-16-15-29-49.gh-issue-137899.JnbEmT.rst new file mode 100644 index 00000000000000..12faf24a0a6d46 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-16-15-29-49.gh-issue-137899.JnbEmT.rst @@ -0,0 +1 @@ +The :mod:`shelve` module will now provide descriptive error messages to better distinguish key and value type errors. Patch by Furkan Onder.