Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/cattrs/strategies/_subclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ def _include_subclasses_with_union_strategy(

original_unstruct_hooks = {}
original_struct_hooks = {}

# Save the existing working_set so we can restore it after the loop.
# include_subclasses may be called while make_dict_structure_fn is
# already generating hooks for outer classes (via a hook factory),
# so we must not clobber the outer working_set.
_had_working_set = hasattr(already_generating, "working_set")
_prev_working_set = getattr(already_generating, "working_set", None)

for cl in union_classes:
# In the first pass, every class gets its own unstructure function according to
# the overrides.
Expand All @@ -209,6 +217,12 @@ def _include_subclasses_with_union_strategy(
original_unstruct_hooks[cl] = unstruct_hook
original_struct_hooks[cl] = struct_hook

# Restore the previous working_set state.
if _had_working_set:
already_generating.working_set = _prev_working_set
elif hasattr(already_generating, "working_set"):
del already_generating.working_set

# Now that's done, we can register all the hooks and generate the
# union handler. The union handler needs them.
final_union = Union[union_classes] # type: ignore
Expand Down
60 changes: 60 additions & 0 deletions tests/strategies/test_include_subclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,63 @@ class Sub(Mid1, Mid2):
assert genconverter.structure({"_type": "Sub"}, Base) == Sub()
assert genconverter.structure({"_type": "Mid1"}, Base) == Mid1()
assert genconverter.structure({"_type": "Mid2"}, Base) == Mid2()


def test_include_subclasses_in_hook_factory():
"""include_subclasses called from within a structure hook factory should
not clobber the outer already_generating working_set (#721)."""
from attrs import frozen, has
from cattrs.gen import make_dict_structure_fn
from cattrs.preconf.json import make_converter

@frozen
class A:
pass

@frozen
class A1(A):
a1: int

@frozen
class B:
id: int
b: str

@frozen
class Container1:
id: int
a: A
b: B

@frozen
class Container2:
id: int
c: Container1
foo: str

def struct_hook_factory(cl, converter):
struct_hook = make_dict_structure_fn(cl, converter)
if not cl.__subclasses__():
converter.register_structure_hook(cl, struct_hook)
else:
def cls_is_cl(cls, _cl=cl):
return cls is _cl
converter.register_structure_hook_func(cls_is_cl, struct_hook)
union_strategy = partial(configure_tagged_union, tag_name="type")
include_subclasses(cl, converter, union_strategy=union_strategy)
return converter.get_structure_hook(cl)

converter = make_converter()
converter.register_structure_hook_factory(has, struct_hook_factory)

unstructured = {
"id": 0,
"c": {"id": 1, "a": {"type": "A1", "a1": 42}, "b": {"id": 2, "b": "hello"}},
"foo": "world",
}
result = converter.structure(unstructured, Container2)
assert result == Container2(
id=0,
c=Container1(id=1, a=A1(a1=42), b=B(id=2, b="hello")),
foo="world",
)
Loading