Skip to content

Commit 62cdb58

Browse files
matmelTinche
authored andcommitted
Implement the fix
1 parent 01f230c commit 62cdb58

File tree

3 files changed

+41
-2
lines changed

3 files changed

+41
-2
lines changed

HISTORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ The third number is for emergencies when we need to start branches for older rel
1111

1212
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1313

14+
## NEXT (UNRELEASED)
15+
16+
- Fix an `AttributeError` in `cattrs` internals that could be triggered by using the `include_subclasses` strategy in a `structure_hook_factory`
17+
([#721](https://github.com/python-attrs/cattrs/issues/721), [#722](https://github.com/python-attrs/cattrs/pull/722))
18+
1419
## 26.1.0 (2026-02-18)
1520

1621
- Add the {mod}`tomllib <cattrs.preconf.tomllib>` preconf converter.

src/cattrs/strategies/_subclasses.py

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

192192
original_unstruct_hooks = {}
193193
original_struct_hooks = {}
194+
195+
original_working_set = None
196+
if hasattr(already_generating, "working_set"):
197+
original_working_set = already_generating.working_set.copy()
198+
194199
for cl in union_classes:
195200
# In the first pass, every class gets its own unstructure function according to
196201
# the overrides.
@@ -209,6 +214,9 @@ def _include_subclasses_with_union_strategy(
209214
original_unstruct_hooks[cl] = unstruct_hook
210215
original_struct_hooks[cl] = struct_hook
211216

217+
if original_working_set is not None:
218+
already_generating.working_set = original_working_set
219+
212220
# Now that's done, we can register all the hooks and generate the
213221
# union handler. The union handler needs them.
214222
final_union = Union[union_classes] # type: ignore

tests/strategies/test_include_subclasses.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,14 +540,30 @@ class Sub(Mid1, Mid2):
540540

541541

542542
def test_subclasses_in_struct_factory():
543+
544+
@frozen
545+
class SubA:
546+
id: int
547+
sub_a: str
548+
549+
@frozen
550+
class SubA1(SubA):
551+
pass
552+
543553
@frozen
544554
class A:
545555
"""Base class"""
546556

557+
s: SubA
558+
547559
@frozen
548560
class A1(A):
549561
a1: int
550562

563+
@frozen
564+
class A2(A):
565+
a2: int
566+
551567
@frozen
552568
class B:
553569
id: int
@@ -586,11 +602,21 @@ def cls_is_cl(cls, _cl=cl):
586602

587603
unstructured = {
588604
"id": 0,
589-
"c": {"id": 1, "a": {"type": "A1", "a1": 42}, "b": {"id": 2, "b": "hello"}},
605+
"c": {
606+
"id": 1,
607+
"a": {
608+
"type": "A1",
609+
"s": {"type": "SubA1", "id": 2, "sub_a": "a"},
610+
"a1": 42,
611+
},
612+
"b": {"id": 3, "b": "hello"},
613+
},
590614
"foo": "world",
591615
}
592616
res = converter.structure(unstructured, Container2)
593617

594618
assert res == Container2(
595-
id=0, c=Container1(id=1, a=A1(a1=42), b=B(id=2, b="Hello"), foo="world")
619+
id=0,
620+
c=Container1(id=1, a=A1(s=SubA1(id=2, sub_a="a"), a1=42), b=B(id=3, b="hello")),
621+
foo="world",
596622
)

0 commit comments

Comments
 (0)