python-attrs/cattrs

Using `register_unstructure_hook` to perform attributes renaming

NicolasGensollen opened this issue · 2 comments

  • cattrs version: 23.2.3
  • Python version: 3.11.8
  • Operating System: MacOS 14.5

Hi there,

I am trying to use register_unstructure_hook to perform some renaming of attributes.
In the following MWE, I was expecting to have all keys being capitalized, not just the ones of the Blog class, i.e. I was expecting to get:

{
    "NAME": "my awesome blog",
    "ARTICLES": [
        {
        "TITLE": "article 1",
        "AUTHOR": "John Doe"
    },
    {
        "TITLE": "article 2",
        "AUTHOR": "Someone"
    }
    ]
}

This seems like a common use case, so I'm pretty sure I'm doing something wrong here.

MWE:

import json
from typing import List, MutableSequence
from attrs import define, fields
from cattr.gen import make_dict_unstructure_fn, override
from cattr.preconf.json import make_converter

@define
class Article:
    title: str
    author: str

@define
class Blog:
    name: str
    articles: MutableSequence[Article]

article1 = Article("article 1", "John Doe")
article2 = Article("article 2", "Someone")
blog = Blog("my awesome blog", [article1, article2])

def _convert_to_upper(name: str) -> str:
    return name.upper()

converter = make_converter()

article_renaming = {a.name: override(rename=_convert_to_upper(a.name)) for a in fields(Article)}
blog_renaming = {a.name: override(rename=_convert_to_upper(a.name)) for a in fields(Blog)}

article_renaming_hook = make_dict_unstructure_fn(Article, converter, **article_renaming)
blog_renaming_hook = make_dict_unstructure_fn(Blog, converter, **blog_renaming)

converter.register_unstructure_hook(Article, article_renaming_hook)
converter.register_unstructure_hook(Blog, blog_renaming_hook)

# This works as expected
print(json.dumps(converter.unstructure([article1, article2]), indent=4))
print("-"*60)
# But this doesn't (the keys for articles aren't uppercased)
print(json.dumps(converter.unstructure(blog), indent=4))
[
    {
        "TITLE": "article 1",
        "AUTHOR": "John Doe"
    },
    {
        "TITLE": "article 2",
        "AUTHOR": "Someone"
    }
]
------------------------------------------------------------
{
    "NAME": "my awesome blog",
    "ARTICLES": [
        {
            "title": "article 1",
            "author": "John Doe"
        },
        {
            "title": "article 2",
            "author": "Someone"
        }
    ]
}

Thanks !

Hello! I'm on paternity leave so I'm not super available, sorry.

You may want to read my comment on a similar issue here: #560 (comment)

You need to generate the hook for Article and register it before you generate the hook for Blog, because the hook for Blog will request the hook for Article (and store it) at generation time.

Guess we really need better docs around this.

Hi @Tinche

Thanks for the quick reply ! 🚀 (especially while on paternity leave)
Indeed, generating the hook for Article and registering it before the one for Blog solved the issue.
I agree that having a short section on this somewhere in the docs with a small example could be useful.
The behavior makes sense when you know what's happening under the hood, but it's pretty confusing when you're just trying things out with a limited understanding.