diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index af13a567ca..cad34983ef 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -1,3 +1,4 @@ +from dataclasses import fields, is_dataclass from enum import Enum import logging from typing import Any, Dict, List, Union, cast @@ -16,14 +17,30 @@ def asdict(value: Any) -> AsDictValue: is introduced, because openWB still requires compatibility with Python 3.5 This function should be replaced when switching to actual Python 3.7 dataclasses. """ - if isinstance(value, (str, int, float)): + if value is None: + return None + elif isinstance(value, (str, int, float)): return value - if isinstance(value, Enum): + elif isinstance(value, Enum): return value.value - if isinstance(value, (list, tuple)): + elif isinstance(value, (list, tuple)): sequence = cast(List[Any], value) return [None if item is None else asdict(item) for item in sequence] - if not isinstance(value, dict): + elif is_dataclass(value) and not isinstance(value, type): + # Bei Dataclasses nur deklarierte Felder serialisieren + return { + field.name: asdict(getattr(value, field.name)) + for field in fields(value) + } + elif isinstance(value, dict): + mapping = cast(Dict[Any, Any], value) + return {key: None if item is None else asdict(item) for key, item in mapping.items()} + else: + default_getstate = getattr(object, "__getstate__", None) + state = getattr(value, "__getstate__", None) + value_class = type(cast(object, value)) + if callable(state) and getattr(value_class, "__getstate__", None) is not default_getstate: + return asdict(state()) value = vars(cast(object, value)) - mapping = cast(Dict[Any, Any], value) - return {key: None if item is None else asdict(item) for key, item in mapping.items()} + mapping = cast(Dict[Any, Any], value) + return {key: None if item is None else asdict(item) for key, item in mapping.items()} diff --git a/packages/dataclass_utils/_dataclass_asdict_test.py b/packages/dataclass_utils/_dataclass_asdict_test.py index 1fe54947ec..b399ccdcf3 100644 --- a/packages/dataclass_utils/_dataclass_asdict_test.py +++ b/packages/dataclass_utils/_dataclass_asdict_test.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass +from typing import Any + import pytest from dataclass_utils import asdict @@ -15,6 +18,30 @@ def __init__(self, a, b): self.b = b +class ValueWithUnserializableState: + def __init__(self, value: Any): + self.value = value + self.unserializable_state = object() + + def __getstate__(self): + return {"value": self.value} + + +class WithTupleState: + def __getstate__(self): + return ("a", 2, None) + + +class WithNoneState: + def __getstate__(self): + return None + + +@dataclass +class DataclassWithRuntimeAttribute: + value: int = 1 + + @pytest.mark.parametrize(["object", "expected_dict"], [ # Test serialization of basic types: pytest.param(SingleValue("someString"), {"value": "someString"}, id="single string"), @@ -24,6 +51,8 @@ def __init__(self, a, b): pytest.param(SingleValue(["a", 2, None]), {"value": ["a", 2, None]}, id="single list"), pytest.param(SingleValue((None, "a", 2)), {"value": [None, "a", 2]}, id="single tuple"), pytest.param(SingleValue({"a": "a", "b": 2}), {"value": {"a": "a", "b": 2}}, id="single object"), + pytest.param(ValueWithUnserializableState("someString"), {"value": "someString"}, + id="object with custom state"), # Test nesting: pytest.param(SingleValue(SingleValue("nested")), {"value": {"value": "nested"}}, id="nested object"), @@ -112,3 +141,22 @@ def test_dataclass_as_dict(): # evaluation assert actual_dict == MY_DATACLASS_AS_DICT + + +def test_asdict_with_getstate_tuple(): + assert asdict(WithTupleState()) == ["a", 2, None] + + +def test_asdict_with_getstate_none(): + assert asdict(WithNoneState()) is None + + +def test_dataclass_only_declared_fields_are_serialized(): + value = DataclassWithRuntimeAttribute() + setattr(value, "runtime_only", "ignore me") + assert asdict(value) == {"value": 1} + + +def test_asdict_raises_for_unsupported_object(): + with pytest.raises(TypeError, match=r"vars\(\) argument must have __dict__ attribute"): + asdict(object())