Skip to content

[BUG] .from_dict fails with from __future__ import annotations and dataclass definition within the same function #548

@orilg

Description

@orilg

Description

I've encountered this issue when writing tests for my project with pytest.
The test is contained in one function and I've added a file to test the behavior of from __future__ import annotations
All my nested dataclass_json field tests failed (when a field of one dataclass_json is another dataclass_json) on .from_dict

for the example below I've got this traceback :

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 17
     15     class_with_inner = MyClass(InnerClass('asdf', 2))
     16     return MyClass.from_dict(class_with_inner.to_dict())
---> 17 print(do_something())

Cell In[1], line 16, in do_something()
     13     my_inner_inst: InnerClass
     15 class_with_inner = MyClass(InnerClass('asdf', 2))
---> 16 return MyClass.from_dict(class_with_inner.to_dict())

File /data/.venv-temp/lib/python3.11/site-packages/dataclasses_json/api.py:70, in DataClassJsonMixin.from_dict(cls, kvs, infer_missing)
     65 @classmethod
     66 def from_dict(cls: Type[A],
     67               kvs: Json,
     68               *,
     69               infer_missing=False) -> A:
---> 70     return _decode_dataclass(cls, kvs, infer_missing)

File /data/.venv-temp/lib/python3.11/site-packages/dataclasses_json/core.py:178, in _decode_dataclass(cls, kvs, infer_missing)
    175 kvs = _handle_undefined_parameters_safe(cls, kvs, usage="from")
    177 init_kwargs = {}
--> 178 types = get_type_hints(cls)
    179 for field in fields(cls):
    180     # The field should be skipped from being added
    181     # to init_kwargs as it's not intended as a constructor argument.
    182     if not field.init:

File /usr/lib/python3.11/typing.py:2336, in get_type_hints(obj, globalns, localns, include_extras)
   2334         if isinstance(value, str):
   2335             value = ForwardRef(value, is_argument=False, is_class=True)
-> 2336         value = _eval_type(value, base_globals, base_locals)
   2337         hints[name] = value
   2338 return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}

File /usr/lib/python3.11/typing.py:371, in _eval_type(t, globalns, localns, recursive_guard)
    364 """Evaluate all forward references in the given type t.
    365
    366 For use of globalns and localns see the docstring for get_type_hints().
    367 recursive_guard is used to prevent infinite recursion with a recursive
    368 ForwardRef.
    369 """
    370 if isinstance(t, ForwardRef):
--> 371     return t._evaluate(globalns, localns, recursive_guard)
    372 if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)):
    373     if isinstance(t, GenericAlias):

File /usr/lib/python3.11/typing.py:877, in ForwardRef._evaluate(self, globalns, localns, recursive_guard)
    872 if self.__forward_module__ is not None:
    873     globalns = getattr(
    874         sys.modules.get(self.__forward_module__, None), '__dict__', globalns
    875     )
    876 type_ = _type_check(
--> 877     eval(self.__forward_code__, globalns, localns),
    878     "Forward references must evaluate to types.",
    879     is_argument=self.__forward_is_argument__,
    880     allow_special_forms=self.__forward_is_class__,
    881 )
    882 self.__forward_value__ = _eval_type(
    883     type_, globalns, localns, recursive_guard | {self.__forward_arg__}
    884 )
    885 self.__forward_evaluated__ = True

File <string>:1

NameError: name 'InnerClass' is not defined

Workaround : moving the inner dataclass definition out of the function code block.

Code snippet that reproduces the issue

from __future__ import annotations
from dataclasses import dataclass
from dataclasses_json import DataClassJsonMixin
  
def do_something() -> MyClass:
    @dataclass
     class InnerClass(DataClassJsonMixin):
        my_str: str
        my_int: int
  
    @dataclass
    class MyClass(DataClassJsonMixin):
        my_inner_inst: InnerClass

    class_with_inner = MyClass(InnerClass('asdf', 2))
    return MyClass.from_dict(class_with_inner.to_dict())
print(do_something())

Describe the results you expected

MyClass(my_inner_inst=InnerClass(my_str='asdf', my_int=2))

Python version you are using

3.11.4

Environment description

asttokens==2.4.1
dataclasses-json==0.6.7
decorator==5.1.1
executing==2.1.0
ipython==8.27.0
jedi==0.19.1
marshmallow==3.22.0
matplotlib-inline==0.1.7
mypy-extensions==1.0.0
packaging==24.1
parso==0.8.4
pexpect==4.9.0
prompt_toolkit==3.0.48
ptyprocess==0.7.0
pure_eval==0.2.3
Pygments==2.18.0
six==1.16.0
stack-data==0.6.3
traitlets==5.14.3
typing-inspect==0.9.0
typing_extensions==4.12.2
wcwidth==0.2.13

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions