cattrs
cattrs copied to clipboard
RecursionError on recursive nested class
- cattrs version: 23.1.2
- Python version: 3.11
- Operating System: Debian bookworm
Description
Hi, I've RecursionError raised when structuring nested class.
What I Did
Here's a minimal code triggering the issue:
from __future__ import annotations
import attrs
import cattrs
@attrs.frozen
class Source:
child: dict[str, Source]
cattrs.structure({"child": {}}, Source)
And part of the traceback:
Traceback (most recent call last):
File "t.py", line 12, in <module>
cattrs.structure({"child": {}}, Source)
[...]
File "lib/python3.11/site-packages/cattrs/converters.py", line 1043, in gen_structure_mapping
h = make_mapping_structure_fn(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/gen/__init__.py", line 679, in make_mapping_structure_fn
val_handler = converter._structure_func.dispatch(val_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 48, in _dispatch
res = self._function_dispatch.dispatch(typ)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 133, in dispatch
return handler(typ)
^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/converters.py", line 985, in gen_structure_attrs_fromdict
h = make_dict_structure_fn(
^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/gen/__init__.py", line 311, in make_dict_structure_fn
handler = find_structure_handler(
^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/gen/_shared.py", line 47, in find_structure_handler
handler = c._structure_func.dispatch(type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 48, in _dispatch
res = self._function_dispatch.dispatch(typ)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 133, in dispatch
return handler(typ)
^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/converters.py", line 1043, in gen_structure_mapping
h = make_mapping_structure_fn(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/gen/__init__.py", line 679, in make_mapping_structure_fn
val_handler = converter._structure_func.dispatch(val_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 48, in _dispatch
res = self._function_dispatch.dispatch(typ)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/dispatch.py", line 133, in dispatch
return handler(typ)
^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/converters.py", line 985, in gen_structure_attrs_fromdict
h = make_dict_structure_fn(
^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/gen/__init__.py", line 302, in make_dict_structure_fn
t = deep_copy_with(t, mapping)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/_generics.py", line 14, in deep_copy_with
tuple(
File "lib/python3.11/site-packages/cattrs/_generics.py", line 17, in <genexpr>
else (deep_copy_with(a, mapping) if is_generic(a) else a)
^^^^^^^^^^^^^
File "lib/python3.11/site-packages/cattrs/_compat.py", line 497, in is_generic
or is_subclass(obj, Generic)
^^^^^^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded
#201 and #299 are related. I think a workaround would be using ForwardRef
or a NewType
hook.
We need to improve make_dict_structure_fn
to take into account classes we're already in the process of generating (or just catch the RecursionError, but that's less efficient). #299 is just because we don't support typing.Self
I think.
Hi,
Does a workaround / solution exist for this yet?
I think a workaround would be using ForwardRef or a NewType hook.
Is there an example of how to do this anywhere?
I ended up coming up with this workaround:
@attrs.define
class Foo:
nested_dict: dict[str, Foo] = attrs.field(factory=dict)
converter = cattrs.Converter()
# Overriding the deserialization of this type is necessary to prevent an infinite recursion bug:
# https://github.com/python-attrs/cattrs/issues/409
def structure_nested_dict(
nested_dict: dict[str, Any], _: type[dict[str, Foo]]
) -> dict[str, Foo]:
return {key: converter.structure(val, Foo) for key, val in nested_dict.items()}
converter.register_structure_hook(
Foo,
cattrs.gen.make_dict_structure_fn(
Foo,
converter,
nested_dict=cattrs.gen.override(struct_hook=structure_nested_dict),
),
)