Skip to content

Commit e4dfda7

Browse files
committed
Do not parameterize RootModel bases in generated types
1 parent d2290ca commit e4dfda7

5 files changed

Lines changed: 123 additions & 236 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ docs = [
109109
# of the default groups so no other environment installs the client (or the
110110
# legacy httpx it depends on). See i18n/README.md.
111111
translate = ["anthropic>=0.121.0"]
112+
# Note: when bumping `datamodel-code-generator`, look at `scripts/codegen_templates/pydantic_v2/RootModel.jinja2`
113+
# if it needs updating:
112114
codegen = ["datamodel-code-generator==0.57.0"]
113115

114116
[build-system]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{#-
2+
Copy of datamodel-code-generator's `pydantic_v2/RootModel.jinja2` with two changes:
3+
4+
- The base `RootModel` class is left unparametrized instead of `RootModel[...]`.
5+
The class annotates `root` anyway, and every distinct `RootModel[...]` parametrization
6+
builds a model that is unused.
7+
- `description` and `field.docstring` go through the `format_docstring` filter. Codegen
8+
only pre-formats them for its built-in templates; custom templates receive the raw text.
9+
The 4-space indent matches codegen's `DOCSTRING_INDENT` / `FIELD_DOCSTRING_INDENT`.
10+
-#}
11+
{%- macro get_type_hint(_fields, use_base_type) -%}
12+
{%- if _fields -%}
13+
{#There will only ever be a single field for RootModel#}
14+
{%- if use_base_type -%}
15+
{{- _fields[0].base_type_hint}}
16+
{%- else -%}
17+
{{- _fields[0].type_hint}}
18+
{%- endif -%}
19+
{%- endif -%}
20+
{%- endmacro -%}
21+
22+
23+
{% for decorator in decorators -%}
24+
{{ decorator }}
25+
{% endfor -%}
26+
27+
{#- Use base_type_hint in generic when regex_engine is set to avoid evaluating pattern before config is processed -#}
28+
{%- set use_base_type = config and config.regex_engine -%}
29+
class {{ class_name }}({{ base_class }}): # pyright: ignore[reportMissingTypeArgument]{% if comment is defined %} # {{ comment }}{% endif %}
30+
{%- if description %}
31+
{{ description | format_docstring(4) }}
32+
{%- endif %}
33+
{%- if config %}
34+
{%- filter indent(4) %}
35+
{% include 'ConfigDict.jinja2' %}
36+
{%- endfilter %}
37+
{%- endif %}
38+
{%- for line in class_body_lines %}
39+
{{ line }}
40+
{%- endfor %}
41+
{%- if not fields and not description and not config and not class_body_lines %}
42+
pass
43+
{%- else %}
44+
{%- set field = fields[0] %}
45+
{%- if not field.annotated and field.field %}
46+
root: {{ field.type_hint }} = {{ field.field }}
47+
{%- else %}
48+
{%- if field.annotated %}
49+
root: {{ field.annotated }}
50+
{%- else %}
51+
root: {{ field.type_hint }}
52+
{%- endif %}
53+
{%- if not field.has_default_factory_in_field and not (field.required or (field.represented_default == 'None' and field.strip_default_none))
54+
%} = {{ field.represented_default }}
55+
{%- endif -%}
56+
{%- endif %}
57+
{%- if field.docstring %}
58+
{{ field.docstring | format_docstring(4) }}
59+
{%- elif field.inline_field_docstring %}
60+
{{ field.inline_field_docstring }}
61+
62+
{%- endif %}
63+
{%- endif %}

scripts/gen_surface_types.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
REPO_ROOT = Path(__file__).resolve().parent.parent
2626
SCHEMA_DIR = REPO_ROOT / "schema"
2727
TYPES_DIR = REPO_ROOT / "src" / "mcp-types" / "mcp_types"
28+
TEMPLATE_DIR = REPO_ROOT / "scripts" / "codegen_templates"
2829

2930
# The result-meta serverInfo stamp: every `$defs` entry carrying this property
3031
# gets its typed `$ref` stripped by `make_server_info_opaque` below.
@@ -191,6 +192,7 @@ def run_codegen(schema_path: Path, output_path: Path) -> None:
191192
"--output-model-type", "pydantic_v2.BaseModel",
192193
"--target-python-version", "3.10",
193194
"--base-class", "mcp_types._wire_base.WireModel",
195+
"--custom-template-dir", str(TEMPLATE_DIR),
194196
"--snake-case-field", "--remove-special-field-name-prefix",
195197
"--use-annotated", "--use-field-description", "--use-schema-description",
196198
"--enum-field-as-literal", "all",
@@ -246,7 +248,13 @@ def build(entry: dict[str, str]) -> str:
246248
source = raw.read_text(encoding="utf-8")
247249

248250
source = re.sub(r"\A# generated by datamodel-codegen:\n#[^\n]*\n", "", source)
249-
source = re.sub(r"^class Model\(RootModel\[Any\]\):\n {4}root: Any\n+", "", source, count=1, flags=re.MULTILINE)
251+
source = re.sub(
252+
r"^class Model\(RootModel\): # pyright: ignore\[reportMissingTypeArgument\]\n {4}root: Any\n+",
253+
"",
254+
source,
255+
count=1,
256+
flags=re.MULTILINE,
257+
)
250258
# Codegen appends `| None` to forward refs of nullable models, which is a
251259
# runtime TypeError on a string ref and redundant since `JSONValue` includes None.
252260
source = source.replace('"JSONValue" | None', '"JSONValue"')

0 commit comments

Comments
 (0)