File support-python314.patch of Package python-dataclasses-json
From 20799887ff1d50dc6ca5d90bc1038ff5160b97f3 Mon Sep 17 00:00:00 2001
From: "paul@iqmo.com" <paul@iqmo.com>
Date: Tue, 19 Aug 2025 21:38:21 -0400
Subject: [PATCH] fix 3.14 / PEP649, but maintain bw compat
---
dataclasses_json/core.py | 40 +++++++++++++++++++++++++++++-
dataclasses_json/undefined.py | 3 ++-
tests/test_undefined_parameters.py | 36 +++++++++++++++++++++++++++
3 files changed, 77 insertions(+), 2 deletions(-)
diff --git a/dataclasses_json/core.py b/dataclasses_json/core.py
index 69f51a3a..313e2615 100644
--- a/dataclasses_json/core.py
+++ b/dataclasses_json/core.py
@@ -18,6 +18,7 @@
from uuid import UUID
from typing_inspect import is_union_type # type: ignore
+import typing
from dataclasses_json import cfg
from dataclasses_json.utils import (_get_type_cons, _get_type_origin,
@@ -44,6 +45,43 @@
Set: frozenset,
})
+PEP649 = sys.version_info >= (3, 14)
+
+if PEP649:
+ import inspect
+
+def _safe_get_type_hints(c, **kwargs):
+
+ if not PEP649:
+ # not running under PEP 649 (future/deferred annotations),
+ return typing.get_type_hints(c, include_extras=True, **kwargs)
+
+ else:
+ if not isinstance(c, type):
+ # If we're passed an instance instead of a class, normalize to its type
+ c = c.__class__
+ if "." not in getattr(c, "__qualname__", ""):
+ # If this is a *top-level class* (no "." in __qualname__),
+ # typing.get_type_hints works fine even under PEP 649.
+ return typing.get_type_hints(c, include_extras=True, **kwargs)
+ else:
+ # Otherwise, this is a *nested class* (defined inside another class or function),
+ # where typing.get_type_hints may fail under PEP 649.
+ ann = {}
+
+ # First collect annotations from bases in the MRO
+ for base in reversed(c.__mro__[:-1]):
+ ann.update(inspect.get_annotations(base, format=inspect.Format.VALUE) or {})
+
+ # For the class itself, use FORWARDREF format to keep "Self"/recursive types intact
+ ann.update(inspect.get_annotations(c, format=inspect.Format.FORWARDREF) or {})
+
+ if ann:
+ return ann
+ else:
+ return {f.name: f.type for f in fields(c)}
+
+
class _ExtendedEncoder(json.JSONEncoder):
def default(self, o) -> Json:
@@ -175,7 +213,7 @@ def _decode_dataclass(cls, kvs, infer_missing):
kvs = _handle_undefined_parameters_safe(cls, kvs, usage="from")
init_kwargs = {}
- types = get_type_hints(cls)
+ types = _safe_get_type_hints(cls)
for field in fields(cls):
# The field should be skipped from being added
# to init_kwargs as it's not intended as a constructor argument.
diff --git a/dataclasses_json/undefined.py b/dataclasses_json/undefined.py
index cb8b2cfc..a94b4718 100644
--- a/dataclasses_json/undefined.py
+++ b/dataclasses_json/undefined.py
@@ -7,6 +7,7 @@
from typing import Any, Callable, Dict, Optional, Tuple, Union, Type, get_type_hints
from enum import Enum
+from .core import _safe_get_type_hints
from marshmallow.exceptions import ValidationError # type: ignore
from dataclasses_json.utils import CatchAllVar
@@ -248,7 +249,7 @@ def _catch_all_init(self, *args, **kwargs):
@staticmethod
def _get_catch_all_field(cls) -> Field:
cls_globals = vars(sys.modules[cls.__module__])
- types = get_type_hints(cls, globalns=cls_globals)
+ types = _safe_get_type_hints(cls, globalns=cls_globals)
catch_all_fields = list(
filter(lambda f: types[f.name] == Optional[CatchAllVar], fields(cls)))
number_of_catch_all_fields = len(catch_all_fields)
diff --git a/tests/test_undefined_parameters.py b/tests/test_undefined_parameters.py
index bac711af..6bd33406 100644
--- a/tests/test_undefined_parameters.py
+++ b/tests/test_undefined_parameters.py
@@ -221,6 +221,42 @@ class Boss:
assert json.loads(boss_json) == Boss.schema().dump(boss)
assert "".join(boss_json.replace('\n', '').split()) == "".join(Boss.schema().dumps(boss).replace('\n', '').split())
+@dataclass_json(undefined=Undefined.INCLUDE)
+@dataclass(frozen=True)
+class Minion2:
+ name: str
+ catch_all: CatchAll
+
+@dataclass_json(undefined=Undefined.INCLUDE)
+@dataclass(frozen=True)
+class Boss2:
+ minions: List[Minion2]
+ catch_all: CatchAll
+
+def test_undefined_parameters_catch_all_schema_roundtrip2(boss_json):
+ boss1 = Boss2.schema().loads(boss_json)
+ dumped_s = Boss2.schema().dumps(boss1)
+ boss2 = Boss2.schema().loads(dumped_s)
+ assert boss1 == boss2
+
+
+def test_undefined_parameters_catch_all_schema_roundtrip(boss_json):
+ @dataclass_json(undefined=Undefined.INCLUDE)
+ @dataclass(frozen=True)
+ class Minion:
+ name: str
+ catch_all: CatchAll
+
+ @dataclass_json(undefined=Undefined.INCLUDE)
+ @dataclass(frozen=True)
+ class Boss:
+ minions: List[Minion]
+ catch_all: CatchAll
+
+ boss1 = Boss.schema().loads(boss_json)
+ dumped_s = Boss.schema().dumps(boss1)
+ boss2 = Boss.schema().loads(dumped_s)
+ assert boss1 == boss2
def test_undefined_parameters_catch_all_schema_roundtrip(boss_json):
@dataclass_json(undefined=Undefined.INCLUDE)