adiumentum 0.1.1__py3-none-any.whl → 0.3.1__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.
@@ -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
+ ---