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

RecursionError on recursive nested class #409

Open
philpep opened this issue Aug 7, 2023 · 4 comments
Open

RecursionError on recursive nested class #409

philpep opened this issue Aug 7, 2023 · 4 comments
Labels

Comments

@philpep
Copy link

philpep commented Aug 7, 2023

  • 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
@PIG208
Copy link
Contributor

PIG208 commented Aug 7, 2023

#201 and #299 are related. I think a workaround would be using ForwardRef or a NewType hook.

@Tinche Tinche added the bug label Aug 7, 2023
@Tinche
Copy link
Member

Tinche commented Aug 7, 2023

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.

@gpalmer-latai
Copy link

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?

@gpalmer-latai
Copy link

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),
        ),
    )

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

No branches or pull requests

4 participants