dacite icon indicating copy to clipboard operation
dacite copied to clipboard

Regression in handling type_hooks for dict keys between dacite version 1.7 and 1.8

Open kkg4theweb opened this issue 2 years ago • 4 comments

Describe the bug

Type hooks don't seem to be respected for dict keys. I have been using type-hooks to map string/name of enum values of a dict to the corresponding python enum. This stopped working in dacite version 1.8 but is fine with version 1.7

To Reproduce Here is the code snippet:

import dataclasses
import dacite
import enum
import typing

@enum.unique
class FooEnum(enum.IntEnum):
  FOO = 1
  BAR = 2

type_hooks = {}
type_hooks[FooEnum] = lambda raw_str: FooEnum[raw_str]

dacite_cfg = dacite.Config(strict_unions_match=True, strict=True, type_hooks=type_hooks)

@dataclasses.dataclass(frozen=True)
class EnumKeyMapTypes:
  string_int_dict_val: typing.Dict[FooEnum, int]

dc_val = EnumKeyMapTypes(string_int_dict_val={
            FooEnum.FOO: 1,
            FooEnum.BAR: 2
        })

py_dict_val = {
        'string_int_dict_val': {
            'FOO': 1,
            'BAR': 2
        }
}

dacite.from_dict(EnumKeyMapTypes, py_dict_val, config=dacite_cfg)

Expected behavior

The dacite.from_dict should result in the following:

EnumKeyMapTypes(string_int_dict_val={<FooEnum.FOO: 1>: 1, <FooEnum.BAR: 2>: 2})

Environment

  • Python version: 3.8.10
  • dacite version: 1.8 (broken), 1.7 (working)

Additional context

This works as expected for dacite version 1.7 but is broken in version 1.8 with the following error:

WrongTypeError                            Traceback (most recent call last)
[<ipython-input-8-4d0ead28977b>](https://localhost:8080/#) in <module>
----> 1 dacite.from_dict(EnumKeyMapTypes, py_dict_val, config=dacite_cfg)

[/usr/local/lib/python3.8/dist-packages/dacite/core.py](https://localhost:8080/#) in from_dict(data_class, data, config)
     67                 raise
     68             if config.check_types and not is_instance(value, field_type):
---> 69                 raise WrongTypeError(field_path=field.name, field_type=field_type, value=value)
     70         else:
     71             try:

WrongTypeError: wrong value type for field "string_int_dict_val" - should be "typing.Dict[__main__.FooEnum, int]" instead of value "{'FOO': 1, 'BAR': 2}" of type "dict"

Google colab broken (1.8): https://colab.research.google.com/drive/1BBMdjkmZow12V_1UlX1dB_jJWWjBx1cv?usp=sharing Google colab working (1.7): https://colab.research.google.com/drive/1z1-qn5ER-FHj7VdOKKlsv7ZzjXm5wekY?usp=sharing

kkg4theweb avatar Feb 05 '23 05:02 kkg4theweb

After some bug fixes since 1.7.0, now these lines was responsible for converting dicts.

https://github.com/konradhalas/dacite/blob/02ee99348d4c8354fa309b8d1f3525dafda592e6/dacite/core.py#L140-L142

Clearly, we see that from line 142, key is not applied with type_hooks in the config.

https://github.com/konradhalas/dacite/blob/02ee99348d4c8354fa309b8d1f3525dafda592e6/dacite/core.py#L142

Don't know if the author was intended, but a quick fix would be direct, as chaning line 141-142 to

        types = extract_generic(collection, defaults=(Any, Any))
        return data_type(
            (
                _build_value(type_=types[0], data=key, config=config),
                _build_value(type_=types[1], data=value, config=config),
            )
            for key, value in data.items()
        )

sunmy2019 avatar Mar 19 '23 17:03 sunmy2019

Below is probably a duplicate from the above issue, just wanted to make sure. Specifically, using cast=[...] to configure your dacite config instead of the type_hooks parameter also causes an issue on 1.8.0 (I have knowledge of it working on 1.6.0, didn't test 1.7.0)

from enum import Enum
from typing import Dict

from dacite import from_dict, Config


class Foo(Enum):
    EVALUE = 'enum_value'


@dataclass
class FooConfig:
    foo: Dict[Foo, str]


data = {"foo": {"enum_value": "bar"}}
# next line throws error in 1.8.0, doesn't in 1.6.0
config = from_dict(data_class=FooConfig, data=data, config=Config(cast=[Foo]))

joachim-j avatar Apr 04 '23 10:04 joachim-j

After some bug fixes since 1.7.0, now these lines was responsible for converting dicts.

https://github.com/konradhalas/dacite/blob/02ee99348d4c8354fa309b8d1f3525dafda592e6/dacite/core.py#L140-L142

Clearly, we see that from line 142, key is not applied with type_hooks in the config.

https://github.com/konradhalas/dacite/blob/02ee99348d4c8354fa309b8d1f3525dafda592e6/dacite/core.py#L142

Don't know if the author was intended, but a quick fix would be direct, as chaning line 141-142 to

        types = extract_generic(collection, defaults=(Any, Any))
        return data_type(
            (
                _build_value(type_=types[0], data=key, config=config),
                _build_value(type_=types[1], data=value, config=config),
            )
            for key, value in data.items()
        )

Thanks for the details! The issue seems to stem from something else, though, as checking as back as v1.6.0, the key was never being passed through _build_value. We'll investigate it some more!

mciszczon avatar May 12 '23 08:05 mciszczon

Thanks for the details! The issue seems to stem from something else, though, as checking as back as v1.6.0, the key was never being passed through _build_value. We'll investigate it some more!

@mciszczon Here's some investigation for you. In 1.7.0, the code path was as follows:

  • from_dict() is called to deserialize the dict.
  • In from_dict(), transform_value() is called on field_data for the field in question. https://github.com/konradhalas/dacite/blob/85c845079785169677e46b0f1e4e49ab884a376b/dacite/core.py#L65-L69
  • In transform_value(), target_type is a generic collection subclassing dict, so this code transforms both the key and the item of value.items(). https://github.com/konradhalas/dacite/blob/85c845079785169677e46b0f1e4e49ab884a376b/dacite/types.py#L43-L49

So the bug in _build_value_for_collection(), where the key wasn't being transformed, never actually manifested because the same thing was being done (without the bug) earlier in the process.

In 1.8.1, transform_value() was removed and its functionality was moved to _build_value(). The key transformation was overlooked, and this bug was introduced.

object-Object avatar Jun 25 '23 15:06 object-Object