|
1 | | -"""Keep `inspect.signature()` accurate for `defer_build` models before their first use. |
| 1 | +"""Support for the SDK's `defer_build=True` pydantic models. |
2 | 2 |
|
3 | 3 | The SDK's model bases set `defer_build=True`, so pydantic builds a model's core |
4 | 4 | schema, validator and serializer on first use instead of while the class |
5 | 5 | statement runs; a module full of models then costs almost nothing to import in |
6 | | -a process that never validates them. |
7 | | -
|
8 | | -The one observable pydantic ties to that build is the class's `__signature__` |
9 | | -(the synthesized `__init__` signature that `inspect.signature(Model)` reports): |
10 | | -under `defer_build` it exists only once the model has built, and until then a |
11 | | -class reports the generic `(**data)`. `DeferredSignature` is a class-level |
12 | | -`__signature__` that completes the (one-time) build via the public |
13 | | -`model_rebuild()` on first signature access, so runtime introspection matches an |
14 | | -eagerly-built model while nothing is built at import. |
| 6 | +a process that never validates them. Two things need help until that first |
| 7 | +build happens: |
| 8 | +
|
| 9 | +* `inspect.signature(Model)`. Under `defer_build` the synthesized `__init__` |
| 10 | + signature (`__signature__`) exists only once the model has built; until then |
| 11 | + a class would report the generic `(**data)`. `DeferredSignature` is a |
| 12 | + class-level `__signature__` that completes the (one-time) build via the |
| 13 | + public `model_rebuild()` on first signature access, so runtime introspection |
| 14 | + matches an eagerly-built model while nothing is built at import. |
| 15 | +* The first build under concurrency. Released pydantic (<= 2.13) does not |
| 16 | + lock `model_rebuild()`, so several threads first-using one model at once |
| 17 | + (sync tools on worker threads, one session per thread) can raise |
| 18 | + `AttributeError: __pydantic_core_schema__` or install a stale validator |
| 19 | + (pydantic#13419; fixed unreleased in pydantic#13438). One process-wide |
| 20 | + reentrant lock around the build closes that window: a late thread waits, |
| 21 | + then finds the model complete and its own rebuild is a no-op. |
| 22 | +
|
| 23 | +`deferred_model` installs both on the root class of a deferred model hierarchy |
| 24 | +(the type-layer bases `MCPModel` / `WireModel` / `WireRootModel`, the JSON-RPC |
| 25 | +envelopes, and the `mcp` package's own model roots); subclasses inherit them. |
| 26 | +
|
| 27 | +This is a private module of `mcp-types`, but the `mcp` package (which |
| 28 | +exact-pins its `mcp-types` sibling) imports `deferred_model` for its own model |
| 29 | +roots too: keep the signatures stable across both packages. |
15 | 30 | """ |
16 | 31 |
|
17 | 32 | from __future__ import annotations |
18 | 33 |
|
| 34 | +import threading |
19 | 35 | from inspect import Signature |
20 | | -from typing import Any, TypeVar, cast |
| 36 | +from typing import Any, Final, TypeVar, cast |
21 | 37 |
|
22 | 38 | from pydantic import BaseModel |
23 | 39 |
|
24 | | -__all__ = ["DeferredSignature", "deferred_model", "install_deferred_signature", "new_deferred_signature"] |
| 40 | +__all__ = [ |
| 41 | + "REBUILD_LOCK", |
| 42 | + "DeferredSignature", |
| 43 | + "deferred_model", |
| 44 | + "install_deferred_signature", |
| 45 | + "new_deferred_signature", |
| 46 | +] |
25 | 47 |
|
26 | 48 | _ModelT = TypeVar("_ModelT", bound=BaseModel) |
27 | 49 |
|
| 50 | +REBUILD_LOCK: Final = threading.RLock() |
| 51 | +"""Serializes the first (deferred) build of every SDK model across threads. |
| 52 | +
|
| 53 | +Reentrant on purpose: completing one model rebuilds the models it references |
| 54 | +on the same thread, re-entering `model_rebuild()`. Held only while a model |
| 55 | +actually builds; a completed model's `model_rebuild()` returns before the |
| 56 | +lock, and no per-message path takes it. |
| 57 | +""" |
| 58 | + |
28 | 59 |
|
29 | 60 | class DeferredSignature: |
30 | 61 | """Class-level `__signature__` for a `defer_build=True` pydantic model. |
@@ -53,41 +84,80 @@ def __get__(self, obj: object | None, owner: type[BaseModel] | None = None) -> A |
53 | 84 | def new_deferred_signature() -> Signature: |
54 | 85 | """A `DeferredSignature` typed as the `Signature` it stands in for. |
55 | 86 |
|
56 | | - Assign it in a model class body WITHOUT an annotation (`__signature__ = |
57 | | - new_deferred_signature()`), matching how `BaseModel` itself binds |
58 | | - `__signature__`, so it never appears in `get_type_hints(Model)`. |
| 87 | + Bound in a class namespace WITHOUT an annotation (matching how `BaseModel` |
| 88 | + itself binds `__signature__`), so it never appears in `get_type_hints(Model)`. |
59 | 89 | """ |
60 | 90 | return cast(Signature, DeferredSignature()) |
61 | 91 |
|
62 | 92 |
|
63 | 93 | def install_deferred_signature(cls: type[BaseModel]) -> None: |
64 | 94 | """Give a still-deferred model class its own lazily-completing `__signature__`. |
65 | 95 |
|
66 | | - Meant for a base's `__pydantic_init_subclass__` hook: every deferred subclass |
67 | | - needs its OWN `__signature__` entry, otherwise it would inherit an |
68 | | - already-built parent's signature. A class that pydantic already completed |
69 | | - (e.g. one that turned `defer_build` off) keeps the real signature it has. |
| 96 | + Every deferred subclass needs its OWN `__signature__` entry, otherwise it |
| 97 | + would inherit an already-built parent's signature. A class that pydantic |
| 98 | + already completed (e.g. one that turned `defer_build` off) keeps the real |
| 99 | + signature it has. |
70 | 100 | """ |
71 | 101 | if not cls.__pydantic_complete__: |
72 | 102 | setattr(cls, "__signature__", new_deferred_signature()) |
73 | 103 |
|
74 | 104 |
|
75 | 105 | def deferred_model(cls: type[_ModelT]) -> type[_ModelT]: |
76 | | - """Class decorator for the root of a `defer_build` model hierarchy under `BaseModel`. |
77 | | -
|
78 | | - Installs the lazy `__signature__` on `cls` and, via a `__pydantic_init_subclass__` |
79 | | - hook, on every model that later subclasses it (a subclass inherits the deferred |
80 | | - build through the config). Models deriving from the SDK bases (`MCPModel`, |
81 | | - `WireModel`, `WireRootModel`) get this from the base and do not need it. |
| 106 | + """Class decorator for the root of a `defer_build=True` model hierarchy. |
| 107 | +
|
| 108 | + Installs, on `cls` and (through inheritance) on every model that later |
| 109 | + subclasses it - a subclass inherits the deferred build through the config: |
| 110 | +
|
| 111 | + * the lazy `__signature__`, via a `__pydantic_init_subclass__` hook that |
| 112 | + stamps a fresh one on each still-deferred subclass; |
| 113 | + * a `model_rebuild` classmethod that takes `REBUILD_LOCK`, so the first |
| 114 | + build of any model in the hierarchy is serialized across threads (public |
| 115 | + pydantic API; `_parent_namespace_depth + 1` accounts for this extra |
| 116 | + frame); |
| 117 | + * a `model_json_schema` classmethod that completes the (deferred) model |
| 118 | + under that lock before pydantic generates the schema, so concurrent first |
| 119 | + schema calls do not race on the not-yet-built class. |
| 120 | +
|
| 121 | + Raises: |
| 122 | + TypeError: `cls` does not set `defer_build=True` in its `model_config`, |
| 123 | + or defines its own `__pydantic_init_subclass__` (which the hook |
| 124 | + installed here would silently replace). |
82 | 125 | """ |
| 126 | + if not cls.model_config.get("defer_build"): |
| 127 | + raise TypeError(f"@deferred_model expects {cls.__name__} to set model_config['defer_build'] = True") |
| 128 | + if "__pydantic_init_subclass__" in vars(cls): |
| 129 | + raise TypeError(f"@deferred_model would replace {cls.__name__}'s own __pydantic_init_subclass__") |
| 130 | + |
83 | 131 | install_deferred_signature(cls) |
84 | 132 |
|
85 | 133 | def __pydantic_init_subclass__(sub: type[BaseModel], **kwargs: Any) -> None: |
86 | 134 | install_deferred_signature(sub) |
87 | | - # Run whatever hook `cls`'s own bases define (BaseModel's is a no-op). |
88 | | - # (`super()` from a classmethod defined outside the class body needs |
89 | | - # both arguments; the two-argument form is opaque to the type checker.) |
| 135 | + # Chain to whatever hook `cls`'s bases define (BaseModel's is a no-op). |
| 136 | + # (`super()` from a classmethod defined outside the class body needs the |
| 137 | + # two-argument form, which is opaque to the type checker.) |
90 | 138 | cast(Any, super(cls, sub)).__pydantic_init_subclass__(**kwargs) |
91 | 139 |
|
| 140 | + def model_rebuild(sub: type[BaseModel], *args: Any, _parent_namespace_depth: int = 2, **kwargs: Any) -> Any: |
| 141 | + # An already-built model is the common case: answer it like pydantic |
| 142 | + # does, without touching the lock. |
| 143 | + if sub.__pydantic_complete__ and not (args or kwargs.get("force")): |
| 144 | + return None |
| 145 | + # One process-wide lock serializes the deferred first build across |
| 146 | + # threads; a late thread finds the model complete and no-ops. |
| 147 | + with REBUILD_LOCK: |
| 148 | + return cast(Any, super(cls, sub)).model_rebuild( |
| 149 | + *args, _parent_namespace_depth=_parent_namespace_depth + 1, **kwargs |
| 150 | + ) |
| 151 | + |
| 152 | + def model_json_schema(sub: type[BaseModel], *args: Any, **kwargs: Any) -> Any: |
| 153 | + # Complete the class before pydantic reads its (mock) core schema: |
| 154 | + # pydantic re-reads that attribute around the build, which is not safe |
| 155 | + # against a concurrent first build; a complete class is only read. |
| 156 | + if not sub.__pydantic_complete__: |
| 157 | + sub.model_rebuild(raise_errors=False) |
| 158 | + return cast(Any, super(cls, sub)).model_json_schema(*args, **kwargs) |
| 159 | + |
92 | 160 | setattr(cls, "__pydantic_init_subclass__", classmethod(__pydantic_init_subclass__)) |
| 161 | + setattr(cls, "model_rebuild", classmethod(model_rebuild)) |
| 162 | + setattr(cls, "model_json_schema", classmethod(model_json_schema)) |
93 | 163 | return cls |
0 commit comments