From 7ed7c0b34690479723dc1f01aca7d674773dc89f Mon Sep 17 00:00:00 2001 From: Zahari Kassabov Date: Wed, 17 Dec 2025 20:52:20 +0000 Subject: [PATCH] Enable ForwardRefs in dataclasses In Python 3.14, type annotations are resolved in a delayed manner by default, and get resolved, on demand, when accession __annotations__. This allows writing recursive types such as a dataclass where some field contains itself. As a minor annoyance, accessing the type attribute of the fields, does not resolve the delayed forward reference. Add tests for Python 3.14 --- .github/workflows/pythonpackage.yml | 2 +- validobj/tests/__init__.py | 0 validobj/tests/conftest.py | 6 +++++- validobj/tests/test_delayed_dataclasses.py | 16 ++++++++++++++++ validobj/validation.py | 5 ++++- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 validobj/tests/__init__.py create mode 100644 validobj/tests/test_delayed_dataclasses.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 94fb4ff..ab4d44a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v1 diff --git a/validobj/tests/__init__.py b/validobj/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/validobj/tests/conftest.py b/validobj/tests/conftest.py index 4070d50..f5dfd04 100644 --- a/validobj/tests/conftest.py +++ b/validobj/tests/conftest.py @@ -2,6 +2,10 @@ # https://docs.pytest.org/en/6.2.x/example/pythoncollection.html#customizing-test-collection +collect_ignore = [] if sys.version_info < (3, 12): # pragma: nocover - collect_ignore = ["test_type_syntax.py"] + collect_ignore += ["test_type_syntax.py"] + +if sys.version_info < (3, 14): # pragma: nocover + collect_ignore += ["test_delayed_dataclasses.py"] diff --git a/validobj/tests/test_delayed_dataclasses.py b/validobj/tests/test_delayed_dataclasses.py new file mode 100644 index 0000000..7f90e59 --- /dev/null +++ b/validobj/tests/test_delayed_dataclasses.py @@ -0,0 +1,16 @@ +from typing import Any +import dataclasses + +from validobj import parse_input + +@dataclasses.dataclass +class Linked: + value: Any + parents: Linked | list[Linked] | None = None + + +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 + + diff --git a/validobj/validation.py b/validobj/validation.py index 47d4d22..73d2d12 100644 --- a/validobj/validation.py +++ b/validobj/validation.py @@ -209,10 +209,13 @@ def _parse_dataclass(value, spec): header=f"Cannot process value into {_typename(spec)!r} because " f"fields do not match.", ) + # Note: We don't use field.type because of https://github.com/python/cpython/issues/137891 + types = spec.__annotations__ + res = {} field_dict = { # Look inside InitVar - f.name: f.type if not isinstance(f.type, dataclasses.InitVar) else f.type.type + f.name: types[f.name] if not isinstance(f.type, dataclasses.InitVar) else f.type.type for f in fields } for k, v in value.items():