from dataclasses import dataclass
from typing import Optional, Union
import dacite
class A(int):
pass
class B(str):
pass
@dataclass
class C:
a_or_b: Optional[Union[A, B]]
obj_from_dict = dacite.from_dict(
data_class=C,
data={"a_or_b": "Hello world!"},
config=dacite.Config(cast=[A, B]),
)
assert obj_from_dict == C(a_or_b=B("Hello world!"))
Issue
I would expect this to work flawlessly, since I included both A and B in cast to turn int to A and str to B so that the types match. Unfortunately this is not the case, instead I get
Traceback (most recent call last):
File "problem.py", line 15, in <module>
obj_from_dict = dacite.from_dict(
File "[...]/lib/python3.8/site-packages/dacite/core.py", line 60, in from_dict
transformed_value = transform_value(
File "[...]/lib/python3.8/site-packages/dacite/types.py", line 24, in transform_value
return transform_value(type_hooks, cast, target_type, value)
File "[...]/lib/python3.8/site-packages/dacite/types.py", line 18, in transform_value
value = target_type(value)
ValueError: invalid literal for int() with base 10: 'Hello world!'
If we inspect this more closely, we first enter from_dict and then transform_value where the target type is Union[A, B, None]. Since the target type is correctly identified as optional by is_optional and since the value is not None (but "Hello world!"), transform_value is again called. This time the target type is extract_optional(Union[A, B, None]). It should be Union[A, B], but in the current implementation it isA. This causes the program to incorrectly cast "Hello World!" to A and the program crashes.
Solution
def extract_optional(optional: Type[Optional[T]]) -> T:
for type_ in extract_generic(optional):
if type_ is not type(None):
return type_
raise ValueError("can not find not-none value")
should be changed to something like
def extract_optional(optional: Type[Optional[T]]) -> T:
other_members = [member for member in extract_generic(optional) if member is not type(None)]
if not other_members:
raise ValueError("can not find not-none value")
else:
return Union[tuple(other_members)]
This way extract_optional(Union[A, B, None]) == Union[A, B] and no incorrect casting happens. The Union is then properly handled by _build_value_for_union. Note that, if other_members contains only a single type, say A, then Union[A] == A.
Basically, this is the same problem reported in #26 for is_optional, but this time for extract_optional. It is also probably related to #161. This issues does not appear without including the types inside the union in type_hooks or cast, because then the incorrect call to transform_values does not actually do anything.
Example to reproduce faulty behavior:
Issue
I would expect this to work flawlessly, since I included both
A
andB
in cast to turnint
toA
andstr
toB
so that the types match. Unfortunately this is not the case, instead I getIf we inspect this more closely, we first enter
from_dict
and thentransform_value
where the target type isUnion[A, B, None]
. Since the target type is correctly identified as optional byis_optional
and since the value is not None (but"Hello world!"
),transform_value
is again called. This time the target type isextract_optional(Union[A, B, None])
. It should beUnion[A, B]
, but in the current implementation it isA
. This causes the program to incorrectly cast"Hello World!"
toA
and the program crashes.Solution
should be changed to something like
This way
extract_optional(Union[A, B, None]) == Union[A, B]
and no incorrect casting happens. The Union is then properly handled by_build_value_for_union
. Note that, ifother_members
contains only a single type, sayA
, thenUnion[A] == A
.Basically, this is the same problem reported in #26 for
is_optional
, but this time forextract_optional
. It is also probably related to #161. This issues does not appear without including the types inside the union intype_hooks
orcast
, because then the incorrect call totransform_values
does not actually do anything.