adiumentum 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- adiumentum/__init__.py +41 -19
- adiumentum/dependency_sorting.py +84 -0
- adiumentum/display.py +49 -0
- adiumentum/file_modification_time.py +19 -11
- adiumentum/frozendict.py +2 -2
- adiumentum/functional.py +57 -6
- adiumentum/io_utils.py +63 -0
- adiumentum/markers.py +67 -84
- adiumentum/merge.py +113 -0
- adiumentum/paths_manager.py +19 -0
- adiumentum/pydantic_extensions.md +839 -0
- adiumentum/pydantic_extensions.py +410 -0
- adiumentum/{string.py → string_utils.py} +10 -2
- adiumentum/timestamping.py +10 -2
- adiumentum/typing_utils.py +115 -2
- adiumentum-0.3.0.dist-info/METADATA +61 -0
- adiumentum-0.3.0.dist-info/RECORD +26 -0
- adiumentum-0.3.0.dist-info/entry_points.txt +3 -0
- adiumentum/io.py +0 -33
- adiumentum-0.1.0.dist-info/METADATA +0 -236
- adiumentum-0.1.0.dist-info/RECORD +0 -19
- {adiumentum-0.1.0.dist-info → adiumentum-0.3.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,839 @@
|
|
1
|
+
# pydantic_extensions
|
2
|
+
|
3
|
+
```py
|
4
|
+
# BaseModel methods:
|
5
|
+
ms = [
|
6
|
+
("_CHECK SIGNATURE", "model_dump_json"),
|
7
|
+
("_CHECK SIGNATURE", "model_dump"),
|
8
|
+
("_CHECK SIGNATURE", "model_json_schema"),
|
9
|
+
("_CHECK SIGNATURE", "model_validate_json"),
|
10
|
+
("_CHECK SIGNATURE", "model_validate"),
|
11
|
+
("_CHECK SIGNATURE", "schema_json"),
|
12
|
+
("_CHECK SIGNATURE", "schema"),
|
13
|
+
("_TODO needed?", "model_post_init"),
|
14
|
+
("_TODO needed?", "model_rebuild"),
|
15
|
+
("_TODO", "__repr__"),
|
16
|
+
("_TODO", "__str__"),
|
17
|
+
("_TODO", "model_config"),
|
18
|
+
("_TODO", "model_construct"),
|
19
|
+
("_TODO", "model_copy"),
|
20
|
+
("_TODO", "model_validate_strings"),
|
21
|
+
("(?) later as needed", "__pretty__"),
|
22
|
+
("(?) later as needed", "__pydantic_complete__"),
|
23
|
+
("(?) later as needed", "__pydantic_computed_fields__"),
|
24
|
+
("(?) later as needed", "__pydantic_core_schema__"),
|
25
|
+
("(?) later as needed", "__pydantic_custom_init__"),
|
26
|
+
("(?) later as needed", "__pydantic_decorators__"),
|
27
|
+
("(?) later as needed", "__pydantic_extra__"),
|
28
|
+
("(?) later as needed", "__pydantic_fields__"),
|
29
|
+
("(?) later as needed", "__pydantic_fields_set__"),
|
30
|
+
("(?) later as needed", "__pydantic_generic_metadata__"),
|
31
|
+
("(?) later as needed", "__pydantic_init_subclass__"),
|
32
|
+
("(?) later as needed", "__pydantic_parent_namespace__"),
|
33
|
+
("(?) later as needed", "__pydantic_post_init__"),
|
34
|
+
("(?) later as needed", "__pydantic_private__"),
|
35
|
+
("(?) later as needed", "__pydantic_root_model__"),
|
36
|
+
("(?) later as needed", "__pydantic_serializer__"),
|
37
|
+
("(?) later as needed", "__pydantic_setattr_handlers__"),
|
38
|
+
("(?) later as needed", "__pydantic_validator__"),
|
39
|
+
("(?) later as needed", "_iter"),
|
40
|
+
("(?) later as needed", "__fields__"),
|
41
|
+
("(?) later as needed", "__dict__"), # return self or self.__class__(kmap(str, self))?
|
42
|
+
("(?) later as needed", "__abstractmethods__"),
|
43
|
+
("(?) later as needed", "__class_vars__"),
|
44
|
+
("(?) later as needed", "__copy__"),
|
45
|
+
("(?) later as needed", "__deepcopy__"),
|
46
|
+
("(?) later as needed", "__fields__"),
|
47
|
+
("(?) later as needed", "__fields_set__"),
|
48
|
+
("(?) later as needed", "__getstate__"),
|
49
|
+
("(?) later as needed", "__reduce__"),
|
50
|
+
("(?) later as needed", "__reduce_ex__"),
|
51
|
+
("(?) later as needed", "__replace__"),
|
52
|
+
("(?) later as needed", "__setstate__"),
|
53
|
+
("(?) later as needed", "__signature__"),
|
54
|
+
("(?) later as needed", "__weakref__"),
|
55
|
+
("(?) later as needed", "_abc_impl"),
|
56
|
+
("(?) later as needed", "_setattr_handler"),
|
57
|
+
("(?) not needed?", "__repr_args__"),
|
58
|
+
("(?) not needed?", "__repr_name__"),
|
59
|
+
("(?) not needed?", "__repr_recursion__"),
|
60
|
+
("(?) not needed?", "__repr_str__"),
|
61
|
+
("(?) not needed?", "__rich_repr__"),
|
62
|
+
("(?) not needed?", "_copy_and_set_values"),
|
63
|
+
("(?) not needed?", "_get_value"),
|
64
|
+
("(?) not needed?", "model_parametrized_name"),
|
65
|
+
("(?) not needed?", "__class_getitem__"), # available in TypeAdapter
|
66
|
+
("(?) not needed?", "__private_attributes__"),
|
67
|
+
("✔ builtin?", "__sizeof__"),
|
68
|
+
("✔ builtin?", "__slots__"),
|
69
|
+
("✔ builtin", "__class__"),
|
70
|
+
("✔ builtin", "__delattr__"),
|
71
|
+
("✔ builtin", "__dir__"),
|
72
|
+
("✔ builtin", "__doc__"),
|
73
|
+
("✔ builtin", "__module__"),
|
74
|
+
("✔ builtin", "__subclasshook__"),
|
75
|
+
("✔ deprecated", "construct"),
|
76
|
+
("✔ deprecated", "copy"),
|
77
|
+
("✔ deprecated", "dict"),
|
78
|
+
("✔ deprecated", "json"),
|
79
|
+
("✔ deprecated", "model_fields"),
|
80
|
+
("✔ deprecated", "parse_file"),
|
81
|
+
("✔ deprecated", "parse_obj"),
|
82
|
+
("✔ deprecated", "parse_raw"),
|
83
|
+
("✔ deprecated", "update_forward_refs"),
|
84
|
+
("✔ deprecated", "validate"),
|
85
|
+
("✔ inherited?", "__new__"),
|
86
|
+
("✔ inherited", "__eq__"),
|
87
|
+
("✔ inherited", "__format__"),
|
88
|
+
("✔ inherited", "__ge__"),
|
89
|
+
("✔ inherited", "__getattr__"),
|
90
|
+
("✔ inherited", "__getattribute__"),
|
91
|
+
("✔ inherited", "__gt__"),
|
92
|
+
("✔ inherited", "__hash__"),
|
93
|
+
("✔ inherited", "__init__"),
|
94
|
+
("✔ inherited", "__init_subclass__"),
|
95
|
+
("✔ inherited", "__iter__"),
|
96
|
+
("✔ inherited", "__le__"),
|
97
|
+
("✔ inherited", "__lt__"),
|
98
|
+
("✔ inherited", "__ne__"),
|
99
|
+
("✔ inherited", "__setattr__"),
|
100
|
+
("✔ not needed?", "_calculate_keys"),
|
101
|
+
("✔ not needed?", "model_computed_fields"),
|
102
|
+
("✔ not needed", "from_orm"),
|
103
|
+
("✔ not needed", "model_fields_set"),
|
104
|
+
("✔", "__annotations__"),
|
105
|
+
("✔", "__get_pydantic_core_schema__"),
|
106
|
+
("✔", "__get_pydantic_json_schema__"),
|
107
|
+
("✔", "model_extra"),
|
108
|
+
]
|
109
|
+
|
110
|
+
type_adapter_methods_unique = [
|
111
|
+
"__final__", #
|
112
|
+
"__orig_bases__", #
|
113
|
+
"__parameters__", #
|
114
|
+
"_config", #
|
115
|
+
"_defer_build", #
|
116
|
+
"_fetch_parent_frame", #
|
117
|
+
"_init_core_attrs", #
|
118
|
+
"_model_config", #
|
119
|
+
"_module_name", #
|
120
|
+
"_parent_depth", #
|
121
|
+
"_type", #
|
122
|
+
"core_schema", #
|
123
|
+
"dump_json", # TODO: use
|
124
|
+
"dump_python", #
|
125
|
+
"get_default_value", #
|
126
|
+
"json_schema", #
|
127
|
+
"json_schemas", #
|
128
|
+
"pydantic_complete", #
|
129
|
+
"rebuild", #
|
130
|
+
"serializer", #
|
131
|
+
"validate_json", #
|
132
|
+
"validate_python", #
|
133
|
+
"validate_strings", #
|
134
|
+
"validator", #
|
135
|
+
]
|
136
|
+
|
137
|
+
type_adapter_methods = [
|
138
|
+
"__annotations__",
|
139
|
+
"__class__",
|
140
|
+
"__class_getitem__",
|
141
|
+
"__delattr__",
|
142
|
+
"__dict__",
|
143
|
+
"__dir__",
|
144
|
+
"__doc__",
|
145
|
+
"__eq__",
|
146
|
+
"__final__",
|
147
|
+
"__format__",
|
148
|
+
"__ge__",
|
149
|
+
"__getattribute__",
|
150
|
+
"__getstate__",
|
151
|
+
"__gt__",
|
152
|
+
"__hash__",
|
153
|
+
"__init__",
|
154
|
+
"__init_subclass__",
|
155
|
+
"__le__",
|
156
|
+
"__lt__",
|
157
|
+
"__module__",
|
158
|
+
"__ne__",
|
159
|
+
"__new__",
|
160
|
+
"__orig_bases__",
|
161
|
+
"__parameters__",
|
162
|
+
"__reduce__",
|
163
|
+
"__reduce_ex__",
|
164
|
+
"__repr__",
|
165
|
+
"__setattr__",
|
166
|
+
"__sizeof__",
|
167
|
+
"__str__",
|
168
|
+
"__subclasshook__",
|
169
|
+
"__weakref__",
|
170
|
+
"_config",
|
171
|
+
"_defer_build",
|
172
|
+
"_fetch_parent_frame",
|
173
|
+
"_init_core_attrs",
|
174
|
+
"_model_config",
|
175
|
+
"_module_name",
|
176
|
+
"_parent_depth",
|
177
|
+
"_type",
|
178
|
+
"core_schema",
|
179
|
+
"dump_json",
|
180
|
+
"dump_python",
|
181
|
+
"get_default_value",
|
182
|
+
"json_schema",
|
183
|
+
"json_schemas",
|
184
|
+
"pydantic_complete",
|
185
|
+
"rebuild",
|
186
|
+
"serializer",
|
187
|
+
"validate_json",
|
188
|
+
"validate_python",
|
189
|
+
"validate_strings",
|
190
|
+
"validator",
|
191
|
+
]
|
192
|
+
```
|
193
|
+
|
194
|
+
|
195
|
+
|
196
|
+
## Discussion
|
197
|
+
|
198
|
+
Excellent question — this touches on a subtle interaction between **generic custom containers**, **`pydantic`’s validation model**, and **Python’s typing system**.
|
199
|
+
|
200
|
+
Let’s go step by step.
|
201
|
+
|
202
|
+
---
|
203
|
+
|
204
|
+
### 🧩 Goal
|
205
|
+
|
206
|
+
You want a generic class
|
207
|
+
|
208
|
+
```python
|
209
|
+
class BaseDict(Generic[K, V]): ...
|
210
|
+
```
|
211
|
+
|
212
|
+
that:
|
213
|
+
|
214
|
+
1. Behaves like a `dict[K, V]`
|
215
|
+
2. Uses `pydantic.TypeAdapter` for validation
|
216
|
+
3. Can appear **as a field in a `BaseModel`**, e.g.:
|
217
|
+
|
218
|
+
```python
|
219
|
+
class MyModel(BaseModel):
|
220
|
+
mapping: MyDict[str, int]
|
221
|
+
```
|
222
|
+
|
223
|
+
---
|
224
|
+
|
225
|
+
### ✅ What Pydantic Expects
|
226
|
+
|
227
|
+
Pydantic (v2.x) uses the [`__get_pydantic_core_schema__`](https://docs.pydantic.dev/latest/api/functional_validators/#pydantic.get_pydantic_core_schema) and `__get_pydantic_json_schema__` protocols to understand custom classes.
|
228
|
+
|
229
|
+
However, **for simple generic containers that are “dict-like”**, you don’t need the full schema hooks if you make your class implement the *minimum dict interface* that pydantic recognizes as “mapping-like”.
|
230
|
+
|
231
|
+
---
|
232
|
+
|
233
|
+
### ✅ Required Methods for Dict Compatibility
|
234
|
+
|
235
|
+
To be treated like a `dict` (and for `TypeAdapter` to use your generic parameters), your class must implement at least these:
|
236
|
+
|
237
|
+
| Method | Purpose | Required by |
|
238
|
+
| --------------------------------- | -------------------------------- | ------------------------------- |
|
239
|
+
| `__init__(self, *args, **kwargs)` | Accepts dict-like initialization | general |
|
240
|
+
| `__getitem__(self, key)` | Retrieve an item | Mapping protocol |
|
241
|
+
| `__setitem__(self, key, value)` | Set an item | MutableMapping |
|
242
|
+
| `__delitem__(self, key)` | Delete an item | MutableMapping |
|
243
|
+
| `__iter__(self)` | Iterate over keys | Mapping protocol |
|
244
|
+
| `__len__(self)` | Number of keys | Mapping protocol |
|
245
|
+
| `items(self)` | Return key-value pairs | Used by validators, serializers |
|
246
|
+
| `keys(self)` | Return keys | Used by validators, serializers |
|
247
|
+
| `values(self)` | Return values | Optional but expected |
|
248
|
+
| `__contains__(self, key)` | For `in` checks | optional but standard |
|
249
|
+
| `__repr__(self)` | For debug clarity | recommended |
|
250
|
+
|
251
|
+
You can get most of this automatically by subclassing `collections.abc.MutableMapping`:
|
252
|
+
|
253
|
+
```python
|
254
|
+
from collections.abc import MutableMapping
|
255
|
+
from typing import Generic, TypeVar
|
256
|
+
from pydantic import TypeAdapter
|
257
|
+
|
258
|
+
K = TypeVar("K")
|
259
|
+
V = TypeVar("V")
|
260
|
+
|
261
|
+
|
262
|
+
class BaseDict(MutableMapping[K, V], Generic[K, V]):
|
263
|
+
def __init__(self, data=None):
|
264
|
+
self._data = dict(data or {})
|
265
|
+
# Validate with TypeAdapter if desired:
|
266
|
+
if data is not None:
|
267
|
+
adapter = TypeAdapter(dict[K, V])
|
268
|
+
self._data = adapter.validate_python(data)
|
269
|
+
|
270
|
+
def __getitem__(self, key: K) -> V:
|
271
|
+
return self._data[key]
|
272
|
+
|
273
|
+
def __setitem__(self, key: K, value: V) -> None:
|
274
|
+
self._data[key] = value
|
275
|
+
|
276
|
+
def __delitem__(self, key: K) -> None:
|
277
|
+
del self._data[key]
|
278
|
+
|
279
|
+
def __iter__(self):
|
280
|
+
return iter(self._data)
|
281
|
+
|
282
|
+
def __len__(self):
|
283
|
+
return len(self._data)
|
284
|
+
|
285
|
+
def __repr__(self):
|
286
|
+
return f"{self.__class__.__name__}({self._data!r})"
|
287
|
+
```
|
288
|
+
|
289
|
+
---
|
290
|
+
|
291
|
+
### ✅ Making It Work Inside a `BaseModel`
|
292
|
+
|
293
|
+
When you define:
|
294
|
+
|
295
|
+
```python
|
296
|
+
class MyDict(BaseDict[str, int]):
|
297
|
+
pass
|
298
|
+
|
299
|
+
|
300
|
+
class MyModel(BaseModel):
|
301
|
+
mapping: MyDict
|
302
|
+
```
|
303
|
+
|
304
|
+
Pydantic v2 will use your `BaseDict`’s `__init__` and type hints to validate `mapping` automatically — because `BaseDict` behaves like a `dict`.
|
305
|
+
|
306
|
+
If you want to **control schema generation** (e.g., for OpenAPI or JSON schema), you can also implement:
|
307
|
+
|
308
|
+
```python
|
309
|
+
def __get_pydantic_core_schema__(cls, source_type, handler):
|
310
|
+
# delegate to dict but keep your class name
|
311
|
+
return handler(dict)
|
312
|
+
```
|
313
|
+
|
314
|
+
but that’s optional unless you need advanced integration.
|
315
|
+
|
316
|
+
---
|
317
|
+
|
318
|
+
### ✅ Summary
|
319
|
+
|
320
|
+
To include subclasses of `BaseDict` as values inside a `BaseModel`:
|
321
|
+
|
322
|
+
**You must implement (or inherit) the dict interface**, i.e.:
|
323
|
+
|
324
|
+
```python
|
325
|
+
__getitem__, __setitem__, __delitem__, __iter__, __len__
|
326
|
+
```
|
327
|
+
|
328
|
+
Optionally add:
|
329
|
+
|
330
|
+
```python
|
331
|
+
items, keys, values, __contains__, __repr__
|
332
|
+
```
|
333
|
+
|
334
|
+
and optionally define:
|
335
|
+
|
336
|
+
```python
|
337
|
+
__get_pydantic_core_schema__ # for full control
|
338
|
+
__get_pydantic_json_schema__ # for schema export
|
339
|
+
```
|
340
|
+
|
341
|
+
If you subclass `collections.abc.MutableMapping`, you automatically satisfy this, and Pydantic will happily treat your class as a `dict`-like field.
|
342
|
+
|
343
|
+
---
|
344
|
+
|
345
|
+
Perfect — here’s how to make your `BaseDict[K, V]` actually *validate* keys and values using **Pydantic’s `TypeAdapter`**, so that it works transparently both:
|
346
|
+
|
347
|
+
* when used *directly* (e.g. `BaseDict[str, int]({"a": 1})`), and
|
348
|
+
* when embedded in a **`BaseModel` field** (e.g. `MyModel(mapping={"a": "1"})` → coerced to `int`).
|
349
|
+
|
350
|
+
---
|
351
|
+
|
352
|
+
## ✅ Step 1. The Generic `BaseDict` Class
|
353
|
+
|
354
|
+
```python
|
355
|
+
from __future__ import annotations
|
356
|
+
from collections.abc import MutableMapping
|
357
|
+
from typing import Generic, TypeVar, Any
|
358
|
+
from pydantic import TypeAdapter
|
359
|
+
|
360
|
+
K = TypeVar("K")
|
361
|
+
V = TypeVar("V")
|
362
|
+
|
363
|
+
|
364
|
+
class BaseDict(MutableMapping[K, V], Generic[K, V]):
|
365
|
+
"""A dict-like container with per-key/value validation via Pydantic."""
|
366
|
+
|
367
|
+
def __init__(self, data: Any | None = None):
|
368
|
+
self._data: dict[K, V] = {}
|
369
|
+
if data is not None:
|
370
|
+
# TypeAdapter for the full mapping type
|
371
|
+
adapter = TypeAdapter(dict[K, V])
|
372
|
+
# Will coerce + validate all keys and values
|
373
|
+
self._data = adapter.validate_python(data)
|
374
|
+
|
375
|
+
# --- Mapping protocol ---
|
376
|
+
def __getitem__(self, key: K) -> V:
|
377
|
+
return self._data[key]
|
378
|
+
|
379
|
+
def __setitem__(self, key: K, value: V) -> None:
|
380
|
+
# validate each key/value pair on assignment
|
381
|
+
key_adapter = TypeAdapter(K)
|
382
|
+
val_adapter = TypeAdapter(V)
|
383
|
+
k = key_adapter.validate_python(key)
|
384
|
+
v = val_adapter.validate_python(value)
|
385
|
+
self._data[k] = v
|
386
|
+
|
387
|
+
def __delitem__(self, key: K) -> None:
|
388
|
+
del self._data[key]
|
389
|
+
|
390
|
+
def __iter__(self):
|
391
|
+
return iter(self._data)
|
392
|
+
|
393
|
+
def __len__(self):
|
394
|
+
return len(self._data)
|
395
|
+
|
396
|
+
def __repr__(self):
|
397
|
+
return f"{self.__class__.__name__}({self._data!r})"
|
398
|
+
|
399
|
+
# --- Optional convenience ---
|
400
|
+
def to_dict(self) -> dict[K, V]:
|
401
|
+
"""Return a plain dict for serialization or JSON export."""
|
402
|
+
return dict(self._data)
|
403
|
+
```
|
404
|
+
|
405
|
+
---
|
406
|
+
|
407
|
+
## ✅ Step 2. Using It as a Base Type
|
408
|
+
|
409
|
+
You can now subclass it for specific key/value types:
|
410
|
+
|
411
|
+
```python
|
412
|
+
class StrIntDict(BaseDict[str, int]):
|
413
|
+
pass
|
414
|
+
```
|
415
|
+
|
416
|
+
And it already works with `TypeAdapter`:
|
417
|
+
|
418
|
+
```python
|
419
|
+
adapter = TypeAdapter(StrIntDict)
|
420
|
+
validated = adapter.validate_python({"a": "42"})
|
421
|
+
print(validated) # StrIntDict({'a': 42})
|
422
|
+
print(type(validated)) # <class '__main__.StrIntDict'>
|
423
|
+
```
|
424
|
+
|
425
|
+
The `"42"` string is converted to `int(42)` automatically.
|
426
|
+
|
427
|
+
---
|
428
|
+
|
429
|
+
## ✅ Step 3. Using It Inside a Pydantic Model
|
430
|
+
|
431
|
+
```python
|
432
|
+
from pydantic import BaseModel
|
433
|
+
|
434
|
+
|
435
|
+
class MyModel(BaseModel):
|
436
|
+
mapping: StrIntDict
|
437
|
+
|
438
|
+
|
439
|
+
m = MyModel(mapping={"x": "100"})
|
440
|
+
print(m.mapping) # StrIntDict({'x': 100})
|
441
|
+
print(type(m.mapping)) # <class '__main__.StrIntDict'>
|
442
|
+
```
|
443
|
+
|
444
|
+
Everything works seamlessly — Pydantic recognizes it as a dict-like container.
|
445
|
+
|
446
|
+
---
|
447
|
+
|
448
|
+
## ✅ Step 4. Extending Validation Logic (Optional)
|
449
|
+
|
450
|
+
You can further specialize validation by overriding `__setitem__` or defining custom methods:
|
451
|
+
|
452
|
+
```python
|
453
|
+
class PositiveIntDict(BaseDict[str, int]):
|
454
|
+
def __setitem__(self, key: str, value: int):
|
455
|
+
value = TypeAdapter(int).validate_python(value)
|
456
|
+
if value <= 0:
|
457
|
+
raise ValueError("All values must be positive integers")
|
458
|
+
super().__setitem__(key, value)
|
459
|
+
```
|
460
|
+
|
461
|
+
Then:
|
462
|
+
|
463
|
+
```python
|
464
|
+
PositiveIntDict({"a": 1, "b": -2}) # → raises ValidationError
|
465
|
+
```
|
466
|
+
|
467
|
+
---
|
468
|
+
|
469
|
+
## ✅ Step 5. (Optional) Schema Control
|
470
|
+
|
471
|
+
If you want the schema for OpenAPI / JSONSchema to show up correctly, you can include:
|
472
|
+
|
473
|
+
```python
|
474
|
+
@classmethod
|
475
|
+
def __get_pydantic_core_schema__(cls, source_type, handler):
|
476
|
+
# Delegate to dict’s schema but use our own class for construction
|
477
|
+
schema = handler(dict)
|
478
|
+
schema["cls"] = cls
|
479
|
+
return schema
|
480
|
+
```
|
481
|
+
|
482
|
+
This step isn’t necessary for normal validation, only for schema generation.
|
483
|
+
|
484
|
+
---
|
485
|
+
|
486
|
+
### ✨ Summary
|
487
|
+
|
488
|
+
✅ For full Pydantic integration:
|
489
|
+
|
490
|
+
* Implement `MutableMapping` (as above)
|
491
|
+
* Use `TypeAdapter(dict[K, V])` for validating constructor input
|
492
|
+
* Optionally use `TypeAdapter(K)` / `TypeAdapter(V)` in `__setitem__` for incremental validation
|
493
|
+
* Subclasses automatically work in `BaseModel`s
|
494
|
+
|
495
|
+
---
|
496
|
+
|
497
|
+
Perfect — this is the last piece of the puzzle.
|
498
|
+
Let’s make your `BaseDict` **dump cleanly into JSON** when used inside a Pydantic model — without needing custom encoders or post-processing.
|
499
|
+
|
500
|
+
---
|
501
|
+
|
502
|
+
## 🧩 Goal
|
503
|
+
|
504
|
+
Given this model:
|
505
|
+
|
506
|
+
```python
|
507
|
+
class MyModel(BaseModel):
|
508
|
+
mapping: StrIntDict
|
509
|
+
```
|
510
|
+
|
511
|
+
we want:
|
512
|
+
|
513
|
+
```python
|
514
|
+
MyModel(mapping={"x": 1}).model_dump_json()
|
515
|
+
```
|
516
|
+
|
517
|
+
to produce:
|
518
|
+
|
519
|
+
```json
|
520
|
+
{"mapping": {"x": 1}}
|
521
|
+
```
|
522
|
+
|
523
|
+
and *not* something like
|
524
|
+
`{"mapping": "StrIntDict({'x': 1})"}` or a nested object.
|
525
|
+
|
526
|
+
---
|
527
|
+
|
528
|
+
## ✅ Approach
|
529
|
+
|
530
|
+
We can make this work automatically by:
|
531
|
+
|
532
|
+
1. Implementing the **`__iter__` and `.items()`** methods properly (Pydantic already looks for those when dumping objects that behave like dicts);
|
533
|
+
2. Optionally adding a **`__get_pydantic_json_schema__`** hook for schema correctness;
|
534
|
+
3. Implementing **`__iter__` → yields key-value pairs** and **`__getitem__`** properly (already done);
|
535
|
+
4. Defining **`__getattr__` or `__getstate__`** is *not* needed;
|
536
|
+
5. For extra clarity, you can also define `__pydantic_serializer__` but it’s not necessary here.
|
537
|
+
|
538
|
+
---
|
539
|
+
|
540
|
+
## ✅ Minimal Change Version (works out of the box)
|
541
|
+
|
542
|
+
If you use the earlier implementation of `BaseDict` that subclasses `MutableMapping` and returns a plain `dict` from `.to_dict()`, **it already serializes correctly**:
|
543
|
+
|
544
|
+
```python
|
545
|
+
from pydantic import BaseModel
|
546
|
+
import json
|
547
|
+
|
548
|
+
|
549
|
+
class StrIntDict(BaseDict[str, int]):
|
550
|
+
pass
|
551
|
+
|
552
|
+
|
553
|
+
class MyModel(BaseModel):
|
554
|
+
mapping: StrIntDict
|
555
|
+
|
556
|
+
|
557
|
+
m = MyModel(mapping={"a": "42"})
|
558
|
+
print(m.model_dump()) # {'mapping': {'a': 42}}
|
559
|
+
print(m.model_dump_json()) # {"mapping":{"a":42}}
|
560
|
+
```
|
561
|
+
|
562
|
+
That’s it. ✅
|
563
|
+
Because Pydantic’s serializer recognizes anything implementing `collections.abc.Mapping`.
|
564
|
+
|
565
|
+
---
|
566
|
+
|
567
|
+
## ✅ Optional Explicit Serializer (for clarity)
|
568
|
+
|
569
|
+
If you prefer to be explicit (especially if you add custom attributes to `BaseDict`), you can define a Pydantic serialization hook:
|
570
|
+
|
571
|
+
```python
|
572
|
+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
|
573
|
+
from pydantic_core import core_schema
|
574
|
+
|
575
|
+
|
576
|
+
class BaseDict(MutableMapping[K, V], Generic[K, V]):
|
577
|
+
...
|
578
|
+
# (all previous methods unchanged)
|
579
|
+
|
580
|
+
@classmethod
|
581
|
+
def __get_pydantic_core_schema__(
|
582
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
583
|
+
) -> core_schema.CoreSchema:
|
584
|
+
# This makes Pydantic serialize your class like a dict
|
585
|
+
return core_schema.no_info_after_validator_function(
|
586
|
+
cls, # after validation, return instance
|
587
|
+
handler(dict), # validate using dict schema
|
588
|
+
)
|
589
|
+
|
590
|
+
@classmethod
|
591
|
+
def __get_pydantic_json_schema__(
|
592
|
+
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
|
593
|
+
):
|
594
|
+
# Delegate schema generation to dict
|
595
|
+
json_schema = handler(core_schema)
|
596
|
+
json_schema.update(type="object")
|
597
|
+
return json_schema
|
598
|
+
```
|
599
|
+
|
600
|
+
Now your class will both **validate** and **serialize** as a mapping, but still **preserve its type** inside Python.
|
601
|
+
|
602
|
+
---
|
603
|
+
|
604
|
+
## ✅ Final Example
|
605
|
+
|
606
|
+
```python
|
607
|
+
from pydantic import BaseModel
|
608
|
+
|
609
|
+
|
610
|
+
class StrIntDict(BaseDict[str, int]):
|
611
|
+
pass
|
612
|
+
|
613
|
+
|
614
|
+
class MyModel(BaseModel):
|
615
|
+
mapping: StrIntDict
|
616
|
+
|
617
|
+
|
618
|
+
m = MyModel(mapping={"a": "42"})
|
619
|
+
print(m) # mapping=StrIntDict({'a': 42})
|
620
|
+
|
621
|
+
print(m.model_dump())
|
622
|
+
# → {'mapping': {'a': 42}}
|
623
|
+
|
624
|
+
print(m.model_dump_json(indent=2))
|
625
|
+
# → {
|
626
|
+
# "mapping": {
|
627
|
+
# "a": 42
|
628
|
+
# }
|
629
|
+
# }
|
630
|
+
```
|
631
|
+
|
632
|
+
---
|
633
|
+
|
634
|
+
## ✅ Summary
|
635
|
+
|
636
|
+
| Goal | Solution |
|
637
|
+
| ---------------------------------------- | ----------------------------------------------------- |
|
638
|
+
| Include dict-like custom types in models | Subclass `MutableMapping` |
|
639
|
+
| Validate keys/values | Use `TypeAdapter(dict[K, V])` or per-key adapters |
|
640
|
+
| Work inside `BaseModel` | Implement `__getitem__`, `__iter__`, `__len__` |
|
641
|
+
| Serialize to JSON cleanly | Ensure Mapping interface; optionally add schema hooks |
|
642
|
+
| Keep Python type fidelity | Return instance from `__get_pydantic_core_schema__` |
|
643
|
+
|
644
|
+
---
|
645
|
+
|
646
|
+
Excellent — this is a great enhancement.
|
647
|
+
You’ll end up with a `BaseDict` that not only validates its contents, but also produces *precise error reports*, e.g.:
|
648
|
+
|
649
|
+
```
|
650
|
+
pydantic_core._pydantic_core.ValidationError: 1 validation error for StrIntDict
|
651
|
+
mapping.a
|
652
|
+
Value error, not a positive integer [type=value_error, input_value='-2', ...]
|
653
|
+
```
|
654
|
+
|
655
|
+
Let's go step by step.
|
656
|
+
|
657
|
+
---
|
658
|
+
|
659
|
+
## ✅ Design Goal
|
660
|
+
|
661
|
+
We want:
|
662
|
+
|
663
|
+
1. Each key/value pair to be validated individually.
|
664
|
+
2. If any pair fails, Pydantic should raise a standard `ValidationError` listing which key caused the problem.
|
665
|
+
3. Work both when constructed directly and inside a `BaseModel`.
|
666
|
+
|
667
|
+
---
|
668
|
+
|
669
|
+
## ✅ Implementation: Detailed Error Tracking
|
670
|
+
|
671
|
+
```python
|
672
|
+
from __future__ import annotations
|
673
|
+
from collections.abc import MutableMapping
|
674
|
+
from typing import Any, Generic, TypeVar
|
675
|
+
from pydantic import TypeAdapter, ValidationError
|
676
|
+
from pydantic_core import ErrorDetails
|
677
|
+
|
678
|
+
K = TypeVar("K")
|
679
|
+
V = TypeVar("V")
|
680
|
+
|
681
|
+
|
682
|
+
class BaseDict(MutableMapping[K, V], Generic[K, V]):
|
683
|
+
"""A dict-like container that validates its contents with Pydantic and
|
684
|
+
reports per-key errors when validation fails."""
|
685
|
+
|
686
|
+
def __init__(self, data: Any | None = None):
|
687
|
+
self._data: dict[K, V] = {}
|
688
|
+
if data is not None:
|
689
|
+
self._data = self._validate_all(data)
|
690
|
+
|
691
|
+
def _validate_all(self, data: Any) -> dict[K, V]:
|
692
|
+
"""Validate all items and collect individual key/value errors."""
|
693
|
+
key_adapter = TypeAdapter(K)
|
694
|
+
val_adapter = TypeAdapter(V)
|
695
|
+
|
696
|
+
validated: dict[K, V] = {}
|
697
|
+
errors: list[ErrorDetails] = []
|
698
|
+
|
699
|
+
# We accept any mapping-like object
|
700
|
+
try:
|
701
|
+
items = data.items()
|
702
|
+
except AttributeError:
|
703
|
+
raise TypeError(f"Expected a mapping, got {type(data).__name__}")
|
704
|
+
|
705
|
+
for key, value in items:
|
706
|
+
try:
|
707
|
+
k = key_adapter.validate_python(key)
|
708
|
+
v = val_adapter.validate_python(value)
|
709
|
+
validated[k] = v
|
710
|
+
except ValidationError as e:
|
711
|
+
# Reformat to make it clear which key failed
|
712
|
+
for err in e.errors():
|
713
|
+
err["loc"] = ("mapping", key, *err["loc"])
|
714
|
+
errors.append(err)
|
715
|
+
|
716
|
+
if errors:
|
717
|
+
raise ValidationError.from_exception_data(self.__class__.__name__, errors)
|
718
|
+
|
719
|
+
return validated
|
720
|
+
|
721
|
+
# --- Mapping protocol ---
|
722
|
+
def __getitem__(self, key: K) -> V:
|
723
|
+
return self._data[key]
|
724
|
+
|
725
|
+
def __setitem__(self, key: K, value: V) -> None:
|
726
|
+
# Validate per-item on assignment
|
727
|
+
try:
|
728
|
+
k = TypeAdapter(K).validate_python(key)
|
729
|
+
v = TypeAdapter(V).validate_python(value)
|
730
|
+
self._data[k] = v
|
731
|
+
except ValidationError as e:
|
732
|
+
raise ValidationError.from_exception_data(
|
733
|
+
self.__class__.__name__,
|
734
|
+
[{"loc": ("mapping", key, *err["loc"]), **err} for err in e.errors()],
|
735
|
+
)
|
736
|
+
|
737
|
+
def __delitem__(self, key: K) -> None:
|
738
|
+
del self._data[key]
|
739
|
+
|
740
|
+
def __iter__(self):
|
741
|
+
return iter(self._data)
|
742
|
+
|
743
|
+
def __len__(self):
|
744
|
+
return len(self._data)
|
745
|
+
|
746
|
+
def __repr__(self):
|
747
|
+
return f"{self.__class__.__name__}({self._data!r})"
|
748
|
+
|
749
|
+
def to_dict(self) -> dict[K, V]:
|
750
|
+
return dict(self._data)
|
751
|
+
```
|
752
|
+
|
753
|
+
---
|
754
|
+
|
755
|
+
## ✅ Example: Subclass with Constraints
|
756
|
+
|
757
|
+
```python
|
758
|
+
class PositiveIntDict(BaseDict[str, int]):
|
759
|
+
def __setitem__(self, key, value):
|
760
|
+
v = TypeAdapter(int).validate_python(value)
|
761
|
+
if v <= 0:
|
762
|
+
raise ValidationError.from_exception_data(
|
763
|
+
self.__class__.__name__,
|
764
|
+
[
|
765
|
+
{
|
766
|
+
"type": "value_error",
|
767
|
+
"loc": ("mapping", key),
|
768
|
+
"msg": "must be positive",
|
769
|
+
"input": value,
|
770
|
+
}
|
771
|
+
],
|
772
|
+
)
|
773
|
+
super().__setitem__(key, v)
|
774
|
+
```
|
775
|
+
|
776
|
+
---
|
777
|
+
|
778
|
+
## ✅ Example Usage
|
779
|
+
|
780
|
+
```python
|
781
|
+
from pydantic import BaseModel
|
782
|
+
|
783
|
+
|
784
|
+
class MyModel(BaseModel):
|
785
|
+
mapping: PositiveIntDict
|
786
|
+
|
787
|
+
|
788
|
+
try:
|
789
|
+
m = MyModel(mapping={"a": 1, "b": "-3"})
|
790
|
+
except ValidationError as e:
|
791
|
+
print(e)
|
792
|
+
```
|
793
|
+
|
794
|
+
**Output:**
|
795
|
+
|
796
|
+
```
|
797
|
+
1 validation error for MyModel
|
798
|
+
mapping.b
|
799
|
+
must be positive [type=value_error, input_value='-3', input_type=str]
|
800
|
+
```
|
801
|
+
|
802
|
+
Each error points exactly to the failing key.
|
803
|
+
|
804
|
+
---
|
805
|
+
|
806
|
+
## ✅ Explanation
|
807
|
+
|
808
|
+
* We use `TypeAdapter(K)` and `TypeAdapter(V)` separately so that **keys and values** each get their own type coercion and validation.
|
809
|
+
* If any validation fails, we collect structured errors using `pydantic.ValidationError.errors()`.
|
810
|
+
* We inject a meaningful `"loc"` (location path) like `("mapping", key)` for each problematic entry — this is what makes Pydantic’s error output precise.
|
811
|
+
|
812
|
+
---
|
813
|
+
|
814
|
+
## ✅ Works Inside Models Too
|
815
|
+
|
816
|
+
This integrates seamlessly with any `BaseModel` field — Pydantic wraps any internal `ValidationError` raised by the constructor and merges it with the model’s error output.
|
817
|
+
|
818
|
+
```python
|
819
|
+
class M(BaseModel):
|
820
|
+
data: PositiveIntDict
|
821
|
+
|
822
|
+
|
823
|
+
M(data={"a": 1, "b": 0})
|
824
|
+
# → raises ValidationError with path: data.b
|
825
|
+
```
|
826
|
+
|
827
|
+
---
|
828
|
+
|
829
|
+
## ✅ Summary
|
830
|
+
|
831
|
+
| Feature | Implementation |
|
832
|
+
| --------------------------- | ------------------------------------------------- |
|
833
|
+
| Per-key/value validation | `TypeAdapter(K)` + `TypeAdapter(V)` |
|
834
|
+
| Aggregated error reporting | Collect errors and raise single `ValidationError` |
|
835
|
+
| Compatible with `BaseModel` | Works transparently |
|
836
|
+
| Clear error location | Each error labeled by failing key |
|
837
|
+
| JSON-safe serialization | Already works as a `Mapping` |
|
838
|
+
|
839
|
+
---
|