Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subclass disambiguation for nested structures. #535

Open
isohedronpipeline opened this issue Apr 23, 2024 · 3 comments
Open

Subclass disambiguation for nested structures. #535

isohedronpipeline opened this issue Apr 23, 2024 · 3 comments
Labels
more-info-needed More information required. question

Comments

@isohedronpipeline
Copy link

isohedronpipeline commented Apr 23, 2024

  • cattrs version: 23.2.3
  • Python version: 3.10.12
  • Operating System: Ubuntu-22.04

I'm using attrs 23.2.0, if that matters.

Description

Hi! I am having trouble adding the special _type key to the unstructured data to inform the structurer how to deal with subtypes. It seems to work for the top level structure, but not a second level structure that also has subtypes.

Any help would be appreciated :-)

What I Did

This is some example code:

from enum import Enum
from attrs import define, field
import cattr
from cattrs.strategies import configure_tagged_union, include_subclasses

from typing import ClassVar, List


class Material(Enum):
    WOOD = "wood"
    PLASTIC = "plastic"


@define
class Toy:
    material: ClassVar[Material]

    name: str


@define
class Lego(Toy):
    material = Material.PLASTIC


@define
class Train(Toy):
    material = Material.WOOD


@define
class ToyBox:
    size: ClassVar[int]

    material: Material
    contents: List[Toy] = field(factory=list)

    def add_toy(self, toy):
        if len(self.contents) >= self.size:
            raise ValueError("ToyBox is full")
        self.contents.append(toy)


@define
class SmallToyBox(ToyBox):
    size = 5


@define
class LargeToyBox(ToyBox):
    size = 10


c = cattr.Converter()
include_subclasses(ToyBox, c, union_strategy=configure_tagged_union)
include_subclasses(Toy, c, union_strategy=configure_tagged_union)


box = SmallToyBox(material=Material.WOOD)
box.add_toy(Lego("space"))
box.add_toy(Lego("house"))
box.add_toy(Train("stream"))
box.add_toy(Train("electric"))

unstructured = c.unstructure(box)

import pprint
pprint.pprint(unstructured)

And I get the following:

{'_type': 'SmallToyBox',
 'contents': [{'name': 'space'},
              {'name': 'house'},
              {'name': 'stream'},
              {'name': 'electric'}],
 'material': 'wood'}

which, as you can see, is not including the _type argument in the nested data structure Toy that needs to deal with subtypes.

A possibly related issue is that I want to be able to string together multiple hooks together. Specifically I want to also include this:
hook = make_dict_unstructure_fn(Toy, c, _cattrs_omit_if_default=True) in addition to supporting subclasses.

How can I do that?

Thanks!

@isohedronpipeline
Copy link
Author

Another thing I tried was:

_cattr_converter.register_unstructure_hook(
    Toy,
    lambda o: {"_type": type(o).__name__, **_cattr_converter.unstructure(o)},
)

but that caused an infinite loop.

@isohedronpipeline isohedronpipeline changed the title Union disambiguation for nested structures. Subclass disambiguation for nested structures. Apr 23, 2024
@Tinche
Copy link
Member

Tinche commented May 4, 2024

Hi,

sorry for the delayed response, I was on vacation.

These strategies are somewhat stateful since a lot of them do some of the work at configuration time, rather than structure/unstructure time. This means the order in which they are applied matters.

I tried switching the order of the hooks here:

c = cattr.Converter()
include_subclasses(Toy, c, union_strategy=configure_tagged_union)
include_subclasses(ToyBox, c, union_strategy=configure_tagged_union)

Doing this, the output now is:

{'_type': 'SmallToyBox',
 'contents': [{'_type': 'Lego', 'name': 'space'},
              {'_type': 'Lego', 'name': 'house'},
              {'_type': 'Train', 'name': 'stream'},
              {'_type': 'Train', 'name': 'electric'}],
 'material': 'wood'}

That that look like what you were going for?

@Tinche Tinche added question more-info-needed More information required. labels May 4, 2024
@isohedronpipeline
Copy link
Author

isohedronpipeline commented May 4, 2024

Yes! Thank you!

I hope you had a good holiday :-)

Can you explain why/how it works by registering Toy prior to ToyBox? It seems a little magical and I'd love to know how that works in case I encounter a simlar issue in the future.

Also, how might I include the hook = make_dict_unstructure_fn(Toy, c, _cattrs_omit_if_default=True) logic in addition to the subclass logic? I can put together some example code of what I'm looking for if you want. Let me know if you'd prefer that to be a separate issue too.

Thanks again, Tin!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
more-info-needed More information required. question
Projects
None yet
Development

No branches or pull requests

2 participants