From 908cd701b46f336b139ead81d4c084dbe56b039c Mon Sep 17 00:00:00 2001 From: Lutz Bender Date: Thu, 3 Sep 2026 12:04:44 +0200 Subject: [PATCH 1/7] fix dataclass asdict --- packages/dataclass_utils/_dataclass_asdict.py | 3 ++- packages/dataclass_utils/_dataclass_asdict_test.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index af13a567ca..fb7c48232c 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -24,6 +24,7 @@ def asdict(value: Any) -> AsDictValue: sequence = cast(List[Any], value) return [None if item is None else asdict(item) for item in sequence] if not isinstance(value, dict): - value = vars(cast(object, value)) + state = getattr(value, "__getstate__", None) + value = state() if state is not None else 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()} diff --git a/packages/dataclass_utils/_dataclass_asdict_test.py b/packages/dataclass_utils/_dataclass_asdict_test.py index 1fe54947ec..8dad6bf9e7 100644 --- a/packages/dataclass_utils/_dataclass_asdict_test.py +++ b/packages/dataclass_utils/_dataclass_asdict_test.py @@ -15,6 +15,15 @@ def __init__(self, a, b): self.b = b +class ValueWithUnserializableState: + def __init__(self, value): + self.value = value + self.unserializable_state = object() + + def __getstate__(self): + return {"value": self.value} + + @pytest.mark.parametrize(["object", "expected_dict"], [ # Test serialization of basic types: pytest.param(SingleValue("someString"), {"value": "someString"}, id="single string"), @@ -24,6 +33,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"), From 7e562604120ee9e68fff05adbc613efe7eb01c0d Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 7 Sep 2026 11:35:04 +0200 Subject: [PATCH 2/7] review --- packages/dataclass_utils/_dataclass_asdict.py | 9 +++++++- .../dataclass_utils/_dataclass_asdict_test.py | 22 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index fb7c48232c..c3fe6532d9 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -16,6 +16,8 @@ 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 value is None: + return None if isinstance(value, (str, int, float)): return value if isinstance(value, Enum): @@ -25,6 +27,11 @@ def asdict(value: Any) -> AsDictValue: return [None if item is None else asdict(item) for item in sequence] if not isinstance(value, dict): state = getattr(value, "__getstate__", None) - value = state() if state is not None else vars(cast(object, value)) + if callable(state): + return asdict(state()) + try: + value = vars(cast(object, value)) + except TypeError: + return cast(AsDictValue, value) 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 8dad6bf9e7..7147af9948 100644 --- a/packages/dataclass_utils/_dataclass_asdict_test.py +++ b/packages/dataclass_utils/_dataclass_asdict_test.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from dataclass_utils import asdict @@ -16,7 +18,7 @@ def __init__(self, a, b): class ValueWithUnserializableState: - def __init__(self, value): + def __init__(self, value: Any): self.value = value self.unserializable_state = object() @@ -24,6 +26,16 @@ def __getstate__(self): return {"value": self.value} +class WithTupleState: + def __getstate__(self): + return ("a", 2, None) + + +class WithNoneState: + def __getstate__(self): + return None + + @pytest.mark.parametrize(["object", "expected_dict"], [ # Test serialization of basic types: pytest.param(SingleValue("someString"), {"value": "someString"}, id="single string"), @@ -123,3 +135,11 @@ 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 From 10242f7f60b936a28ec5c6b9ef5382078b78dbf8 Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:49:06 +0200 Subject: [PATCH 3/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/dataclass_utils/_dataclass_asdict.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index c3fe6532d9..bfa5ac18bb 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -26,8 +26,8 @@ def asdict(value: Any) -> AsDictValue: sequence = cast(List[Any], value) return [None if item is None else asdict(item) for item in sequence] if not isinstance(value, dict): - state = getattr(value, "__getstate__", None) - if callable(state): + default_getstate = getattr(object, "__getstate__", None) + if callable(state) and getattr(type(value), "__getstate__", None) is not default_getstate: return asdict(state()) try: value = vars(cast(object, value)) From a849598aae141812823a63d7942c7e7832d0993a Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 7 Sep 2026 11:56:02 +0200 Subject: [PATCH 4/7] fix review --- packages/dataclass_utils/_dataclass_asdict.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index bfa5ac18bb..5b44f1f094 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -27,8 +27,8 @@ def asdict(value: Any) -> AsDictValue: return [None if item is None else asdict(item) for item in sequence] if not isinstance(value, dict): default_getstate = getattr(object, "__getstate__", None) - if callable(state) and getattr(type(value), "__getstate__", None) is not default_getstate: - return asdict(state()) + if callable(default_getstate) and getattr(type(value), "__getstate__", None) is not default_getstate: + return asdict(default_getstate()) try: value = vars(cast(object, value)) except TypeError: From 1c458cb8f525f6111f81c97f37d0731eb640a34e Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 7 Sep 2026 11:59:25 +0200 Subject: [PATCH 5/7] fix --- packages/dataclass_utils/_dataclass_asdict.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index 5b44f1f094..e53f6adfe9 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -27,8 +27,10 @@ def asdict(value: Any) -> AsDictValue: return [None if item is None else asdict(item) for item in sequence] if not isinstance(value, dict): default_getstate = getattr(object, "__getstate__", None) - if callable(default_getstate) and getattr(type(value), "__getstate__", None) is not default_getstate: - return asdict(default_getstate()) + state = getattr(value, "__getstate__", None) + value_class = value.__class__ + if callable(state) and getattr(value_class, "__getstate__", None) is not default_getstate: + return asdict(state()) try: value = vars(cast(object, value)) except TypeError: From ded83d52c536edadaeb597197e50f4df9a06855a Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 7 Sep 2026 14:43:37 +0200 Subject: [PATCH 6/7] review --- packages/dataclass_utils/_dataclass_asdict.py | 29 ++++++++++++------- .../dataclass_utils/_dataclass_asdict_test.py | 17 +++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index e53f6adfe9..491075f8f0 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 @@ -18,22 +19,28 @@ def asdict(value: Any) -> AsDictValue: """ if value is None: return None - if isinstance(value, (str, int, float)): + 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: None if getattr(value, field.name) is None else 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 = value.__class__ + value_class = type(cast(object, value)) if callable(state) and getattr(value_class, "__getstate__", None) is not default_getstate: return asdict(state()) - try: - value = vars(cast(object, value)) - except TypeError: - return cast(AsDictValue, value) - mapping = cast(Dict[Any, Any], value) - return {key: None if item is None else asdict(item) for key, item in mapping.items()} + 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()} diff --git a/packages/dataclass_utils/_dataclass_asdict_test.py b/packages/dataclass_utils/_dataclass_asdict_test.py index 7147af9948..b399ccdcf3 100644 --- a/packages/dataclass_utils/_dataclass_asdict_test.py +++ b/packages/dataclass_utils/_dataclass_asdict_test.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Any import pytest @@ -36,6 +37,11 @@ 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"), @@ -143,3 +149,14 @@ def test_asdict_with_getstate_tuple(): 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()) From 73dea56db42c33abb48285f2523d91353a656e9e Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:52:06 +0200 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/dataclass_utils/_dataclass_asdict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index 491075f8f0..cad34983ef 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -29,7 +29,7 @@ def asdict(value: Any) -> AsDictValue: elif is_dataclass(value) and not isinstance(value, type): # Bei Dataclasses nur deklarierte Felder serialisieren return { - field.name: None if getattr(value, field.name) is None else asdict(getattr(value, field.name)) + field.name: asdict(getattr(value, field.name)) for field in fields(value) } elif isinstance(value, dict):