setspec 0.2.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.
- setspec/__about__.py +3 -0
- setspec/__init__.py +114 -0
- setspec/artifacts.py +4 -0
- setspec/base.py +256 -0
- setspec/benchmark/v1.py +594 -0
- setspec/capability/v1.py +394 -0
- setspec/envelope.py +468 -0
- setspec/error/v1.py +4 -0
- setspec/errors.py +68 -0
- setspec/event/v1.py +4 -0
- setspec/goal/v1.py +390 -0
- setspec/goldens/.gitkeep +0 -0
- setspec/machine/v1.py +131 -0
- setspec/metrics.py +184 -0
- setspec/model/v1.py +169 -0
- setspec/provenance.py +59 -0
- setspec/py.typed +0 -0
- setspec/schemas/.gitkeep +0 -0
- setspec/serialization.py +330 -0
- setspec/vocabulary.py +205 -0
- setspec-0.2.0.dist-info/METADATA +160 -0
- setspec-0.2.0.dist-info/RECORD +24 -0
- setspec-0.2.0.dist-info/WHEEL +4 -0
- setspec-0.2.0.dist-info/licenses/LICENSE +201 -0
setspec/__about__.py
ADDED
setspec/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""setspec — every versioned data contract that crosses an application boundary.
|
|
2
|
+
|
|
3
|
+
Layer 2: contract types built on :mod:`baseaicore`'s vocabulary and pydantic. No I/O, no
|
|
4
|
+
configuration, no logging, no HTTP, and no behaviour beyond structural and range validation. This
|
|
5
|
+
package answers "is this a well-formed result?" — never "is this a *good* result?", which belongs
|
|
6
|
+
to the application that computed it ([spec §3](../../docs/packages/setspec/spec.md)).
|
|
7
|
+
|
|
8
|
+
What is exported below is every piece of **shared, non-versioned** infrastructure as of Phase 2
|
|
9
|
+
(``docs/packages/setspec/development-plan.md``): the schema envelope, version parsing and the
|
|
10
|
+
reader policy; canonical serialization with the ``Unsupported`` and RFC 3339 codecs; the strict and
|
|
11
|
+
preserving payload bases with the generator that pairs them; ``MetricValue``; and the capability
|
|
12
|
+
vocabulary. Anything not listed in ``__all__`` is private and may change without a version bump.
|
|
13
|
+
|
|
14
|
+
**Payload types are not re-exported here.** ``model.identity``, ``machine.profile``,
|
|
15
|
+
``benchmark.result``, ``benchmark.run_summary``, ``capability.evidence`` and
|
|
16
|
+
``benchmark.evidence_bundle`` (Phase 2), and ``benchmark.goal_pack`` /
|
|
17
|
+
``benchmark.calibration_report`` (ADR-0031), live in their own versioned modules —
|
|
18
|
+
``setspec.model.v1``, ``setspec.machine.v1``, ``setspec.benchmark.v1``, ``setspec.capability.v1``,
|
|
19
|
+
``setspec.goal.v1`` — and are imported from there, e.g.
|
|
20
|
+
``from setspec.capability.v1 import CapabilityEvidenceOut``.
|
|
21
|
+
This is not an oversight: ADR-0009 rule 6
|
|
22
|
+
requires a v1 payload to remain importable as ``setspec.benchmark.v1`` for a deprecation window
|
|
23
|
+
after ``benchmark.result 2.0`` ships, which only works if ``v1`` and ``v2`` are different modules
|
|
24
|
+
rather than two classes racing for one flat name. ``MetricValue`` and the vocabulary helpers below
|
|
25
|
+
are re-exported flatly because neither is itself a versioned wire payload — they are shared
|
|
26
|
+
infrastructure a payload's fields are built from, exactly like the envelope and serialization
|
|
27
|
+
helpers above them.
|
|
28
|
+
|
|
29
|
+
>>> from setspec import GeneratorInfo, SchemaVersion, dump_envelope
|
|
30
|
+
>>> generator = GeneratorInfo(name="freeweight", version="1.0.0")
|
|
31
|
+
>>> document = dump_envelope(
|
|
32
|
+
... {"tokens_per_second": 42.0},
|
|
33
|
+
... schema="benchmark.result",
|
|
34
|
+
... version=SchemaVersion(1, 0),
|
|
35
|
+
... generator=generator,
|
|
36
|
+
... )
|
|
37
|
+
|
|
38
|
+
The two halves of every payload type are the design, not an accident: a writer uses the ``Out``
|
|
39
|
+
class and may emit only fields it knows; a reader uses the ``In`` class and keeps fields it does
|
|
40
|
+
not (ADR-0009 rule 4). That is what lets a v1.0
|
|
41
|
+
reader carry a v1.1 document through a re-export without destroying the parts it cannot interpret.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from setspec.__about__ import __version__
|
|
47
|
+
from setspec.base import (
|
|
48
|
+
PayloadDefinition,
|
|
49
|
+
PreservingPayload,
|
|
50
|
+
StrictPayload,
|
|
51
|
+
WireEnum,
|
|
52
|
+
payload_models,
|
|
53
|
+
)
|
|
54
|
+
from setspec.envelope import (
|
|
55
|
+
DRAFT_SCHEMAS,
|
|
56
|
+
SUPPORTED_SCHEMAS,
|
|
57
|
+
GeneratorInfo,
|
|
58
|
+
SchemaEnvelope,
|
|
59
|
+
SchemaVersion,
|
|
60
|
+
dump_envelope,
|
|
61
|
+
load_envelope,
|
|
62
|
+
)
|
|
63
|
+
from setspec.errors import SchemaVersionUnsupported, ValidationError
|
|
64
|
+
from setspec.metrics import Aggregation, MetricValueFields, MetricValueIn, MetricValueOut
|
|
65
|
+
from setspec.serialization import (
|
|
66
|
+
MAX_PAYLOAD_BYTES,
|
|
67
|
+
MAX_PAYLOAD_DEPTH,
|
|
68
|
+
UNSUPPORTED_JSON,
|
|
69
|
+
MeasurementField,
|
|
70
|
+
TimestampField,
|
|
71
|
+
canonical_dumps,
|
|
72
|
+
parse_json,
|
|
73
|
+
)
|
|
74
|
+
from setspec.vocabulary import (
|
|
75
|
+
CAPABILITIES,
|
|
76
|
+
CAPABILITY_VOCABULARY_VERSION,
|
|
77
|
+
RESERVED_ROOTS,
|
|
78
|
+
is_known_capability,
|
|
79
|
+
validate_capability,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"CAPABILITIES",
|
|
84
|
+
"CAPABILITY_VOCABULARY_VERSION",
|
|
85
|
+
"DRAFT_SCHEMAS",
|
|
86
|
+
"MAX_PAYLOAD_BYTES",
|
|
87
|
+
"MAX_PAYLOAD_DEPTH",
|
|
88
|
+
"RESERVED_ROOTS",
|
|
89
|
+
"SUPPORTED_SCHEMAS",
|
|
90
|
+
"UNSUPPORTED_JSON",
|
|
91
|
+
"Aggregation",
|
|
92
|
+
"GeneratorInfo",
|
|
93
|
+
"MeasurementField",
|
|
94
|
+
"MetricValueFields",
|
|
95
|
+
"MetricValueIn",
|
|
96
|
+
"MetricValueOut",
|
|
97
|
+
"PayloadDefinition",
|
|
98
|
+
"PreservingPayload",
|
|
99
|
+
"SchemaEnvelope",
|
|
100
|
+
"SchemaVersion",
|
|
101
|
+
"SchemaVersionUnsupported",
|
|
102
|
+
"StrictPayload",
|
|
103
|
+
"TimestampField",
|
|
104
|
+
"ValidationError",
|
|
105
|
+
"WireEnum",
|
|
106
|
+
"__version__",
|
|
107
|
+
"canonical_dumps",
|
|
108
|
+
"dump_envelope",
|
|
109
|
+
"is_known_capability",
|
|
110
|
+
"load_envelope",
|
|
111
|
+
"parse_json",
|
|
112
|
+
"payload_models",
|
|
113
|
+
"validate_capability",
|
|
114
|
+
]
|
setspec/artifacts.py
ADDED
setspec/base.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Contract module — the two payload base classes and the generator that pairs them.
|
|
2
|
+
|
|
3
|
+
Imports pydantic; performs no I/O and reads no clock.
|
|
4
|
+
|
|
5
|
+
ADR-0009 rule 4 requires every payload type to
|
|
6
|
+
exist as **two** classes generated from one definition, and the reason is a specific failure this
|
|
7
|
+
suite refuses to ship:
|
|
8
|
+
|
|
9
|
+
* A writer must emit only fields it knows, or it silently exports garbage it cannot explain.
|
|
10
|
+
* A reader must not *strip* fields it does not know, or an old tool re-exporting a new document
|
|
11
|
+
quietly destroys data — the sort of loss nobody notices until the original is gone.
|
|
12
|
+
|
|
13
|
+
Those two rules contradict each other in one class, so there are two: :class:`StrictPayload`
|
|
14
|
+
refuses unknown keys, :class:`PreservingPayload` keeps them. The round-trip contract
|
|
15
|
+
(``load(dump(x)) == x``, spec §11.3) is asserted **per class** — never across the pair, because
|
|
16
|
+
the pair is not meant to agree about unknown keys. That is the whole design.
|
|
17
|
+
|
|
18
|
+
Both bases are strict about types. Pydantic's default coercion would read ``"5"`` as ``5`` and
|
|
19
|
+
``5.0`` as ``5``, which is exactly the silent coercion spec §13 forbids: on a wire contract a
|
|
20
|
+
string where a number belongs is a producer bug, and hiding it makes the bug arrive somewhere
|
|
21
|
+
further away. Widening ``int`` to ``float`` is still allowed, because JSON writes ``1.0`` as ``1``
|
|
22
|
+
and refusing that would reject valid documents.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from enum import StrEnum
|
|
28
|
+
from types import MappingProxyType
|
|
29
|
+
from typing import TYPE_CHECKING, Annotated, Any, Final, cast
|
|
30
|
+
|
|
31
|
+
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Mapping
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"PayloadDefinition",
|
|
38
|
+
"PreservingPayload",
|
|
39
|
+
"StrictPayload",
|
|
40
|
+
"WireEnum",
|
|
41
|
+
"WireSequence",
|
|
42
|
+
"payload_models",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_SHARED_CONFIG: Final[ConfigDict] = ConfigDict(
|
|
46
|
+
# No silent coercion: a wire contract that repairs its input hides the producer's defect
|
|
47
|
+
# (spec §13). `int` still widens to `float`, which JSON requires.
|
|
48
|
+
strict=True,
|
|
49
|
+
# Payloads are documents, not state. A loaded payload that some later code mutates is a
|
|
50
|
+
# document that no longer matches the bytes it came from.
|
|
51
|
+
frozen=True,
|
|
52
|
+
# Field order in the model is not the wire order — canonical JSON sorts keys — but a stable
|
|
53
|
+
# declaration order keeps generated JSON Schema diffs readable between releases.
|
|
54
|
+
populate_by_name=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PayloadDefinition(BaseModel):
|
|
59
|
+
"""Base for a payload's field declarations, and the common ancestor of both generated halves.
|
|
60
|
+
|
|
61
|
+
A definition class is not itself a payload: it carries the fields and says nothing about
|
|
62
|
+
unknown keys, which is the one thing the two generated halves must disagree about. Subclass it
|
|
63
|
+
to declare fields, then hand it to :func:`payload_models`::
|
|
64
|
+
|
|
65
|
+
class MetricValueFields(PayloadDefinition):
|
|
66
|
+
unit: str
|
|
67
|
+
value: MeasurementField
|
|
68
|
+
|
|
69
|
+
Both halves inherit from it as well, which is what lets :func:`payload_models` return a pair
|
|
70
|
+
typed as the definition itself. Without a common ancestor a type checker would see the
|
|
71
|
+
generated classes as bare bases with no fields, and every consumer of a payload — in this
|
|
72
|
+
repository and in the three applications — would need a cast to read one.
|
|
73
|
+
|
|
74
|
+
**Nesting one payload inside another.** A field can be typed directly as another definition —
|
|
75
|
+
``model: ModelIdentityFields`` inside ``CapabilityEvidenceFields`` — rather than as that
|
|
76
|
+
definition's generated ``Out`` or ``In``. This is deliberate, not an oversight: the outer
|
|
77
|
+
payload's own Out/In split already governs whether *the document as a whole* may carry an
|
|
78
|
+
unknown field, and ADR-0009 rule 4's guarantee that matters most is that a reader never
|
|
79
|
+
*loses* data. A definition's own ``extra="allow"`` (below) makes every nested payload
|
|
80
|
+
preserving by default in both directions — a writer embedding a stray field one level down is
|
|
81
|
+
a narrower gap than a reader silently discarding one, and accepting that narrower gap avoids
|
|
82
|
+
generating a full, independently-versioned Out/In pair for every sub-structure that is never
|
|
83
|
+
transmitted on its own.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(**_SHARED_CONFIG, extra="allow")
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def extras(self) -> Mapping[str, Any]:
|
|
90
|
+
"""Return the keys this build did not recognise, as a read-only mapping.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Every key present in the source document that is not a declared field, in the order
|
|
94
|
+
the document listed them. Always empty on a :class:`StrictPayload`, which refuses
|
|
95
|
+
unknown keys outright — it is defined on both halves so that code reading a payload
|
|
96
|
+
need not know which half it was handed.
|
|
97
|
+
|
|
98
|
+
The mapping is a read-only view: the payload is frozen, and handing out a mutable
|
|
99
|
+
reference to its interior would make that promise false.
|
|
100
|
+
"""
|
|
101
|
+
return MappingProxyType(self.__pydantic_extra__ or {})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StrictPayload(PayloadDefinition):
|
|
105
|
+
"""Base for the **outbound** half of a payload pair: writers use this.
|
|
106
|
+
|
|
107
|
+
Refuses unknown keys. A writer that hands this model a field the schema does not define has
|
|
108
|
+
either misspelled a name or is emitting a version it has not declared, and both are defects
|
|
109
|
+
worth failing on at the point of construction rather than discovering in an export months
|
|
110
|
+
later (ADR-0009 rule 5).
|
|
111
|
+
|
|
112
|
+
Invariants:
|
|
113
|
+
* Immutable after construction (``frozen``), so a serialized document always matches the
|
|
114
|
+
object it came from.
|
|
115
|
+
* Strictly typed: no string-to-number or float-to-int coercion.
|
|
116
|
+
* ``load(dump(x)) == x`` for every instance. There are no unknown keys to preserve, so the
|
|
117
|
+
contract is simply that nothing is lost or reinterpreted.
|
|
118
|
+
|
|
119
|
+
Not hashable in general: equality is by field value, and a field holding a mapping makes the
|
|
120
|
+
instance unhashable exactly as a frozen dataclass containing a dict would be.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(**_SHARED_CONFIG, extra="forbid")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PreservingPayload(PayloadDefinition):
|
|
127
|
+
"""Base for the **inbound** half of a payload pair: readers use this.
|
|
128
|
+
|
|
129
|
+
Keeps unknown keys instead of dropping them, and re-emits them on dump. This is what makes a
|
|
130
|
+
v1.0 reader safe to put in front of a v1.1 document: it validates the fields it knows, carries
|
|
131
|
+
the ones it does not, and a re-export loses nothing
|
|
132
|
+
(ADR-0009 rule 4).
|
|
133
|
+
|
|
134
|
+
The relaxation is bounded and deliberate. ``extra="allow"`` weakens strictness on input, which
|
|
135
|
+
is the documented cost of preservation across a version gap; every *known* field is still
|
|
136
|
+
validated strictly, so an unknown key can add information but can never change the meaning of
|
|
137
|
+
a field this build already understands.
|
|
138
|
+
|
|
139
|
+
Invariants:
|
|
140
|
+
* Immutable after construction (``frozen``).
|
|
141
|
+
* Known fields strictly typed; unknown keys kept verbatim and reachable through
|
|
142
|
+
:attr:`extras`.
|
|
143
|
+
* ``load(dump(x)) == x`` for every instance, unknown keys included.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
model_config = ConfigDict(**_SHARED_CONFIG, extra="allow")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
type WireEnum[EnumT: StrEnum] = Annotated[EnumT, Field(strict=False)]
|
|
150
|
+
"""A :class:`~enum.StrEnum` field that accepts its own member values from the wire.
|
|
151
|
+
|
|
152
|
+
Needed because the bases validate strictly, and strict mode in Python mode wants an actual enum
|
|
153
|
+
*instance* — while a document that has just been parsed from JSON holds the plain string
|
|
154
|
+
``"mean"``. Since :func:`~setspec.envelope.load_envelope` hands back exactly such a mapping, a
|
|
155
|
+
plainly annotated enum field would reject every real document while passing every hand-built test,
|
|
156
|
+
which is the worst possible place for a validation rule to be wrong.
|
|
157
|
+
|
|
158
|
+
Relaxing strictness here costs nothing: membership is still exact, so an unknown name, an integer
|
|
159
|
+
index and a bool are all still refused. Use it for every enum field in every payload::
|
|
160
|
+
|
|
161
|
+
aggregation: WireEnum[Aggregation]
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _coerce_to_tuple(value: object) -> object:
|
|
166
|
+
"""Convert a list or tuple to a tuple; pass anything else through for pydantic to reject.
|
|
167
|
+
|
|
168
|
+
JSON has one sequence type, and it deserializes to a Python ``list``. Strict mode validates
|
|
169
|
+
the *input Python type* against the annotation, and a bare ``tuple[T, ...]`` under
|
|
170
|
+
``strict=True`` accepts only an actual ``tuple`` — never the ``list`` every real document
|
|
171
|
+
supplies — so every ordered-collection field would reject every real document without this.
|
|
172
|
+
"""
|
|
173
|
+
if isinstance(value, list | tuple):
|
|
174
|
+
return tuple(value)
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
type WireSequence[T] = Annotated[tuple[T, ...], BeforeValidator(_coerce_to_tuple)]
|
|
179
|
+
"""An ordered collection field that accepts a JSON array and stores it as an immutable tuple.
|
|
180
|
+
|
|
181
|
+
Needed for the same reason as :data:`WireEnum`, one layer down: JSON arrays deserialize to
|
|
182
|
+
``list``, but a payload is a document (frozen, and meant to stay that way all the way down), so
|
|
183
|
+
the field itself is a ``tuple`` — a stored ``list`` would leave a mutable object reachable through
|
|
184
|
+
an otherwise-immutable model. Element types are still validated strictly and individually; only
|
|
185
|
+
the *container's own type* is relaxed, exactly as :data:`WireEnum` relaxes only the enum's own
|
|
186
|
+
strictness. Use it for every ordered-collection field in every payload::
|
|
187
|
+
|
|
188
|
+
gpus: WireSequence[GpuProfileFields] = ()
|
|
189
|
+
source_run_ids: WireSequence[str] = ()
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def payload_models[DefinitionT: PayloadDefinition](
|
|
194
|
+
definition: type[DefinitionT],
|
|
195
|
+
*,
|
|
196
|
+
name: str | None = None,
|
|
197
|
+
) -> tuple[type[DefinitionT], type[DefinitionT]]:
|
|
198
|
+
"""Generate the ``Out``/``In`` class pair for one payload definition.
|
|
199
|
+
|
|
200
|
+
One definition, two classes, so the field list cannot drift between what writers emit and what
|
|
201
|
+
readers accept — the drift ADR-0009 rule 4 exists to prevent. Write the fields once::
|
|
202
|
+
|
|
203
|
+
class MetricValueFields(BaseModel):
|
|
204
|
+
unit: str
|
|
205
|
+
value: MeasurementField
|
|
206
|
+
|
|
207
|
+
MetricValueOut, MetricValueIn = payload_models(MetricValueFields)
|
|
208
|
+
|
|
209
|
+
``MetricValueOut`` refuses unknown keys; ``MetricValueIn`` preserves them. Neither inherits the
|
|
210
|
+
other, and the definition itself is never used as a payload — it carries fields, not a policy
|
|
211
|
+
about unknown keys.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
definition: A pydantic model holding the field declarations. Its own ``model_config`` is
|
|
215
|
+
honoured except where it would contradict the base's guarantees: ``extra``, ``strict``
|
|
216
|
+
and ``frozen`` always come from the generated class, since those three *are* the
|
|
217
|
+
contract being generated.
|
|
218
|
+
name: Stem for the generated class names, which become ``<stem>Out`` and ``<stem>In``.
|
|
219
|
+
Defaults to the definition's own name with a trailing ``Fields`` or ``Definition``
|
|
220
|
+
removed, so ``MetricValueFields`` yields ``MetricValueOut`` and ``MetricValueIn``.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The ``(Out, In)`` pair, in that order — the order they are used in: a producer writes
|
|
224
|
+
``Out``, a consumer reads ``In``. Both are typed as the definition, so a type checker sees
|
|
225
|
+
the declared fields; the halves differ in the one way a type cannot express, which is what
|
|
226
|
+
they do with a key the definition never declared.
|
|
227
|
+
"""
|
|
228
|
+
stem = name if name is not None else _stem_of(definition.__name__)
|
|
229
|
+
return (
|
|
230
|
+
_generate(definition, StrictPayload, f"{stem}Out"),
|
|
231
|
+
_generate(definition, PreservingPayload, f"{stem}In"),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _stem_of(class_name: str) -> str:
|
|
236
|
+
"""Strip a definition class's naming suffix to get the payload's real name."""
|
|
237
|
+
for suffix in ("Fields", "Definition"):
|
|
238
|
+
if class_name.endswith(suffix) and len(class_name) > len(suffix):
|
|
239
|
+
return class_name[: -len(suffix)]
|
|
240
|
+
return class_name
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _generate[DefinitionT: PayloadDefinition](
|
|
244
|
+
definition: type[DefinitionT],
|
|
245
|
+
base: type[PayloadDefinition],
|
|
246
|
+
class_name: str,
|
|
247
|
+
) -> type[DefinitionT]:
|
|
248
|
+
"""Build one generated class from a definition and a base, with the base's config winning."""
|
|
249
|
+
# The definition's config is kept for anything cosmetic (title, json_schema_extra) but the
|
|
250
|
+
# base's `extra`/`strict`/`frozen` are re-applied on top: those three are the guarantee the
|
|
251
|
+
# base exists to make, and a definition must not be able to opt out of them by accident.
|
|
252
|
+
# `ConfigDict` is a TypedDict, so a runtime merge of two of them is a plain dict as far as
|
|
253
|
+
# the type checker is concerned; the cast restates what the merge preserves.
|
|
254
|
+
merged = cast("ConfigDict", {**dict(definition.model_config), **dict(base.model_config)})
|
|
255
|
+
generated = type(class_name, (definition, base), {"model_config": merged})
|
|
256
|
+
return cast("type[DefinitionT]", generated)
|