Something went wrong. Try again.
lol
Something went wrong. Try again.
5.3 kB · 145 lines
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146From 20799887ff1d50dc6ca5d90bc1038ff5160b97f3 Mon Sep 17 00:00:00 2001From: "paul@iqmo.com" <paul@iqmo.com>Date: Tue, 19 Aug 2025 21:38:21 -0400Subject: [PATCH 3/8] 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.pyindex 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.pyindex 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.pyindex 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)