Repository: konradhalas/dacite

Issue:
Cannot cast sets
There is a problem with casting sets/frozensets, for which is quite common that lists are used for json when serializing.

This fails in dacite as iterables are casted before inner values are transformed.

An example of failing code:
```
from dataclasses import dataclass
from typing import Set

from dacite import Config, from_dict


@dataclass(frozen=True)
class A:
    x: int


@dataclass
class SetOfA:
    set_a: Set[A]


from_dict(data_class=SetOfA, data={"set_a": [{"x": 1}]}, config=Config(cast=[set]))
```

which fails with:
```
TypeError: unhashable type: 'dict'
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbb47dd..9a7ef6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-Nothing yet.
+### Fixed
+
+- Fix set's casting
 
 ## [1.7.0] - 2022-12-22
 
diff --git a/dacite/core.py b/dacite/core.py
index a808304..bdc6a9d 100644
--- a/dacite/core.py
+++ b/dacite/core.py
@@ -1,7 +1,7 @@
 import copy
 from dataclasses import is_dataclass
 from itertools import zip_longest
-from typing import TypeVar, Type, Optional, get_type_hints, Mapping, Any
+from typing import TypeVar, Type, Optional, get_type_hints, Mapping, Any, Collection
 
 from dacite.config import Config
 from dacite.data import Data
@@ -28,10 +28,10 @@ from dacite.types import (
     is_union,
     extract_generic,
     is_optional,
-    transform_value,
     extract_origin_collection,
     is_init_var,
     extract_init_var,
+    is_subclass,
 )
 
 T = TypeVar("T")
@@ -63,10 +63,7 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
         if field.name in data:
             try:
                 field_data = data[field.name]
-                transformed_value = transform_value(
-                    type_hooks=config.type_hooks, cast=config.cast, target_type=field.type, value=field_data
-                )
-                value = _build_value(type_=field.type, data=transformed_value, config=config)
+                value = _build_value(type_=field.type, data=field_data, config=config)
             except DaciteFieldError as error:
                 error.update_path(field.name)
                 raise
@@ -90,12 +87,23 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
 def _build_value(type_: Type, data: Any, config: Config) -> Any:
     if is_init_var(type_):
         type_ = extract_init_var(type_)
+    if type_ in config.type_hooks:
+        data = config.type_hooks[type_](data)
+    if is_optional(type_) and data is None:
+        return data
     if is_union(type_):
-        return _build_value_for_union(union=type_, data=data, config=config)
-    elif is_generic_collection(type_) and is_instance(data, extract_origin_collection(type_)):
-        return _build_value_for_collection(collection=type_, data=data, config=config)
+        data = _build_value_for_union(union=type_, data=data, config=config)
+    elif is_generic_collection(type_):
+        data = _build_value_for_collection(collection=type_, data=data, config=config)
     elif is_dataclass(type_) and is_instance(data, Data):
-        return from_dict(data_class=type_, data=data, config=config)
+        data = from_dict(data_class=type_, data=data, config=config)
+    for cast_type in config.cast:
+        if is_subclass(type_, cast_type):
+            if is_generic_collection(type_):
+                data = extract_origin_collection(type_)(data)
+            else:
+                data = type_(data)
+            break
     return data
 
 
@@ -108,12 +116,9 @@ def _build_value_for_union(union: Type, data: Any, config: Config) -> Any:
         try:
             # noinspection PyBroadException
             try:
-                data = transform_value(
-                    type_hooks=config.type_hooks, cast=config.cast, target_type=inner_type, value=data
-                )
+                value = _build_value(type_=inner_type, data=data, config=config)
             except Exception:  # pylint: disable=broad-except
                 continue
-            value = _build_value(type_=inner_type, data=data, config=config)
             if is_instance(value, inner_type):
                 if config.strict_unions_match:
                     union_matches[inner_type] = value
@@ -132,10 +137,10 @@ def _build_value_for_union(union: Type, data: Any, config: Config) -> Any:
 
 def _build_value_for_collection(collection: Type, data: Any, config: Config) -> Any:
     data_type = data.__class__
-    if is_instance(data, Mapping):
+    if is_instance(data, Mapping) and is_subclass(collection, Mapping):
         item_type = extract_generic(collection, defaults=(Any, Any))[1]
         return data_type((key, _build_value(type_=item_type, data=value, config=config)) for key, value in data.items())
-    elif is_instance(data, tuple):
+    elif is_instance(data, tuple) and is_subclass(collection, tuple):
         if not data:
             return data_type()
         types = extract_generic(collection)
@@ -144,5 +149,7 @@ def _build_value_for_collection(collection: Type, data: Any, config: Config) ->
         return data_type(
             _build_value(type_=type_, data=item, config=config) for item, type_ in zip_longest(data, types)
         )
-    item_type = extract_generic(collection, defaults=(Any,))[0]
-    return data_type(_build_value(type_=item_type, data=item, config=config) for item in data)
+    elif is_instance(data, Collection) and is_subclass(collection, Collection):
+        item_type = extract_generic(collection, defaults=(Any,))[0]
+        return data_type(_build_value(type_=item_type, data=item, config=config) for item in data)
+    return data
diff --git a/dacite/types.py b/dacite/types.py
index b6a3750..0b1de36 100644
--- a/dacite/types.py
+++ b/dacite/types.py
@@ -6,10 +6,7 @@ from typing import (
     Union,
     Collection,
     TypeVar,
-    Dict,
-    Callable,
     Mapping,
-    List,
     Tuple,
     cast as typing_cast,
 )
@@ -17,41 +14,6 @@ from typing import (
 T = TypeVar("T", bound=Any)
 
 
-def transform_value(
-    type_hooks: Dict[Type, Callable[[Any], Any]], cast: List[Type], target_type: Type, value: Any
-) -> Any:
-    if is_init_var(target_type):
-        target_type = extract_init_var(target_type)
-    if target_type in type_hooks:
-        value = type_hooks[target_type](value)
-    else:
-        for cast_type in cast:
-            if is_subclass(target_type, cast_type):
-                if is_generic_collection(target_type):
-                    value = extract_origin_collection(target_type)(value)
-                else:
-                    value = target_type(value)
-                break
-    if is_optional(target_type):
-        if value is None:
-            return None
-        target_type = extract_optional(target_type)
-        return transform_value(type_hooks, cast, target_type, value)
-    if is_generic_collection(target_type) and isinstance(value, extract_origin_collection(target_type)):
-        collection_cls = value.__class__
-        if issubclass(collection_cls, dict):
-            key_cls, item_cls = extract_generic(target_type, defaults=(Any, Any))
-            return collection_cls(
-                {
-                    transform_value(type_hooks, cast, key_cls, key): transform_value(type_hooks, cast, item_cls, item)
-                    for key, item in value.items()
-                }
-            )
-        item_cls = extract_generic(target_type, defaults=(Any,))[0]
-        return collection_cls(transform_value(type_hooks, cast, item_cls, item) for item in value)
-    return value
-
-
 def extract_origin_collection(collection: Type) -> Type:
     try:
         return collection.__extra__


Test patch:
diff --git a/tests/core/test_collection.py b/tests/core/test_collection.py
index 1aff1a8..fd337d5 100644
--- a/tests/core/test_collection.py
+++ b/tests/core/test_collection.py
@@ -90,6 +90,22 @@ def test_from_dict_with_cast_of_both_collection_and_inner_type():
     assert result == X(set_int={1, 2})
 
 
+def test_from_dict_with_set_of_dataclasses():
+    @dataclass(frozen=True)
+    class A:
+        i: int
+
+    @dataclass
+    class X:
+        set_a: Set[A]
+
+    data = {"set_a": [{"i": 1}, {"i": 2}]}
+
+    result = from_dict(data_class=X, data=data, config=Config(cast=[set]))
+
+    assert result == X(set_a={A(i=1), A(i=2)})
+
+
 def test_from_dict_with_dict():
     @dataclass
     class X:
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
index f8fec33..0422fda 100644
--- a/tests/core/test_config.py
+++ b/tests/core/test_config.py
@@ -33,6 +33,16 @@ def test_from_dict_with_type_hooks_and_optional():
     assert result == X(s="test")
 
 
+def test_from_dict_with_type_hooks_and_optional_null_value():
+    @dataclass
+    class X:
+        s: Optional[str]
+
+    result = from_dict(X, {"s": None}, Config(type_hooks={str: str.lower}))
+
+    assert result == X(s=None)
+
+
 def test_from_dict_with_type_hooks_and_union():
     @dataclass
     class X:
diff --git a/tests/test_types.py b/tests/test_types.py
index 17958f5..7b074a8 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -15,7 +15,6 @@ from dacite.types import (
     extract_generic,
     is_new_type,
     extract_new_type,
-    transform_value,
     is_literal,
     is_init_var,
     extract_init_var,
@@ -373,75 +372,6 @@ def test_extract_generic_with_defaults():
     assert extract_generic(List, defaults=(Any,)) == (Any,)
 
 
-def test_transform_value_without_matching_type():
-    assert transform_value({}, [], str, 1) == 1
-
-
-def test_transform_value_with_matching_type():
-    assert transform_value({int: lambda x: x + 1}, [], int, 1) == 2
-
-
-def test_transform_value_with_optional_and_not_none_value():
-    assert transform_value({str: str}, [], Optional[str], 1) == "1"
-
-
-def test_transform_value_with_optional_and_none_value():
-    assert transform_value({str: str}, [], Optional[str], None) is None
-
-
-def test_transform_value_with_optional_and_exact_matching_type():
-    assert transform_value({Optional[str]: str}, [], Optional[str], None) == "None"
-
-
-def test_transform_value_with_generic_sequence_and_matching_item():
-    assert transform_value({str: str}, [], List[str], [1]) == ["1"]
-
-
-def test_transform_value_with_generic_sequence_and_matching_sequence():
-    assert transform_value({List[int]: lambda x: list(reversed(x))}, [], List[int], [1, 2]) == [2, 1]
-
-
-def test_transform_value_with_generic_sequence_and_matching_both_item_and_sequence():
-    assert transform_value({List[int]: lambda x: list(reversed(x)), int: int}, [], List[int], ["1", "2"]) == [2, 1]
-
-
-def test_transform_value_without_matching_generic_sequence():
-    assert transform_value({}, [], List[int], {1}) == {1}
-
-
-def test_transform_value_with_nested_generic_sequence():
-    assert transform_value({str: str}, [], List[List[str]], [[1]]) == [["1"]]
-
-
-def test_transform_value_with_generic_abstract_collection():
-    assert transform_value({str: str}, [], Collection[str], [1]) == ["1"]
-
-
-def test_transform_value_with_generic_mapping():
-    assert transform_value({str: str, int: int}, [], Dict[str, int], {1: "2"}) == {"1": 2}
-
-
-def test_transform_value_with_nested_generic_mapping():
-    assert transform_value({str: str, int: int}, [], Dict[str, Dict[str, int]], {1: {2: "3"}}) == {"1": {"2": 3}}
-
-
-def test_transform_value_with_new_type():
-    MyStr = NewType("MyStr", str)
-
-    assert transform_value({MyStr: str.upper, str: str.lower}, [], MyStr, "Test") == "TEST"
-
-
-def test_transform_value_with_cast_matching_type():
-    assert transform_value({}, [int], int, "1") == 1
-
-
-def test_transform_value_with_cast_matching_base_class():
-    class MyInt(int):
-        pass
-
-    assert transform_value({}, [int], MyInt, "1") == 1
-
-
 @init_var_type_support
 def test_extract_init_var():
     assert extract_init_var(InitVar[int]) == int