Skip to content

Hookable attrs unstructuring #558

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

Closed
salotz opened this issue Jul 28, 2024 · 4 comments
Closed

Hookable attrs unstructuring #558

salotz opened this issue Jul 28, 2024 · 4 comments

Comments

@salotz
Copy link

salotz commented Jul 28, 2024

I am looking to unstructure attrs classes to immutables.Map instead of dicts.

What would be the best way to do this?

That is other than rewriting the entire hook factories :)

@Tinche
Copy link
Member

Tinche commented Jul 30, 2024

We don't need to rewrite the hook factory, we just need to wrap it ;)

from attrs import has
from immutables import Map

from cattrs import Converter
from cattrs.gen import make_dict_unstructure_fn

c = Converter()


def my_attrs_hook_factory(cls):
    base_hook = make_dict_unstructure_fn(cls, c)

    def my_hook(instance):
        return Map(base_hook(instance))

    return my_hook


c.register_unstructure_hook_factory(has, my_attrs_hook_factory)

Let me know if you have any more questions!

@Tinche Tinche closed this as completed Jul 30, 2024
@salotz
Copy link
Author

salotz commented Jul 30, 2024

Great, thank you!

cattrs is awesome, but definitely sometimes finding the right pattern can be a challenge.

I can actually use this in other scenarios, like unconditionally injecting type tags.

@salotz
Copy link
Author

salotz commented Jul 31, 2024

Here is my type tagging factory example. Thankfully nothing fancy.

import cattrs
import attrs

conv = cattrs.Converter()

def tag_attrs_hook_factory(cl):

    base_hook = cattrs.gen.make_dict_unstructure_fn(cl, conv)

    def hook(instance):

        unstruct = base_hook(instance)
        unstruct["_type"] = type(instance).__name__

        return unstruct
    return hook


conv.register_unstructure_hook_factory(attrs.has, tag_attrs_hook_factory)

@attrs.define
class Thing:
    a: int

assert conv.unstructure(Thing(1)) == {
    "_type" : "Thing",
    "a" : 1,
}

@salotz
Copy link
Author

salotz commented Aug 1, 2024

@Tinche Adding to the type tagging example above, custom metamethods. The problem is that the metamethods and tag factory function cannot mix.

import cattrs
from cattrs.strategies import use_class_methods
import attrs

# Use a set of metamethods
@attrs.define
class Thing:
    a: int

    def _unstructure(self):
        return {"a" : str(self.a)}

    @classmethod
    def _structure(cls, val):
        return cls(a=int(val["a"]))

conv = cattrs.Converter()
use_class_methods(
    conv,
    "_structure",
    "_unstructure",
)

assert conv.unstructure(Thing(1)) == {
    "a" : "1",
}


def tag_attrs_hook_factory(cl):

    base_hook = cattrs.gen.make_dict_unstructure_fn(cl, conv)

    def hook(instance):

        unstruct = base_hook(instance)
        unstruct["_type"] = type(instance).__name__

        return unstruct
    return hook


tagging_conv_a = cattrs.Converter()
tagging_conv_a.register_unstructure_hook_factory(attrs.has, tag_attrs_hook_factory)
use_class_methods(
    tagging_conv_a,
    "_structure",
    "_unstructure",
)

# THIS IS WRONG. Does not have the tag
assert tagging_conv_a.unstructure(Thing(1)) == {
    "a" : "1",
}

tagging_conv_b = cattrs.Converter()
use_class_methods(
    tagging_conv_b,
    "_structure",
    "_unstructure",
)
tagging_conv_b.register_unstructure_hook_factory(attrs.has, tag_attrs_hook_factory)

# THIS IS WRONG. Does not convert the sub value correctly via the metamethod
assert tagging_conv_b.unstructure(Thing(1)) == {
    "_type" : "Thing",
    "a" : 1,
}

What I tried initially was to just inject the converter so it dispatches to the metamethods properly. But this causes infinite recursion, e.g.:

def tag_attrs_hook_factory(cl):

    converter = ... # via closure

    def hook(instance, converter):

        unstruct = converter.unstructure(instance)
        unstruct["_type"] = type(instance).__name__

        return unstruct
    return hook

Any ideas? I was thinking that the make_dict_unstructure_fn would honor the metamethods, but it doesn't seem that is the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants