Skip to content

Commit ac60559

Browse files
authored
test: Run the model generator in unit tests (#1082)
PR CI never runs `poe generate-models`. Only the nightly regeneration workflow does, so a `datamodel-code-generator` update that breaks generation can pass CI and automerge through Renovate. The first sign would be a failed nightly run. The new unit test runs both codegen passes with the repo's `[tool.datamodel-codegen]` config on a small spec, then the post-processing script. It checks the result: `docs_group` decorators, the camelCase alias generator, `AwareDatetime` for date-time, enums turned into open `Literal[...] | str` aliases in `_literals.py`, and TypedDict pruning plus camelCase twins. It runs in under a second. Same idea as apify/apify-client-js#1097. *✍️ Drafted by Claude Code*
1 parent fc3e096 commit ac60559

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import subprocess
5+
import sys
6+
from typing import TYPE_CHECKING
7+
8+
from scripts.postprocess_generated_models import (
9+
REPO_ROOT,
10+
RESOURCE_INPUT_TYPEDDICTS,
11+
build_alias_map,
12+
postprocess_models,
13+
postprocess_typeddicts,
14+
)
15+
16+
if TYPE_CHECKING:
17+
from pathlib import Path
18+
19+
MINIMAL_SPEC = {
20+
'openapi': '3.1.0',
21+
'info': {'title': 'Test', 'version': '1.0.0'},
22+
'paths': {},
23+
'components': {
24+
'schemas': {
25+
'RunStatus': {'type': 'string', 'enum': ['READY', 'RUNNING', 'SUCCEEDED']},
26+
'Run': {
27+
'type': 'object',
28+
'required': ['id', 'startedAt', 'status'],
29+
'properties': {
30+
'id': {'type': 'string'},
31+
'startedAt': {'type': 'string', 'format': 'date-time'},
32+
'status': {'$ref': '#/components/schemas/RunStatus'},
33+
},
34+
},
35+
# Post-processing fails unless every TypedDict seed is present in the generated file. Each gets its own
36+
# field, so `reuse_model` doesn't turn them into empty subclasses of one shared class.
37+
**{
38+
name: {
39+
'type': 'object',
40+
'required': [f'{name[0].lower()}{name[1:]}Id'],
41+
'properties': {f'{name[0].lower()}{name[1:]}Id': {'type': 'string'}},
42+
}
43+
for name in RESOURCE_INPUT_TYPEDDICTS
44+
},
45+
}
46+
},
47+
}
48+
49+
50+
def run_datamodel_codegen(*args: str) -> None:
51+
# Running from the repository root makes datamodel-codegen pick up `[tool.datamodel-codegen]` from `pyproject.toml`.
52+
subprocess.run( # noqa: S603
53+
[sys.executable, '-m', 'datamodel_code_generator', *args],
54+
check=True,
55+
cwd=REPO_ROOT,
56+
)
57+
58+
59+
def test_model_generation_pipeline(tmp_path: Path) -> None:
60+
"""The `generate-models` pipeline (both codegen passes and post-processing) runs on a minimal spec."""
61+
# PR CI never runs `poe generate-models`, so this catches a dependency update that breaks the generator.
62+
spec_path = tmp_path / 'openapi.json'
63+
spec_path.write_text(json.dumps(MINIMAL_SPEC))
64+
models_path = tmp_path / '_models.py'
65+
literals_path = tmp_path / '_literals.py'
66+
typeddicts_path = tmp_path / '_typeddicts.py'
67+
68+
run_datamodel_codegen('--input', str(spec_path), '--output', str(models_path), '--alias-generator', 'to_camel')
69+
run_datamodel_codegen(
70+
'--input',
71+
str(spec_path),
72+
'--output',
73+
str(typeddicts_path),
74+
'--output-model-type',
75+
'typing.TypedDict',
76+
'--no-use-closed-typed-dict',
77+
)
78+
postprocess_models(models_path, literals_path)
79+
postprocess_typeddicts(typeddicts_path, build_alias_map(models_path.read_text()))
80+
81+
models = models_path.read_text()
82+
assert "@docs_group('Models')\nclass Run(BaseModel):" in models
83+
assert 'alias_generator=to_camel' in models
84+
assert 'started_at: AwareDatetime' in models
85+
assert 'from apify_client._literals import RunStatus' in models
86+
assert 'class RunStatus' not in models
87+
88+
literals = literals_path.read_text()
89+
assert "RunStatus = Literal[\n 'READY',\n 'RUNNING',\n 'SUCCEEDED',\n] | str" in literals
90+
91+
typeddicts = typeddicts_path.read_text()
92+
assert 'class RequestDict(TypedDict):\n request_id: str' in typeddicts
93+
assert 'class RequestCamelDict(TypedDict):\n requestId: str' in typeddicts
94+
assert 'class Run' not in typeddicts

0 commit comments

Comments
 (0)