Skip to content

Commit aa43e5e

Browse files
committed
test(models): add Media/Checkout/open-union tests and strict type annotations
1 parent b011be4 commit aa43e5e

6 files changed

Lines changed: 594 additions & 94 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
node_modules/
22
.ruff_cache/
3+
.mypy_cache/
4+
.pytest_cache/
5+
dist/
36
__pycache__/
47
.venv/
58
*.py[cod]

‎postprocess_models.py‎

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167

168168
_VALIDATOR_TEMPLATE = '''
169169
@model_validator(mode="after")
170-
def {marker}(self):
170+
def {marker}(self) -> Any:
171171
"""JSON Schema minProperties: require at least {minimum}
172172
provided {properties_noun}."""
173173
provided = self.model_fields_set | set(self.model_extra or {{}})
@@ -183,7 +183,7 @@ def {marker}(self):
183183

184184
_MAX_VALIDATOR_TEMPLATE = '''
185185
@model_validator(mode="after")
186-
def {marker}(self):
186+
def {marker}(self) -> Any:
187187
"""JSON Schema maxProperties: allow at most {maximum}
188188
provided {properties_noun}."""
189189
provided = self.model_fields_set | set(self.model_extra or {{}})
@@ -199,7 +199,7 @@ def {marker}(self):
199199

200200
_PROPNAMES_VALIDATOR_TEMPLATE = '''
201201
@model_validator(mode="after")
202-
def {marker}(self):
202+
def {marker}(self) -> Any:
203203
"""JSON Schema propertyNames: every extra key must match the
204204
declared reverse-domain pattern (schema propertyNames)."""
205205
pattern = {pattern!r}
@@ -216,7 +216,7 @@ def {marker}(self):
216216

217217
_DEPENDENT_REQUIRED_TEMPLATE = '''
218218
@model_validator(mode="after")
219-
def {marker}(self):
219+
def {marker}(self) -> Any:
220220
"""JSON Schema dependentRequired: enforce dependent fields."""
221221
rules = {rules!r}
222222
provided = self.model_fields_set | set(self.model_extra or {{}})
@@ -238,9 +238,9 @@ def {marker}(self):
238238

239239
_CONDITIONAL_REQUIRED_TEMPLATE = '''
240240
@model_validator(mode="after")
241-
def {marker}(self):
241+
def {marker}(self) -> Any:
242242
"""JSON Schema if/then: enforce conditionally required fields."""
243-
rules = {rules!r}
243+
rules: list[dict[str, Any]] = {rules!r}
244244
for rule in rules:
245245
if getattr(self, rule["discriminator"], None) not in rule["values"]:
246246
continue
@@ -274,9 +274,9 @@ def {marker}(self):
274274

275275
_CONDITIONAL_BOUNDS_TEMPLATE = '''
276276
@model_validator(mode="after")
277-
def {marker}(self):
277+
def {marker}(self) -> Any:
278278
"""JSON Schema if/then: enforce conditional numeric bounds."""
279-
rules = {rules!r}
279+
rules: list[dict[str, Any]] = {rules!r}
280280
checks = {checks!r}
281281
for rule in rules:
282282
actual = getattr(self, rule["discriminator"], None)
@@ -300,11 +300,11 @@ def {marker}(self):
300300

301301
_RETYPE_TEMPLATE = '''
302302
@model_validator(mode="after")
303-
def {marker}(self):
303+
def {marker}(self) -> Any:
304304
"""JSON Schema if/then: approximate a discriminator's array-item
305305
retyping to a different referenced schema, via that schema's own
306306
required keys and const-pinned fields."""
307-
rules = {rules!r}
307+
rules: list[dict[str, Any]] = {rules!r}
308308
for rule in rules:
309309
actual = getattr(self, rule["discriminator"], None)
310310
if actual not in rule["values"]:
@@ -339,11 +339,11 @@ def {marker}(self):
339339

340340
_UNIQUE_VALIDATOR_TEMPLATE = '''
341341
@field_validator("{field}", mode="after")
342-
def {marker}_{field}(cls, value): # noqa: N805
342+
def {marker}_{field}(cls, value: Any) -> Any: # noqa: N805
343343
"""JSON Schema uniqueItems: reject duplicate entries."""
344344
if value is None:
345345
return value
346-
seen = []
346+
seen: list[Any] = []
347347
for item in value:
348348
if item in seen:
349349
raise ValueError(
@@ -362,7 +362,7 @@ def {marker}_{field}(cls, value): # noqa: N805
362362
# date-time pattern UCP uses is anchored only at the end.
363363
_DATETIME_PATTERN_TEMPLATE = '''
364364
@field_validator("{field}", mode="before")
365-
def {marker}_{field}(cls, value): # noqa: N805
365+
def {marker}_{field}(cls, value: Any) -> Any: # noqa: N805
366366
"""JSON Schema pattern: match the raw date-time string, since
367367
pydantic cannot apply a regex to the parsed datetime."""
368368
pattern = {pattern!r}
@@ -843,7 +843,7 @@ def _predicate_expr(pairs):
843843
def _build_contains_function(func_name, groups, item_condition=None):
844844
"""Render the module-level ``AfterValidator`` counting function."""
845845
lines = [
846-
f"def {func_name}(value):",
846+
f"def {func_name}(value: list[Any]) -> list[Any]:",
847847
' """JSON Schema contains/minContains/maxContains (see #49)."""',
848848
]
849849
for index, group in enumerate(groups):
@@ -949,7 +949,9 @@ def inject_array_contains(source, alias_name, groups, item_condition=None):
949949
addition = f", AfterValidator({func_name})"
950950
out = source[:insert_at] + addition + source[insert_at:]
951951
func_src = _build_contains_function(func_name, groups, item_condition)
952-
insert_at = assign_re.search(out).start()
952+
assign_match = assign_re.search(out)
953+
assert assign_match is not None
954+
insert_at = assign_match.start()
953955
out = out[:insert_at] + func_src + "\n\n" + out[insert_at:]
954956
if item_condition:
955957
out = _inject_item_condition_on_item_class(
@@ -962,7 +964,7 @@ def inject_array_contains(source, alias_name, groups, item_condition=None):
962964

963965
_ITEM_COND_TEMPLATE = '''
964966
@model_validator(mode="after")
965-
def {marker}(self):
967+
def {marker}(self) -> Any:
966968
"""JSON Schema if/then: require {required!r} when {field!r} is custom."""
967969
if getattr(self, {field!r}, None) not in {excluded!r}:
968970
_present = self.model_fields_set | set(self.model_extra or {{}})
@@ -1827,7 +1829,7 @@ def _patch_max_properties():
18271829

18281830
_NOT_ENUM_TEMPLATE = '''
18291831
@model_validator(mode="after")
1830-
def {marker}(self):
1832+
def {marker}(self) -> Any:
18311833
"""JSON Schema not.enum: reject known variant discriminator values on open-union fallback."""
18321834
if getattr(self, {field!r}, None) in {excluded!r}:
18331835
raise ValueError(
@@ -2365,7 +2367,7 @@ def _patch_open_union_not_enum():
23652367

23662368
_FORBIDDEN_KEYS_TEMPLATE = '''
23672369
@model_validator(mode="after")
2368-
def {marker}(self):
2370+
def {marker}(self) -> Any:
23692371
"""JSON Schema not: reject forbidden property keys."""
23702372
_present = self.model_fields_set | set(self.model_extra or {{}})
23712373
for _key in {forbidden!r}:
@@ -2663,14 +2665,18 @@ def find_datetime_pattern_fields(source):
26632665
items), which is left as generated and must be reported.
26642666
"""
26652667
tree = ast.parse(source)
2666-
fields = [
2667-
(
2668-
cls.name,
2669-
stmt.target.id,
2670-
_pattern_keyword(stmt.value).value.value,
2671-
)
2672-
for cls, stmt in _datetime_pattern_fields(tree)
2673-
]
2668+
fields = []
2669+
for cls, stmt in _datetime_pattern_fields(tree):
2670+
if not isinstance(stmt.target, ast.Name):
2671+
continue
2672+
keyword = _pattern_keyword(stmt.value)
2673+
if (
2674+
keyword is None
2675+
or not isinstance(keyword.value, ast.Constant)
2676+
or not isinstance(keyword.value.value, str)
2677+
):
2678+
continue
2679+
fields.append((cls.name, stmt.target.id, keyword.value.value))
26742680
unsupported = [
26752681
node.lineno
26762682
for node in ast.walk(tree)
@@ -2706,10 +2712,13 @@ def inject_datetime_patterns(source):
27062712
cls, stmt = next(
27072713
_datetime_pattern_fields(ast.parse(source)), (None, None)
27082714
)
2709-
if stmt is None:
2715+
if cls is None or stmt is None:
27102716
return source
2717+
assert isinstance(stmt.target, ast.Name)
27112718
call = stmt.value
2719+
assert isinstance(call, ast.Call)
27122720
keyword = _pattern_keyword(call)
2721+
assert keyword is not None and isinstance(keyword.value, ast.Constant)
27132722
call.keywords = [kw for kw in call.keywords if kw is not keyword]
27142723
default = call.args[0] if len(call.args) == 1 else None
27152724
if (
@@ -2726,13 +2735,19 @@ def inject_datetime_patterns(source):
27262735
value = " = None"
27272736
else:
27282737
value = f" = {ast.unparse(call)}"
2738+
assert (
2739+
stmt.annotation.end_lineno is not None
2740+
and stmt.annotation.end_col_offset is not None
2741+
)
2742+
assert call.end_lineno is not None and call.end_col_offset is not None
27292743
start = _source_offset(
27302744
source, stmt.annotation.end_lineno, stmt.annotation.end_col_offset
27312745
)
27322746
end = _source_offset(source, call.end_lineno, call.end_col_offset)
27332747
source = source[:start] + value + source[end:]
27342748

27352749
match = re.search(rf"^class {re.escape(cls.name)}\(", source, re.M)
2750+
assert match is not None
27362751
end_match = re.compile(r"^\S", re.M).search(source, match.end())
27372752
body_end = end_match.start() if end_match else len(source)
27382753
method = _DATETIME_PATTERN_TEMPLATE.format(
@@ -2803,12 +2818,12 @@ def _write_schemas_reexport_init():
28032818
):
28042819
exports.append(node.target.id)
28052820
continue
2806-
if (
2807-
isinstance(node, type_alias_cls)
2808-
and isinstance(node.name, ast.Name)
2809-
and not node.name.id.startswith("_")
2810-
):
2811-
exports.append(node.name.id)
2821+
if isinstance(node, type_alias_cls):
2822+
alias_name = getattr(node, "name", None)
2823+
if isinstance(
2824+
alias_name, ast.Name
2825+
) and not alias_name.id.startswith("_"):
2826+
exports.append(alias_name.id)
28122827
exports = sorted(dict.fromkeys(exports))
28132828
init_path = generated_path.parent / "__init__.py"
28142829
if exports:

‎pyproject.toml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ custom-file-header = """
6161
# generated by datamodel-codegen
6262
# pylint: disable=all
6363
# pyformat: disable
64+
# mypy: disable-error-code="assignment"
65+
# pyright: reportIncompatibleVariableOverride=false, reportGeneralTypeIssues=false
6466
"""
6567

6668
[tool.ruff]
@@ -89,3 +91,25 @@ convention = "google"
8991
combine-as-imports = true
9092
force-sort-within-sections = true
9193
case-sensitive = true
94+
95+
[tool.pyright]
96+
include = ["src/ucp_sdk", "tests/test_models.py"]
97+
98+
[tool.mypy]
99+
python_version = "3.10"
100+
strict = true
101+
files = ["src/ucp_sdk", "tests/test_models.py"]
102+
cache_dir = "/tmp/ucp_sdk_mypy_cache"
103+
104+
[[tool.mypy.overrides]]
105+
module = ["postprocess_models", "tests.test_codegen_pipeline", "test_codegen_pipeline"]
106+
disallow_untyped_defs = false
107+
disallow_incomplete_defs = false
108+
disallow_untyped_calls = false
109+
check_untyped_defs = false
110+
warn_return_any = false
111+
disable_error_code = ["var-annotated", "type-arg", "arg-type", "call-arg", "attr-defined", "union-attr", "index", "operator", "annotation-unchecked"]
112+
113+
[tool.pytest.ini_options]
114+
pythonpath = ["."]
115+
cache_dir = "/tmp/ucp_sdk_pytest_cache"

0 commit comments

Comments
 (0)