Skip to content

Commit fc8449a

Browse files
committed
Save and restore working_set in include_subclasses
When include_subclasses is called from within a structure hook factory (which is itself invoked during make_dict_structure_fn), it would overwrite already_generating.working_set with its own set and then reset it to an empty set. This caused the outer make_dict_structure_fn to fail with AttributeError when trying to clean up its working_set. Now the existing working_set is saved before the loop and restored after, so nested calls work correctly.
1 parent fd887b7 commit fc8449a

File tree

2 files changed

+74
-0
lines changed

2 files changed

+74
-0
lines changed

src/cattrs/strategies/_subclasses.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ def _include_subclasses_with_union_strategy(
191191

192192
original_unstruct_hooks = {}
193193
original_struct_hooks = {}
194+
195+
# Save the existing working_set so we can restore it after the loop.
196+
# include_subclasses may be called while make_dict_structure_fn is
197+
# already generating hooks for outer classes (via a hook factory),
198+
# so we must not clobber the outer working_set.
199+
_had_working_set = hasattr(already_generating, "working_set")
200+
_prev_working_set = getattr(already_generating, "working_set", None)
201+
194202
for cl in union_classes:
195203
# In the first pass, every class gets its own unstructure function according to
196204
# the overrides.
@@ -209,6 +217,12 @@ def _include_subclasses_with_union_strategy(
209217
original_unstruct_hooks[cl] = unstruct_hook
210218
original_struct_hooks[cl] = struct_hook
211219

220+
# Restore the previous working_set state.
221+
if _had_working_set:
222+
already_generating.working_set = _prev_working_set
223+
elif hasattr(already_generating, "working_set"):
224+
del already_generating.working_set
225+
212226
# Now that's done, we can register all the hooks and generate the
213227
# union handler. The union handler needs them.
214228
final_union = Union[union_classes] # type: ignore

tests/strategies/test_include_subclasses.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,3 +536,63 @@ class Sub(Mid1, Mid2):
536536
assert genconverter.structure({"_type": "Sub"}, Base) == Sub()
537537
assert genconverter.structure({"_type": "Mid1"}, Base) == Mid1()
538538
assert genconverter.structure({"_type": "Mid2"}, Base) == Mid2()
539+
540+
541+
def test_include_subclasses_in_hook_factory():
542+
"""include_subclasses called from within a structure hook factory should
543+
not clobber the outer already_generating working_set (#721)."""
544+
from attrs import frozen, has
545+
from cattrs.gen import make_dict_structure_fn
546+
from cattrs.preconf.json import make_converter
547+
548+
@frozen
549+
class A:
550+
pass
551+
552+
@frozen
553+
class A1(A):
554+
a1: int
555+
556+
@frozen
557+
class B:
558+
id: int
559+
b: str
560+
561+
@frozen
562+
class Container1:
563+
id: int
564+
a: A
565+
b: B
566+
567+
@frozen
568+
class Container2:
569+
id: int
570+
c: Container1
571+
foo: str
572+
573+
def struct_hook_factory(cl, converter):
574+
struct_hook = make_dict_structure_fn(cl, converter)
575+
if not cl.__subclasses__():
576+
converter.register_structure_hook(cl, struct_hook)
577+
else:
578+
def cls_is_cl(cls, _cl=cl):
579+
return cls is _cl
580+
converter.register_structure_hook_func(cls_is_cl, struct_hook)
581+
union_strategy = partial(configure_tagged_union, tag_name="type")
582+
include_subclasses(cl, converter, union_strategy=union_strategy)
583+
return converter.get_structure_hook(cl)
584+
585+
converter = make_converter()
586+
converter.register_structure_hook_factory(has, struct_hook_factory)
587+
588+
unstructured = {
589+
"id": 0,
590+
"c": {"id": 1, "a": {"type": "A1", "a1": 42}, "b": {"id": 2, "b": "hello"}},
591+
"foo": "world",
592+
}
593+
result = converter.structure(unstructured, Container2)
594+
assert result == Container2(
595+
id=0,
596+
c=Container1(id=1, a=A1(a1=42), b=B(id=2, b="hello")),
597+
foo="world",
598+
)

0 commit comments

Comments
 (0)