From bd8ef9468130747bf090a38e5d8e2fcff2366423 Mon Sep 17 00:00:00 2001 From: kigland Date: Sat, 30 May 2026 03:13:31 +0800 Subject: [PATCH] Preserve the Box subclass for nested boxes after unpickling Box.__init__ derives box_class from self.__class__, but Box.__new__ hardcoded the base Box. Unpickling calls __new__ + __setstate__ (not __init__), so a subclass instance ended up with box_class=Box, and nested boxes created on access after unpickling became base Box instead of the subclass. Use cls in __new__ to match __init__. --- box/box.py | 2 +- test/test_box.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/box/box.py b/box/box.py index 8252643..f1d7840 100644 --- a/box/box.py +++ b/box/box.py @@ -233,7 +233,7 @@ def __new__( "box_recast": box_recast, "box_dots": box_dots, "box_dots_exclude": re.compile(box_dots_exclude) if box_dots_exclude else None, - "box_class": box_class if box_class is not None else Box, + "box_class": box_class if box_class is not None else cls, "box_namespace": box_namespace, } ) diff --git a/test/test_box.py b/test/test_box.py index e5c56e0..c5896ad 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -32,6 +32,10 @@ from box.converters import BOX_PARAMETERS +class _SubclassBox(Box): + """Module-level Box subclass so its instances are picklable in tests.""" + + def mp_queue_test(q): bx = q.get() try: @@ -686,6 +690,17 @@ def test_pickle(self): assert bx == loaded2 loaded2.box_options = bx.box_options + def test_pickle_preserves_subclass_on_nested_access(self): + if platform.python_implementation() == "PyPy": + pytest.skip("Pickling does not work correctly on PyPy") + # Unpickling goes through __new__ + __setstate__ but not __init__, so + # box_class must be set from cls in __new__ too. Otherwise nested boxes + # created on access after unpickling fall back to the base Box (#308). + b = _SubclassBox({"a": {"b": 1}}) + loaded = pickle.loads(pickle.dumps(b)) + assert isinstance(loaded, _SubclassBox) + assert type(loaded.a) is _SubclassBox + def test_pickle_default_box(self): if platform.python_implementation() == "PyPy": pytest.skip("Pickling does not work correctly on PyPy")