Fields of type Optional[Union[A, B]] not working with cast
Example to reproduce faulty behavior:
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 is A. 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.
I just rushed into this issue. Any updates on it?