From 8c458e50d168d088219e214d7752428fe1cbea08 Mon Sep 17 00:00:00 2001 From: ***** <721466+soodoku@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:15:41 -0700 Subject: [PATCH] Tolerate a missing _cloudpickle_submodules key in _function_setstate _function_setstate pops "_cloudpickle_submodules" from the slotstate without a default, so a payload that does not carry that key raises an opaque KeyError from inside pickle.loads rather than a normal unpickling failure. Before c6f8cd4 the code guarded this: if '_cloudpickle_submodules' in state: state.pop('_cloudpickle_submodules') That commit consolidated the module and the surviving version pops unconditionally. Restore the tolerance by passing a default. Valid cloudpickle payloads always set the key, so this is a no-op for them. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGES.md | 5 +++++ cloudpickle/cloudpickle.py | 6 ++++-- tests/cloudpickle_test.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a6b0b443..01c5b996 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,11 @@ In development - Make pickling of functions depending on globals in notebook more deterministic. ([PR#560](https://github.com/cloudpipe/cloudpickle/pull/560)) +- Tolerate a missing `_cloudpickle_submodules` key when restoring the state of + a dynamic function, so that a malformed payload no longer crashes with an + opaque `KeyError`. + ([issue#593](https://github.com/cloudpipe/cloudpickle/issues/593)) + 3.1.2 ===== diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 963a8259..ae042006 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -1152,8 +1152,10 @@ def _function_setstate(obj, state): # the pickled function to work correctly at unpickling time. Now that these # submodules are depickled (hence imported), they can be removed from the # object's state (the object state only served as a reference holder to - # these submodules) - slotstate.pop("_cloudpickle_submodules") + # these submodules). Tolerate its absence: valid cloudpickle payloads always + # set this key, but a malformed one need not, and popping it unguarded turns + # that into an opaque KeyError. + slotstate.pop("_cloudpickle_submodules", None) obj.__globals__.update(obj_globals) obj.__globals__["__builtins__"] = __builtins__ diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index e2097d1c..0d17d7d0 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -48,6 +48,7 @@ from cloudpickle.cloudpickle import _make_empty_cell from cloudpickle.cloudpickle import _extract_class_dict, _whichmodule from cloudpickle.cloudpickle import _lookup_module_and_qualname +from cloudpickle.cloudpickle import _function_setstate from .testutils import subprocess_worker from .testutils import subprocess_pickle_echo @@ -3180,5 +3181,18 @@ def test_module_level_pickler(): assert cloudpickle.Pickler is cloudpickle.CloudPickler +def test_function_setstate_without_submodules_key(): + # #593: a payload whose slotstate lacks "_cloudpickle_submodules" must not + # crash with an opaque KeyError. Valid cloudpickle payloads always carry + # that key, but malformed ones need not, and the guard that used to allow + # for its absence was dropped when the module was consolidated. + def f(x): + return x + + _function_setstate(f, ({}, {"__globals__": {}, "__closure__": None})) + + assert f(1) == 1 + + if __name__ == "__main__": unittest.main()