flpit 0.2.0__tar.gz → 0.2.1.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flpit-0.2.0 → flpit-0.2.1.dev3}/PKG-INFO +3 -2
- {flpit-0.2.0 → flpit-0.2.1.dev3}/pyproject.toml +5 -3
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flpit/core/linq.py +29 -7
- flpit-0.2.1.dev3/src/flpit/json.py +427 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/.gitignore +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/LICENSE +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/README.md +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flp/__init__.py +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flpit/__init__.py +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flpit/core/__init__.py +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flpit/flp.py +0 -0
- {flpit-0.2.0 → flpit-0.2.1.dev3}/src/flpit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev3
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
5
|
Project-URL: Homepage, https://github.com/riemar/flpit
|
|
6
6
|
License: MIT
|
|
@@ -16,7 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: msgspec>=0.22.0
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
|
|
22
23
|
# FlpIt
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1.dev3"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
keywords = ["linq", "fluent", "iterable", "pipeline", "lazy-evaluation", "pyrefly", "type-safe", "generators"]
|
|
9
9
|
classifiers = [
|
|
@@ -18,7 +18,9 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
19
|
"Typing :: Typed"
|
|
20
20
|
]
|
|
21
|
-
dependencies = [
|
|
21
|
+
dependencies = [
|
|
22
|
+
"msgspec>=0.22.0",
|
|
23
|
+
]
|
|
22
24
|
|
|
23
25
|
[project.urls]
|
|
24
26
|
Homepage = "https://github.com/riemar/flpit"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import SupportsIndex
|
|
3
1
|
import decimal
|
|
4
2
|
from decimal import Decimal, Overflow, Inexact
|
|
5
3
|
|
|
@@ -15,15 +13,21 @@ from typing import (
|
|
|
15
13
|
Iterable,
|
|
16
14
|
Iterator,
|
|
17
15
|
Collection,
|
|
16
|
+
SupportsIndex,
|
|
18
17
|
List,
|
|
19
18
|
Set,
|
|
20
19
|
Type,
|
|
21
20
|
TypeVar,
|
|
21
|
+
ClassVar,
|
|
22
22
|
Union,
|
|
23
23
|
override,
|
|
24
24
|
overload,
|
|
25
25
|
cast as typing_cast,
|
|
26
|
+
GenericAlias
|
|
26
27
|
)
|
|
28
|
+
|
|
29
|
+
from flpit.json import JsonSerializerOptions, decode_dataclass, encode_dataclass
|
|
30
|
+
|
|
27
31
|
TItem = TypeVar("TItem")
|
|
28
32
|
TResult = TypeVar("TResult")
|
|
29
33
|
TKey = TypeVar("TKey")
|
|
@@ -246,8 +250,8 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
246
250
|
for item in self:
|
|
247
251
|
if predicate(item):
|
|
248
252
|
yield item
|
|
249
|
-
|
|
250
|
-
return
|
|
253
|
+
# return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
|
|
254
|
+
return FlpIt[TItem](_FactoryIterable(_generator))
|
|
251
255
|
|
|
252
256
|
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
253
257
|
"""Projects each element into a new form."""
|
|
@@ -617,10 +621,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
617
621
|
|
|
618
622
|
raise MultipleMatchesError() if has_predicate else MultipleElementsError()
|
|
619
623
|
|
|
620
|
-
|
|
621
624
|
def to_list(self) -> FlpList[TItem]:
|
|
622
625
|
"""Explicitly materializes the query into a FlpList."""
|
|
623
|
-
return FlpList(self)
|
|
626
|
+
return FlpList[TItem](self)
|
|
627
|
+
|
|
624
628
|
|
|
625
629
|
from functools import partial
|
|
626
630
|
|
|
@@ -731,6 +735,8 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
731
735
|
| Fluent List
|
|
732
736
|
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
733
737
|
"""
|
|
738
|
+
__type_args__: ClassVar[tuple[Any, ...]]
|
|
739
|
+
|
|
734
740
|
def __init__(self, source: Iterable[TItem] | _Sentinel=_SENTINEL):
|
|
735
741
|
if source is None:
|
|
736
742
|
raise SourceNoneError()
|
|
@@ -1001,7 +1007,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1001
1007
|
return self # type: ignore[return-value]
|
|
1002
1008
|
|
|
1003
1009
|
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
1004
|
-
return FlpIt(self.__list.data).where(predicate)
|
|
1010
|
+
return FlpIt[TItem](self.__list.data).where(predicate)
|
|
1005
1011
|
|
|
1006
1012
|
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
1007
1013
|
return FlpIt(self.__list.data).select(selector)
|
|
@@ -1179,3 +1185,19 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1179
1185
|
def to_list(self) -> FlpList[TItem]:
|
|
1180
1186
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
1181
1187
|
return FlpList(self.__list.data.copy())
|
|
1188
|
+
|
|
1189
|
+
# =====================================================================
|
|
1190
|
+
# 5. json serialization
|
|
1191
|
+
# =====================================================================
|
|
1192
|
+
|
|
1193
|
+
@classmethod
|
|
1194
|
+
def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpList[T]:
|
|
1195
|
+
return FlpList[T](decode_dataclass(data, list[item_type], jso))
|
|
1196
|
+
|
|
1197
|
+
def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
|
|
1198
|
+
return encode_dataclass(self.__list.data, list[item_type], jso)
|
|
1199
|
+
|
|
1200
|
+
def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
|
|
1201
|
+
return self.to_json_bytes(item_type, jso).decode(encoding="utf-8")
|
|
1202
|
+
|
|
1203
|
+
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
import dataclasses
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from dataclasses import MISSING, dataclass
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Any, ForwardRef, get_args, get_origin, get_type_hints
|
|
12
|
+
from hashlib import blake2s
|
|
13
|
+
|
|
14
|
+
import msgspec
|
|
15
|
+
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
|
|
18
|
+
class JsonNamingPolicy(StrEnum):
|
|
19
|
+
lower_case = "lower"
|
|
20
|
+
upper_case = "upper"
|
|
21
|
+
camel_case = "camel"
|
|
22
|
+
pascal_case = "pascal"
|
|
23
|
+
kebap_case = "kebab"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True, frozen=True)
|
|
27
|
+
class JsonSerializerOptions:
|
|
28
|
+
naming_policy: JsonNamingPolicy = JsonNamingPolicy.camel_case
|
|
29
|
+
write_indented: bool = False
|
|
30
|
+
native:bool=False
|
|
31
|
+
"""
|
|
32
|
+
| Set to true for the native speed of msgspec
|
|
33
|
+
|
|
34
|
+
!! only set to true if you know what you are doing !!
|
|
35
|
+
You'll receive the msgspec.msgstruct object that will try to do it's best to behave like your dataclass.
|
|
36
|
+
But remember !! "It's NOT your dataclass" !!
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def fingerprint(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Identifies a unique jso instance for building a struct
|
|
43
|
+
!! All parts of a jso that don't influence the defstruct MUST be ignored!
|
|
44
|
+
"""
|
|
45
|
+
settings = self.naming_policy.value
|
|
46
|
+
return blake2s(settings.encode(), digest_size=6).hexdigest()
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Metadata Annotation Markers
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
class JsonIgnore:
|
|
53
|
+
def __class_getitem__[T](cls, item: type[T]) -> Any:
|
|
54
|
+
return Annotated[item, cls]
|
|
55
|
+
|
|
56
|
+
class JsonInclude:
|
|
57
|
+
def __class_getitem__[T](cls, item: type[T]) -> Any:
|
|
58
|
+
return Annotated[item, cls]
|
|
59
|
+
|
|
60
|
+
class JsonProperty:
|
|
61
|
+
def __class_getitem__[T](cls, item: tuple[type[T], str]) -> Any:
|
|
62
|
+
target_type, json_name = item
|
|
63
|
+
return Annotated[target_type, cls(json_name)]
|
|
64
|
+
|
|
65
|
+
def __init__(self, name: str):
|
|
66
|
+
self.name = name
|
|
67
|
+
|
|
68
|
+
JIgnore = JsonIgnore
|
|
69
|
+
JInclude = JsonInclude
|
|
70
|
+
JProp = JsonProperty
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------
|
|
73
|
+
# Generated type metadata
|
|
74
|
+
# ---------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True, eq=False)
|
|
77
|
+
class _WireType:
|
|
78
|
+
source: type
|
|
79
|
+
"""The source type to build a msgspec.struct for"""
|
|
80
|
+
hints: dict[str, Any]
|
|
81
|
+
namespace: types.ModuleType
|
|
82
|
+
"""the name space we are targeting"""
|
|
83
|
+
struct: type | None = None
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def module_name(self: _WireType) -> str:
|
|
87
|
+
return self.namespace.__name__
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def class_name(self: _WireType) -> str:
|
|
91
|
+
return self.source.__name__
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def ref_name(self: _WireType) -> str:
|
|
95
|
+
identity = (
|
|
96
|
+
f"{self.source.__module__}_{self.source.__qualname__}"
|
|
97
|
+
)
|
|
98
|
+
return "_ref_" + re.sub(r"\W", "_", identity)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------
|
|
102
|
+
# Module and annotation utilities
|
|
103
|
+
# ---------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def _generated_module(source: type) -> types.ModuleType:
|
|
106
|
+
"""Generates the required module definition for our runtime only dynamic msgspec.struct"""
|
|
107
|
+
name = f"flpit.json._generated.{source.__module__}"
|
|
108
|
+
|
|
109
|
+
module = sys.modules.get(name)
|
|
110
|
+
if module is None:
|
|
111
|
+
module = types.ModuleType(name)
|
|
112
|
+
sys.modules[name] = module
|
|
113
|
+
|
|
114
|
+
return module
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_dataclass_type(tp: Any) -> bool:
|
|
118
|
+
return isinstance(tp, type) and dataclasses.is_dataclass(tp)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _nested_dataclasses(tp: Any):
|
|
122
|
+
"""Yield dataclass types referenced by an annotation."""
|
|
123
|
+
if _is_dataclass_type(tp):
|
|
124
|
+
yield tp
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if isinstance(tp, (list, tuple)):
|
|
128
|
+
for item in tp:
|
|
129
|
+
yield from _nested_dataclasses(item)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
origin = get_origin(tp)
|
|
133
|
+
|
|
134
|
+
if origin is typing.Annotated:
|
|
135
|
+
yield from _nested_dataclasses(get_args(tp)[0])
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if origin is typing.Literal:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
for arg in get_args(tp):
|
|
142
|
+
yield from _nested_dataclasses(arg)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _rewrite_type(
|
|
146
|
+
tp: Any,
|
|
147
|
+
owner: _WireType,
|
|
148
|
+
graph: dict[type, _WireType],
|
|
149
|
+
*,
|
|
150
|
+
resolve: bool = False,
|
|
151
|
+
) -> Any:
|
|
152
|
+
"""Replace source dataclasses with generated Structs or ForwardRefs."""
|
|
153
|
+
if tp is getattr(typing, "Self", None):
|
|
154
|
+
info = owner
|
|
155
|
+
return info.struct if resolve else ForwardRef(info.ref_name)
|
|
156
|
+
|
|
157
|
+
if _is_dataclass_type(tp):
|
|
158
|
+
info = graph[tp]
|
|
159
|
+
return info.struct if resolve else ForwardRef(info.ref_name)
|
|
160
|
+
|
|
161
|
+
if isinstance(tp, list):
|
|
162
|
+
return [
|
|
163
|
+
_rewrite_type(item, owner, graph, resolve=resolve)
|
|
164
|
+
for item in tp
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
if isinstance(tp, tuple):
|
|
168
|
+
return tuple(
|
|
169
|
+
_rewrite_type(item, owner, graph, resolve=resolve)
|
|
170
|
+
for item in tp
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
origin = get_origin(tp)
|
|
174
|
+
|
|
175
|
+
if origin is None or origin is typing.Literal:
|
|
176
|
+
return tp
|
|
177
|
+
|
|
178
|
+
args = get_args(tp)
|
|
179
|
+
|
|
180
|
+
if origin is typing.Annotated:
|
|
181
|
+
base = _rewrite_type(
|
|
182
|
+
args[0], owner, graph, resolve=resolve
|
|
183
|
+
)
|
|
184
|
+
return typing.Annotated[(base, *args[1:])]
|
|
185
|
+
|
|
186
|
+
rewritten: tuple[Any, ...] = tuple(
|
|
187
|
+
_rewrite_type(arg, owner, graph, resolve=resolve)
|
|
188
|
+
for arg in args
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if origin in (typing.Union, types.UnionType):
|
|
192
|
+
union: Any = typing.Union # with all the magic here, there's no way to make typing really happy
|
|
193
|
+
return union[rewritten]
|
|
194
|
+
|
|
195
|
+
params = rewritten[0] if len(rewritten) == 1 else rewritten
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
return origin[params]
|
|
199
|
+
except (TypeError, AttributeError) as exc:
|
|
200
|
+
copier = getattr(tp, "copy_with", None)
|
|
201
|
+
if copier is not None:
|
|
202
|
+
return copier(rewritten)
|
|
203
|
+
|
|
204
|
+
raise TypeError(
|
|
205
|
+
f"Cannot rewrite annotation {tp!r}"
|
|
206
|
+
) from exc
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------
|
|
210
|
+
# Graph discovery
|
|
211
|
+
# ---------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _discover_graph(
|
|
214
|
+
roots: tuple[type, ...],
|
|
215
|
+
) -> dict[type, _WireType]:
|
|
216
|
+
"""Discover reachable dataclasses and resolve their annotations."""
|
|
217
|
+
graph: dict[type, _WireType] = {}
|
|
218
|
+
pending = list(roots)
|
|
219
|
+
|
|
220
|
+
while pending:
|
|
221
|
+
source = pending.pop()
|
|
222
|
+
|
|
223
|
+
if source in graph:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
info = _WireType(
|
|
227
|
+
source=source,
|
|
228
|
+
hints=get_type_hints(source, include_extras=True),
|
|
229
|
+
namespace=_generated_module(source),
|
|
230
|
+
)
|
|
231
|
+
graph[source] = info
|
|
232
|
+
|
|
233
|
+
for field in dataclasses.fields(source):
|
|
234
|
+
if not field.init:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
pending.extend(
|
|
238
|
+
_nested_dataclasses(info.hints[field.name])
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return graph
|
|
242
|
+
|
|
243
|
+
def _allows_none(tp: Any) -> bool:
|
|
244
|
+
if get_origin(tp) is typing.Annotated:
|
|
245
|
+
tp = get_args(tp)[0]
|
|
246
|
+
|
|
247
|
+
return type(None) in get_args(tp)
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------
|
|
250
|
+
# Struct construction
|
|
251
|
+
# ---------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def _struct_fields(
|
|
254
|
+
info: _WireType,
|
|
255
|
+
graph: dict[type, _WireType],
|
|
256
|
+
) -> list[tuple[Any, ...]]:
|
|
257
|
+
definitions: list[tuple[Any, ...]] = []
|
|
258
|
+
|
|
259
|
+
for field in dataclasses.fields(info.source):
|
|
260
|
+
if not field.init:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
annotation = _rewrite_type(
|
|
264
|
+
info.hints[field.name],
|
|
265
|
+
info,
|
|
266
|
+
graph,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if field.default is not MISSING:
|
|
270
|
+
spec = (field.name, annotation, field.default)
|
|
271
|
+
|
|
272
|
+
elif field.default_factory is not MISSING:
|
|
273
|
+
spec = (
|
|
274
|
+
field.name,
|
|
275
|
+
annotation,
|
|
276
|
+
msgspec.field(
|
|
277
|
+
default_factory=field.default_factory
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# for now... no enforcing of explicit null in json
|
|
282
|
+
elif _allows_none(info.hints[field.name]):
|
|
283
|
+
spec = (field.name, annotation, None)
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
spec = (field.name, annotation)
|
|
287
|
+
|
|
288
|
+
definitions.append(spec)
|
|
289
|
+
|
|
290
|
+
return definitions
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _register_forward_refs(
|
|
294
|
+
graph: dict[type, _WireType],
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Expose every graph reference in each participating module."""
|
|
297
|
+
namespaces = []
|
|
298
|
+
|
|
299
|
+
for info in graph.values():
|
|
300
|
+
if not any(info.namespace is ns for ns in namespaces):
|
|
301
|
+
namespaces.append(info.namespace)
|
|
302
|
+
|
|
303
|
+
for namespace in namespaces:
|
|
304
|
+
for info in graph.values():
|
|
305
|
+
setattr(namespace, info.ref_name, info.struct)
|
|
306
|
+
|
|
307
|
+
def _behavior_namespace(source: type) -> dict[str, Any]:
|
|
308
|
+
return {
|
|
309
|
+
name: value
|
|
310
|
+
for name, value in vars(source).items()
|
|
311
|
+
if (
|
|
312
|
+
not name.startswith("__")
|
|
313
|
+
and callable(value)
|
|
314
|
+
)
|
|
315
|
+
# pyrefly: ignore [implicit-any-lambda]
|
|
316
|
+
} | { "__class__": property(lambda self, cls=source: cls) }
|
|
317
|
+
|
|
318
|
+
def _build_graph(
|
|
319
|
+
roots: tuple[type, ...],
|
|
320
|
+
jso: JsonSerializerOptions,
|
|
321
|
+
) -> dict[type, _WireType]:
|
|
322
|
+
graph = _discover_graph(roots)
|
|
323
|
+
|
|
324
|
+
for wt in graph.values():
|
|
325
|
+
wt.struct = msgspec.defstruct(
|
|
326
|
+
wt.class_name,
|
|
327
|
+
_struct_fields(wt, graph),
|
|
328
|
+
module=wt.module_name,
|
|
329
|
+
namespace=_behavior_namespace(wt.source),
|
|
330
|
+
rename=jso.naming_policy.value,
|
|
331
|
+
kw_only=True,
|
|
332
|
+
omit_defaults=True, # don't render nulls
|
|
333
|
+
forbid_unknown_fields=False, # ensure ignoring unknow fields!
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
_register_forward_refs(graph)
|
|
337
|
+
|
|
338
|
+
for wt in graph.values():
|
|
339
|
+
msgspec.inspect.type_info(wt.struct)
|
|
340
|
+
|
|
341
|
+
# Publish the readable generated class names.
|
|
342
|
+
for wt in graph.values():
|
|
343
|
+
setattr(wt.namespace, wt.class_name, wt.struct)
|
|
344
|
+
|
|
345
|
+
return graph
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------
|
|
349
|
+
# Cache and public API
|
|
350
|
+
# ---------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
# we must only build new when the
|
|
353
|
+
_jso_registry: dict[str, JsonSerializerOptions] = {}
|
|
354
|
+
_registry_lock = threading.Lock()
|
|
355
|
+
|
|
356
|
+
@lru_cache(maxsize=None)
|
|
357
|
+
def _wrapped_lru(roots: tuple[type, ...], jso_id: str):
|
|
358
|
+
with _registry_lock:
|
|
359
|
+
jso = _jso_registry[jso_id]
|
|
360
|
+
return _build_graph(roots, jso)
|
|
361
|
+
|
|
362
|
+
def _cached_graph(roots:tuple[object, ...], jso: JsonSerializerOptions):
|
|
363
|
+
with _registry_lock:
|
|
364
|
+
_jso_registry[jso.fingerprint] = jso
|
|
365
|
+
|
|
366
|
+
return _wrapped_lru(roots, jso.fingerprint)
|
|
367
|
+
|
|
368
|
+
def wire_type_for(
|
|
369
|
+
target_type: type,
|
|
370
|
+
jso: JsonSerializerOptions,
|
|
371
|
+
):
|
|
372
|
+
roots = tuple(_nested_dataclasses(target_type))
|
|
373
|
+
|
|
374
|
+
if not roots:
|
|
375
|
+
raise TypeError(
|
|
376
|
+
"Only @dataclass are supported!, "
|
|
377
|
+
f"got {target_type!r}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Normalize the root set so User and list[User] share the graph.
|
|
381
|
+
graph_roots = tuple(sorted(set(roots), key=id))
|
|
382
|
+
|
|
383
|
+
graph = _cached_graph(
|
|
384
|
+
graph_roots,
|
|
385
|
+
jso,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return _rewrite_type(
|
|
389
|
+
target_type,
|
|
390
|
+
graph[roots[0]],
|
|
391
|
+
graph,
|
|
392
|
+
resolve=True,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def decode_dataclass(
|
|
397
|
+
data: bytes | str,
|
|
398
|
+
target: type,
|
|
399
|
+
jso: JsonSerializerOptions|None=None
|
|
400
|
+
):
|
|
401
|
+
if jso is None:
|
|
402
|
+
jso = JsonSerializerOptions()
|
|
403
|
+
|
|
404
|
+
wire_type = wire_type_for(target, jso)
|
|
405
|
+
parsed = msgspec.json.decode(data, type=wire_type)
|
|
406
|
+
|
|
407
|
+
if jso.native:
|
|
408
|
+
return parsed
|
|
409
|
+
|
|
410
|
+
return msgspec.convert(
|
|
411
|
+
parsed,
|
|
412
|
+
target,
|
|
413
|
+
from_attributes=True,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def encode_dataclass[T](
|
|
417
|
+
data: Any,
|
|
418
|
+
target: type[T],
|
|
419
|
+
jso: JsonSerializerOptions|None=None
|
|
420
|
+
) -> bytes:
|
|
421
|
+
if jso is None:
|
|
422
|
+
jso = JsonSerializerOptions()
|
|
423
|
+
|
|
424
|
+
wire_type = wire_type_for(target, jso)
|
|
425
|
+
wire_obj = msgspec.convert(data, wire_type, from_attributes=True)
|
|
426
|
+
return msgspec.json.encode(wire_obj)
|
|
427
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|