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

Hooks for fields of attrs classes are being ignored when defining hook for class itself #560

Closed
fortysix2ahead opened this issue Jul 30, 2024 · 2 comments

Comments

@fortysix2ahead
Copy link

  • cattrs version: 23.2.3
  • Python version: 3.10
  • Operating System: MacOS

Description

Maybe this is not a bug, but expected behaviour, but at least I feel it's confusing. Consider the following code:

from datetime import timedelta
from pprint import pprint

from attrs import define, field
from cattrs.gen import make_dict_unstructure_fn, override
from cattrs.preconf.json import make_converter

@define
class ClassOne:

	id: int = field( default=None )
	internal_id: int = field( default=None )
	time: timedelta = field( default=None )

def timedelta_to_str( obj: timedelta ) -> str:
	return f'{obj.seconds} sec'

def test_case_1():
	converter = make_converter()
	converter.register_unstructure_hook( timedelta, timedelta_to_str )

	c = ClassOne( 10, 100, timedelta( hours=2 ) )
	return converter.unstructure( c )

def test_case_2():
	converter = make_converter()
	hook = make_dict_unstructure_fn( ClassOne, converter, _cattrs_omit_if_default=True, internal_id=override( omit=True ) )
	converter.register_unstructure_hook( timedelta, timedelta_to_str )
	converter.register_unstructure_hook( ClassOne, hook )

	c = ClassOne( 10, 100, timedelta( hours=2 ) )
	return converter.unstructure( c )

if __name__ == '__main__':
	# result OK: {'id': 10, 'internal_id': 100, 'time': '7200 sec'}
	pprint( test_case_1() )

	# result: {'id': 10, 'time': datetime.timedelta(seconds=7200)}
	# result expected: {'id': 10, 'time': '7200 sec'}
	pprint( test_case_2() )

In test case 1 I create an unstructure hook to manipulate how a timedelta is serialized. This works fine. In the second case I define a hook for ClassOne, because I don't want internal_id to appear in the result. This works also fine, but the hook for the time field now is being ignored. I know that I can do this when creating hook by using time = override( unstruct_hook = ...), but that's going to be tedious if I had 20 different time fields. But I lack some understanding when and how hooks are evaluated?

@Tinche
Copy link
Member

Tinche commented Aug 1, 2024

Hello. I can shed some light on what's happening.

The short answer: you should register the timedelta hook before creating the unstructure hook for ClassOne.

The long answer: to me, it seems like your mental model of the hook being generated for ClassOne would look (logically, and ignoring _cattrs_omit_if_default and overrides) like this:

def hook(inst):
    return {
        "id": unstructure(inst.id),
        "internal_id": unstructure(inst.internal_id),
        "time": unstructure(inst.timedelta)
}

But this isn't quite what's happening. Instead, make_dict_unstructure_fn looks (logically) like this:

id_hook = converter.get_unstructure_hook(int)
internal_id_hook = converter.get_unstructure_hook(int)
time_hook = converter.get_unstructure_hook(timedelta)

def hook(inst):
    return {
        "id": id_hook(inst.id),
        "internal_id": internal_id_hook(inst.internal_id),
        "time": time_hook(inst.timedelta)
    }

return hook

So at the time you call make_dict_unstructure_fn( ClassOne, converter, ...), the hook for timedelta is still the default one, and this is what you see in the payload.

Why is this the default behavior? Because it's much, much faster to do the hook dispatch once and early rather than on every unstructure (even though the converter has a cache) and in practice controlling the order in which hooks are registered isn't too hard. I agree it can be confusing if you don't understand what's going on.

Hope this helps!

@salotz
Copy link

salotz commented Aug 6, 2024

Should be a pinned post, really helped me.

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

3 participants