summaryrefslogtreecommitdiff
path: root/pkgs/development/python-modules/dataclasses-json/python-3.14-compat.patch
blob: e09a5d91b4d56680e7a189633ec10618b24a53f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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 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.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)