From 5d226df04fe4a8942573c6625a3ed5c3c0b5d08d Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Wed, 23 Sep 2026 00:40:22 -0300 Subject: [PATCH] gh-157976: Fix shelve.Shelf.sync() leaving writeback disabled on error Shelf.sync() sets self.writeback to False while it writes the cached entries back. If writing an entry raises, for example because a cached object cannot be pickled, writeback is never set back to True. The shelf keeps working, but later mutations of accessed objects are silently not persisted. Restore writeback in a finally block. The cache is kept on failure, so the caller can fix the offending entry and call sync() again. --- Lib/shelve.py | 8 +++++--- Lib/test/test_shelve.py | 19 +++++++++++++++++++ ...-09-23-03-52-29.gh-issue-157976.cf2fd0.rst | 2 ++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-23-03-52-29.gh-issue-157976.cf2fd0.rst diff --git a/Lib/shelve.py b/Lib/shelve.py index 9f6296667fdb6bd..f791123f0457e35 100644 --- a/Lib/shelve.py +++ b/Lib/shelve.py @@ -176,9 +176,11 @@ def __del__(self): def sync(self): if self.writeback and self.cache: self.writeback = False - for key, entry in self.cache.items(): - self[key] = entry - self.writeback = True + try: + for key, entry in self.cache.items(): + self[key] = entry + finally: + self.writeback = True self.cache = {} if hasattr(self.dict, 'sync'): self.dict.sync() diff --git a/Lib/test/test_shelve.py b/Lib/test/test_shelve.py index 5f6a030e018f96a..e0f7844ce900dd5 100644 --- a/Lib/test/test_shelve.py +++ b/Lib/test/test_shelve.py @@ -148,6 +148,25 @@ def test_writeback_also_writes_immediately(self): p2 = d[encodedkey] self.assertNotEqual(p1, p2) # Write creates new object in store + def test_writeback_restored_after_failed_sync(self): + class Unpicklable: + def __reduce__(self): + raise RuntimeError('cannot pickle') + + d = {} + with shelve.Shelf(d, writeback=True) as s: + s['key'] = [] + s['key'].append(Unpicklable()) + with self.assertRaises(RuntimeError): + s.sync() + self.assertTrue(s.writeback) + self.assertIn('key', s.cache) + s['key'].clear() + s['other'] = [1] + s['other'].append(2) + self.assertEqual(pickle.loads(d[b'key']), []) + self.assertEqual(pickle.loads(d[b'other']), [1, 2]) + def test_with(self): d1 = {} with shelve.Shelf(d1, protocol=2, writeback=False) as s: diff --git a/Misc/NEWS.d/next/Library/2026-09-23-03-52-29.gh-issue-157976.cf2fd0.rst b/Misc/NEWS.d/next/Library/2026-09-23-03-52-29.gh-issue-157976.cf2fd0.rst new file mode 100644 index 000000000000000..a67d59edb892665 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-23-03-52-29.gh-issue-157976.cf2fd0.rst @@ -0,0 +1,2 @@ +Fix :meth:`shelve.Shelf.sync` leaving *writeback* disabled when writing back +a cached entry raises an exception. Patch by Christian Aurich Zanettini Martins.