|
6 | 6 | from typing import Any |
7 | 7 |
|
8 | 8 | import pytest |
9 | | -from attrs import define |
| 9 | +from attrs import define, frozen, has |
10 | 10 |
|
11 | 11 | from cattrs import Converter, override |
12 | 12 | from cattrs.errors import ClassValidationError, StructureHandlerNotFoundError |
| 13 | +from cattrs.gen import make_dict_structure_fn |
13 | 14 | from cattrs.strategies import configure_tagged_union, include_subclasses |
14 | 15 |
|
15 | 16 | from .._compat import is_py311_plus |
@@ -536,3 +537,91 @@ class Sub(Mid1, Mid2): |
536 | 537 | assert genconverter.structure({"_type": "Sub"}, Base) == Sub() |
537 | 538 | assert genconverter.structure({"_type": "Mid1"}, Base) == Mid1() |
538 | 539 | assert genconverter.structure({"_type": "Mid2"}, Base) == Mid2() |
| 540 | + |
| 541 | + |
| 542 | +def test_subclasses_in_struct_factory(): |
| 543 | + """ |
| 544 | + Check the structuring does not fail with an attribute error when include_subclasses |
| 545 | + is called within a structure_hook_factory on a complex class tree involving |
| 546 | + subclasses several levels deep (#721) |
| 547 | + """ |
| 548 | + |
| 549 | + @frozen |
| 550 | + class SubA: |
| 551 | + id: int |
| 552 | + sub_a: str |
| 553 | + |
| 554 | + @frozen |
| 555 | + class SubA1(SubA): |
| 556 | + pass |
| 557 | + |
| 558 | + @frozen |
| 559 | + class A: |
| 560 | + """Base class""" |
| 561 | + |
| 562 | + s: SubA |
| 563 | + |
| 564 | + @frozen |
| 565 | + class A1(A): |
| 566 | + a1: int |
| 567 | + |
| 568 | + @frozen |
| 569 | + class A2(A): |
| 570 | + a2: int |
| 571 | + |
| 572 | + @frozen |
| 573 | + class B: |
| 574 | + id: int |
| 575 | + b: str |
| 576 | + |
| 577 | + @frozen |
| 578 | + class Container1: |
| 579 | + id: int |
| 580 | + a: A |
| 581 | + b: B |
| 582 | + |
| 583 | + @frozen |
| 584 | + class Container2: |
| 585 | + id: int |
| 586 | + c: Container1 |
| 587 | + foo: str |
| 588 | + |
| 589 | + def struct_hook_factory(cl, converter: Converter): |
| 590 | + struct_hook = make_dict_structure_fn(cl, converter) |
| 591 | + if not cl.__subclasses__(): |
| 592 | + converter.register_structure_hook(cl, struct_hook) |
| 593 | + |
| 594 | + else: |
| 595 | + |
| 596 | + def cls_is_cl(cls, _cl=cl): |
| 597 | + return cls is _cl |
| 598 | + |
| 599 | + converter.register_structure_hook_func(cls_is_cl, struct_hook) |
| 600 | + union_strategy = partial(configure_tagged_union, tag_name="type") |
| 601 | + include_subclasses(cl, converter, union_strategy=union_strategy) |
| 602 | + |
| 603 | + return converter.get_structure_hook(cl) |
| 604 | + |
| 605 | + converter = Converter() |
| 606 | + converter.register_structure_hook_factory(has, struct_hook_factory) |
| 607 | + |
| 608 | + unstructured = { |
| 609 | + "id": 0, |
| 610 | + "c": { |
| 611 | + "id": 1, |
| 612 | + "a": { |
| 613 | + "type": "A1", |
| 614 | + "s": {"type": "SubA1", "id": 2, "sub_a": "a"}, |
| 615 | + "a1": 42, |
| 616 | + }, |
| 617 | + "b": {"id": 3, "b": "hello"}, |
| 618 | + }, |
| 619 | + "foo": "world", |
| 620 | + } |
| 621 | + res = converter.structure(unstructured, Container2) |
| 622 | + |
| 623 | + assert res == Container2( |
| 624 | + id=0, |
| 625 | + c=Container1(id=1, a=A1(s=SubA1(id=2, sub_a="a"), a1=42), b=B(id=3, b="hello")), |
| 626 | + foo="world", |
| 627 | + ) |
0 commit comments