python-attrs / python-attrs/cattrs

`make_unstructure_dict_unstructure_fn` does not honor `use_class_methods`

Open
#566 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.1k
Forks
159
Avg merge
12h 21m
Merged PRs (30d)
6

Description

From comments in #558. Custom metamethods and make_unstructure_dict_unstructure_fn cannot be used together:

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

I think that make_dict_unstructure_fn and friends should honor the metamethods for the passed in converter and break recursion. I'm not sure if this makes sense though... In any case this is a difficult case to handle generally.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the two tagging examples and inspect make_dict_unstructure_fn, make_unstructure_dict_unstructure_fn, and use_class_methods to trace hook dispatch and recursion. Done means the tagging hook preserves its _type field while nested values still use the configured metamethods, without infinite recursion.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.