From 39b58bb85584a8f8321e392f3779d9f1590a4db2 Mon Sep 17 00:00:00 2001 From: Zahari Kassabov Date: Thu, 18 Dec 2025 10:56:36 +0000 Subject: [PATCH] Fix processing of dataclasses types The workaround for https://github.com/python/cpython/issues/137891 is more complicated than just accessing the __annotations__ of the dataclass instead of field.type. Indeed one has to also process all the base classes. Add a test for derived classes as well. --- validobj/tests/test_delayed_dataclasses.py | 27 +++++++++++++++++++++- validobj/validation.py | 17 +++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/validobj/tests/test_delayed_dataclasses.py b/validobj/tests/test_delayed_dataclasses.py index 7f90e59..ecb1d7f 100644 --- a/validobj/tests/test_delayed_dataclasses.py +++ b/validobj/tests/test_delayed_dataclasses.py @@ -1,16 +1,41 @@ from typing import Any import dataclasses -from validobj import parse_input +import pytest + +from validobj import parse_input, ValidationError @dataclasses.dataclass class Linked: value: Any parents: Linked | list[Linked] | None = None +@dataclasses.dataclass +class Parent: + parent_field: int = 1 + override_field: str = 'parent' + +@dataclasses.dataclass +class Child(Parent): + child_field: int = 2 + override_field: int = 3 + def test_delayed_annotations(): inp = {'value': 1, 'parents': {'value': 2, 'parents': [{'value': 3}, {'value': 4, 'parents': {'value': 5}}]}} assert parse_input(inp, Linked).parents.value == 2 +def test_derived_dataclasses(): + inp = {'child_field': 3, 'parent_field': 4, 'override_field': 5} + res = parse_input(inp, Child) + assert res.child_field == 3 + assert res.parent_field == 4 + assert res.override_field == 5 + + with pytest.raises(ValidationError): + parse_input({'override_field': 'x'}, Child) + + + + diff --git a/validobj/validation.py b/validobj/validation.py index 73d2d12..b8f74fb 100644 --- a/validobj/validation.py +++ b/validobj/validation.py @@ -181,6 +181,21 @@ def _dataclasses_fields(class_or_instance): or f._field_type is dataclasses._FIELD_INITVAR ) +def _dataclass_types(cls): + """ Workaround for https://github.com/python/cpython/issues/137891 + + Note that, contrary to dataclasses.fields, the annotations for the base fields are + not propagated automatically, so they need to be extracted from the base classes. + """ + res = {} + + # Base classes, including current one, from parent to child, excluding object + for base in cls.__mro__[1::-1]: + if dataclasses.is_dataclass(base): + res.update(base.__annotations__) + + return res + def _dataclass_required_allowed(fields): allowed = set() @@ -210,7 +225,7 @@ def _parse_dataclass(value, spec): f"fields do not match.", ) # Note: We don't use field.type because of https://github.com/python/cpython/issues/137891 - types = spec.__annotations__ + types = _dataclass_types(spec) res = {} field_dict = {