From 3fc0434c5227d04693439229b40ff33b19f3d0bf Mon Sep 17 00:00:00 2001 From: vishkaty Date: Wed, 15 Jul 2026 22:40:35 -0400 Subject: [PATCH 1/4] fix: enforce minProperties on generated models (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit datamodel-code-generator drops minProperties on object schemas with declared properties, so Description() validated with zero fields in violation of description.json's minProperties: 1. (minProperties on free-form object properties is unaffected — the generator already maps those to Field(min_length=...) on the dict field.) A new post-generation step (postprocess_models.py, wired into generate_models.sh before formatting) scans the preprocessed schemas for root-level minProperties constraints and injects a model_validator(mode="after") into each matching generated class. JSON Schema counts the keys present on the object, so the validator counts provided fields (model_fields_set) unioned with extra keys (model_extra): an explicit null is a present key, and unknown keys on extra="allow" models count too. The step is idempotent, data-driven from the schemas (future minProperties additions are picked up on regeneration), and fails generation loudly if a constraint can't be mapped to a generated class. The committed Description model is regenerated with the validator. Tests cover the injector (dependency-free) and the enforced semantics on Description; the workflow now installs the package so those semantic tests run in CI. --- .github/workflows/tests.yml | 2 + generate_models.sh | 3 + postprocess_models.py | 145 ++++++++++++++++++ .../schemas/shopping/types/description.py | 12 +- tests/test_min_properties.py | 136 ++++++++++++++++ 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 postprocess_models.py create mode 100644 tests/test_min_properties.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e6a3a2..2daad35 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,5 +38,7 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Install package (generated-model tests import it) + run: pip install -e . - name: Run preprocessing tests run: python -m unittest discover -s tests -p "test_*.py" diff --git a/generate_models.sh b/generate_models.sh index a45543c..4fd43d8 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -87,6 +87,9 @@ uv run \ --additional-imports pydantic.ConfigDict +echo "Post-processing generated models (constraints the generator ignores)..." +uv run python postprocess_models.py || exit 1 + echo "Formatting generated models..." uv run ruff format uv run ruff check --fix "$OUTPUT_DIR" diff --git a/postprocess_models.py b/postprocess_models.py new file mode 100644 index 0000000..cb01ebd --- /dev/null +++ b/postprocess_models.py @@ -0,0 +1,145 @@ +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Post-generation fixes for constraints datamodel-code-generator ignores. + +``minProperties`` on an object schema WITH declared properties is dropped by +the generator (issue #49): every field is optional, so an empty instance +passes validation in violation of the schema. (``minProperties`` on a +free-form object property is already handled natively — the generator maps it +to ``Field(min_length=...)`` on the dict field.) + +This script scans the preprocessed schemas for root-level ``minProperties`` +constraints and injects a ``model_validator(mode="after")`` into the matching +generated classes. JSON Schema counts the keys present on the object, so the +validator counts provided fields (``model_fields_set``) unioned with extra +keys (``model_extra``) — an explicit null is a present key, and unknown keys +on ``extra="allow"`` models count too. + +Runs from generate_models.sh between generation and formatting; idempotent. +""" + +import json +import re +import sys +from pathlib import Path + +SCHEMA_DIR = Path("ucp/source/schemas") +OUTPUT_DIR = Path("src/ucp_sdk/models/schemas") + +_MARKER = "_enforce_min_properties" + +_VALIDATOR_TEMPLATE = ''' + @model_validator(mode="after") + def {marker}(self): + """JSON Schema minProperties: require at least {minimum} provided propert{y_ies}.""" + provided = self.model_fields_set | set(self.model_extra or {{}}) + if len(provided) < {minimum}: + raise ValueError( + "At least {minimum} propert{y_ies} must be provided " + "(schema minProperties={minimum})" + ) + return self +''' + + +def find_root_min_properties(schema_dir): + """Map schema title -> minProperties for root-level object constraints.""" + found = {} + for path in sorted(Path(schema_dir).rglob("*.json")): + try: + schema = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + continue + if not isinstance(schema, dict): + continue + minimum = schema.get("minProperties") + if not minimum or not schema.get("properties"): + continue + title = schema.get("title") + if not title: + print( + f" ! {path}: root minProperties but no title; cannot map to a class" + ) + continue + found[title] = minimum + return found + + +def _ensure_validator_import(source): + """Add model_validator to the existing pydantic import if missing.""" + if re.search(r"^from pydantic import .*\bmodel_validator\b", source, re.M): + return source + return re.sub( + r"^(from pydantic import [^\n]+)$", + lambda m: f"{m.group(1)}, model_validator", + source, + count=1, + flags=re.M, + ) + + +def inject_min_properties(source, class_name, minimum): + """Inject the minProperties validator at the end of ``class_name``.""" + if f"def {_MARKER}(" in source: + return source + class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) + match = class_re.search(source) + if not match: + return source + # The class body ends at the next top-level statement or EOF. + tail = re.compile(r"^\S", re.M) + end_match = tail.search(source, match.end()) + end = end_match.start() if end_match else len(source) + method = _VALIDATOR_TEMPLATE.format( + marker=_MARKER, + minimum=minimum, + y_ies="y" if minimum == 1 else "ies", + ) + body = source[:end].rstrip("\n") + rest = source[end:] + out = body + "\n" + method + ("\n" + rest if rest else "") + return _ensure_validator_import(out) + + +def main(): + constraints = find_root_min_properties(SCHEMA_DIR) + if not constraints: + print("postprocess: no root-level minProperties constraints found") + return 0 + patched = 0 + for title, minimum in sorted(constraints.items()): + hits = [] + for path in sorted(OUTPUT_DIR.rglob("*.py")): + source = path.read_text(encoding="utf-8") + if not re.search(rf"^class {re.escape(title)}\(", source, re.M): + continue + updated = inject_min_properties(source, title, minimum) + if updated != source: + path.write_text(updated, encoding="utf-8") + patched += 1 + hits.append(path) + label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" + print(f" minProperties={minimum} on '{title}' -> {label}") + if not hits: + print( + f" ! '{title}' has no generated class; constraint not enforced" + ) + return 1 + print(f"postprocess: {patched} module(s) patched") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/ucp_sdk/models/schemas/shopping/types/description.py b/src/ucp_sdk/models/schemas/shopping/types/description.py index 18f6582..6207d51 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/description.py +++ b/src/ucp_sdk/models/schemas/shopping/types/description.py @@ -18,7 +18,7 @@ from __future__ import annotations -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, model_validator class Description(BaseModel): @@ -41,3 +41,13 @@ class Description(BaseModel): """ Markdown-formatted content. """ + + @model_validator(mode="after") + def _enforce_min_properties(self): + """JSON Schema minProperties: require at least 1 provided property.""" + provided = self.model_fields_set | set(self.model_extra or {}) + if len(provided) < 1: + raise ValueError( + "At least 1 property must be provided (schema minProperties=1)" + ) + return self diff --git a/tests/test_min_properties.py b/tests/test_min_properties.py new file mode 100644 index 0000000..0842ee3 --- /dev/null +++ b/tests/test_min_properties.py @@ -0,0 +1,136 @@ +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""minProperties enforcement on generated models (issue #49). + +JSON Schema's ``minProperties`` counts the keys present on the object, so the +generated model must reject an instance with fewer provided properties — +including the no-argument case — while accepting any combination of declared +fields (explicit nulls are present keys) and extra fields (``extra="allow"`` +models accept unknown keys, which count too). + +The injector tests are dependency-free; the ``Description`` semantic tests +need the package importable (``pip install -e .``) and skip otherwise. +""" + +import json +import tempfile +import unittest +from pathlib import Path + +import postprocess_models + +try: + from pydantic import ValidationError + + from ucp_sdk.models.schemas.shopping.types.description import Description + + HAVE_SDK = True +except ImportError: # pragma: no cover - exercised only without install + HAVE_SDK = False + + +@unittest.skipUnless( + HAVE_SDK, "requires the installed package (pip install -e .)" +) +class DescriptionMinPropertiesTest(unittest.TestCase): + """description.json declares minProperties: 1 at the schema root.""" + + def test_empty_instance_rejected(self): + with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): + Description() + + def test_empty_mapping_rejected(self): + with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): + Description.model_validate({}) + + def test_single_declared_field_accepted(self): + self.assertEqual(Description(plain="hello").plain, "hello") + + def test_explicit_null_key_counts_as_present(self): + # {"html": null} has one property per JSON Schema's key counting. + Description.model_validate({"html": None}) + + def test_extra_field_counts_as_present(self): + # extra="allow": an unknown key is a present property. + Description.model_validate({"x-vendor-note": "hi"}) + + def test_all_fields_accepted(self): + Description(plain="p", html="

p

", markdown="p") + + +class InjectorTest(unittest.TestCase): + """The post-generation injector's own behavior.""" + + SCHEMA = { + "title": "Sample", + "type": "object", + "minProperties": 2, + "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, + } + + MODULE = ( + "from __future__ import annotations\n" + "\n" + "from pydantic import BaseModel, ConfigDict\n" + "\n" + "\n" + "class Sample(BaseModel):\n" + ' """A sample."""\n' + "\n" + " model_config = ConfigDict(\n" + ' extra="allow",\n' + " )\n" + " a: str | None = None\n" + " b: str | None = None\n" + ) + + def test_injects_validator_with_declared_minimum(self): + out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) + self.assertIn("model_validator", out) + self.assertIn("at least 2", out.lower()) + + @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") + def test_injected_validator_enforces_count(self): + out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) + namespace: dict = {} + exec(compile(out, "", "exec"), namespace) # noqa: S102 + sample_cls = namespace["Sample"] + with self.assertRaises(ValidationError): + sample_cls(a="only-one") + sample_cls(a="one", b="two") + + def test_injection_is_idempotent(self): + once = postprocess_models.inject_min_properties( + self.MODULE, "Sample", 2 + ) + twice = postprocess_models.inject_min_properties(once, "Sample", 2) + self.assertEqual(once, twice) + + def test_schema_scan_finds_root_constraints(self): + with tempfile.TemporaryDirectory() as tmp: + sub = Path(tmp) / "sub" + sub.mkdir() + (sub / "sample.json").write_text(json.dumps(self.SCHEMA)) + (sub / "plain.json").write_text( + json.dumps( + {"title": "Plain", "type": "object", "properties": {}} + ) + ) + found = postprocess_models.find_root_min_properties(Path(tmp)) + self.assertEqual(found, {"Sample": 2}) + + +if __name__ == "__main__": + unittest.main() From 34561dfa9576750371f5a59ba857ba141af8f5da Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 17 Jul 2026 07:46:43 +0000 Subject: [PATCH 2/4] test: merge minProperties tests into tests/test_codegen_pipeline.py Deletes tests/test_min_properties.py and merges its test cases into tests/test_codegen_pipeline.py (renamed from test_preprocess_schemas.py to better reflect its expanded scope of testing both pre- and post-processing). --- ...ss_schemas.py => test_codegen_pipeline.py} | 102 +++++++++++++ tests/test_min_properties.py | 136 ------------------ 2 files changed, 102 insertions(+), 136 deletions(-) rename tests/{test_preprocess_schemas.py => test_codegen_pipeline.py} (84%) delete mode 100644 tests/test_min_properties.py diff --git a/tests/test_preprocess_schemas.py b/tests/test_codegen_pipeline.py similarity index 84% rename from tests/test_preprocess_schemas.py rename to tests/test_codegen_pipeline.py index 92fda86..4890720 100644 --- a/tests/test_preprocess_schemas.py +++ b/tests/test_codegen_pipeline.py @@ -17,14 +17,25 @@ import contextlib import copy import io +import json import sys import tempfile import unittest from pathlib import Path from unittest import mock +import postprocess_models import preprocess_schemas +try: + from pydantic import ValidationError + + from ucp_sdk.models.schemas.shopping.types.description import Description + + HAVE_SDK = True +except ImportError: # pragma: no cover + HAVE_SDK = False + class SchemaNormalizationTest(unittest.TestCase): """Tests schema flattening and reference normalization.""" @@ -535,5 +546,96 @@ def test_main_preprocesses_schema_tree_end_to_end(self) -> None: self.assertEqual(child_variant["required"], ["value"]) +@unittest.skipUnless( + HAVE_SDK, "requires the installed package (pip install -e .)" +) +class DescriptionMinPropertiesTest(unittest.TestCase): + """description.json declares minProperties: 1 at the schema root.""" + + def test_empty_instance_rejected(self): + with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): + Description() + + def test_empty_mapping_rejected(self): + with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): + Description.model_validate({}) + + def test_single_declared_field_accepted(self): + self.assertEqual(Description(plain="hello").plain, "hello") + + def test_explicit_null_key_counts_as_present(self): + # {"html": null} has one property per JSON Schema's key counting. + Description.model_validate({"html": None}) + + def test_extra_field_counts_as_present(self): + # extra="allow": an unknown key is a present property. + Description.model_validate({"x-vendor-note": "hi"}) + + def test_all_fields_accepted(self): + Description(plain="p", html="

p

", markdown="p") + + +class InjectorTest(unittest.TestCase): + """The post-generation injector's own behavior.""" + + SCHEMA = { + "title": "Sample", + "type": "object", + "minProperties": 2, + "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, + } + + MODULE = ( + "from __future__ import annotations\n" + "\n" + "from pydantic import BaseModel, ConfigDict\n" + "\n" + "\n" + "class Sample(BaseModel):\n" + ' """A sample."""\n' + "\n" + " model_config = ConfigDict(\n" + ' extra="allow",\n' + " )\n" + " a: str | None = None\n" + " b: str | None = None\n" + ) + + def test_injects_validator_with_declared_minimum(self): + out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) + self.assertIn("model_validator", out) + self.assertIn("at least 2", out.lower()) + + @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") + def test_injected_validator_enforces_count(self): + out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) + namespace: dict = {} + exec(compile(out, "", "exec"), namespace) # noqa: S102 + sample_cls = namespace["Sample"] + with self.assertRaises(ValidationError): + sample_cls(a="only-one") + sample_cls(a="one", b="two") + + def test_injection_is_idempotent(self): + once = postprocess_models.inject_min_properties( + self.MODULE, "Sample", 2 + ) + twice = postprocess_models.inject_min_properties(once, "Sample", 2) + self.assertEqual(once, twice) + + def test_schema_scan_finds_root_constraints(self): + with tempfile.TemporaryDirectory() as tmp: + sub = Path(tmp) / "sub" + sub.mkdir() + (sub / "sample.json").write_text(json.dumps(self.SCHEMA)) + (sub / "plain.json").write_text( + json.dumps( + {"title": "Plain", "type": "object", "properties": {}} + ) + ) + found = postprocess_models.find_root_min_properties(Path(tmp)) + self.assertEqual(found, {"Sample": 2}) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_min_properties.py b/tests/test_min_properties.py deleted file mode 100644 index 0842ee3..0000000 --- a/tests/test_min_properties.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""minProperties enforcement on generated models (issue #49). - -JSON Schema's ``minProperties`` counts the keys present on the object, so the -generated model must reject an instance with fewer provided properties — -including the no-argument case — while accepting any combination of declared -fields (explicit nulls are present keys) and extra fields (``extra="allow"`` -models accept unknown keys, which count too). - -The injector tests are dependency-free; the ``Description`` semantic tests -need the package importable (``pip install -e .``) and skip otherwise. -""" - -import json -import tempfile -import unittest -from pathlib import Path - -import postprocess_models - -try: - from pydantic import ValidationError - - from ucp_sdk.models.schemas.shopping.types.description import Description - - HAVE_SDK = True -except ImportError: # pragma: no cover - exercised only without install - HAVE_SDK = False - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class DescriptionMinPropertiesTest(unittest.TestCase): - """description.json declares minProperties: 1 at the schema root.""" - - def test_empty_instance_rejected(self): - with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): - Description() - - def test_empty_mapping_rejected(self): - with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): - Description.model_validate({}) - - def test_single_declared_field_accepted(self): - self.assertEqual(Description(plain="hello").plain, "hello") - - def test_explicit_null_key_counts_as_present(self): - # {"html": null} has one property per JSON Schema's key counting. - Description.model_validate({"html": None}) - - def test_extra_field_counts_as_present(self): - # extra="allow": an unknown key is a present property. - Description.model_validate({"x-vendor-note": "hi"}) - - def test_all_fields_accepted(self): - Description(plain="p", html="

p

", markdown="p") - - -class InjectorTest(unittest.TestCase): - """The post-generation injector's own behavior.""" - - SCHEMA = { - "title": "Sample", - "type": "object", - "minProperties": 2, - "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Sample(BaseModel):\n" - ' """A sample."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - " a: str | None = None\n" - " b: str | None = None\n" - ) - - def test_injects_validator_with_declared_minimum(self): - out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) - self.assertIn("model_validator", out) - self.assertIn("at least 2", out.lower()) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_count(self): - out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - sample_cls = namespace["Sample"] - with self.assertRaises(ValidationError): - sample_cls(a="only-one") - sample_cls(a="one", b="two") - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_min_properties( - self.MODULE, "Sample", 2 - ) - twice = postprocess_models.inject_min_properties(once, "Sample", 2) - self.assertEqual(once, twice) - - def test_schema_scan_finds_root_constraints(self): - with tempfile.TemporaryDirectory() as tmp: - sub = Path(tmp) / "sub" - sub.mkdir() - (sub / "sample.json").write_text(json.dumps(self.SCHEMA)) - (sub / "plain.json").write_text( - json.dumps( - {"title": "Plain", "type": "object", "properties": {}} - ) - ) - found = postprocess_models.find_root_min_properties(Path(tmp)) - self.assertEqual(found, {"Sample": 2}) - - -if __name__ == "__main__": - unittest.main() From c3a415fb5b3adea53843165f7a4594f63cd5e765 Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 17 Jul 2026 07:48:49 +0000 Subject: [PATCH 3/4] fix: resolve ruff linter issues in postprocess_models.py Replaces print statements with sys.stdout.write/sys.stderr.write (T201), splits long lines (E501), and adds docstring to main (D103) to comply with project lint configuration. --- postprocess_models.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/postprocess_models.py b/postprocess_models.py index cb01ebd..0a9ea67 100644 --- a/postprocess_models.py +++ b/postprocess_models.py @@ -43,7 +43,8 @@ _VALIDATOR_TEMPLATE = ''' @model_validator(mode="after") def {marker}(self): - """JSON Schema minProperties: require at least {minimum} provided propert{y_ies}.""" + """JSON Schema minProperties: require at least {minimum} + provided propert{y_ies}.""" provided = self.model_fields_set | set(self.model_extra or {{}}) if len(provided) < {minimum}: raise ValueError( @@ -69,8 +70,9 @@ def find_root_min_properties(schema_dir): continue title = schema.get("title") if not title: - print( - f" ! {path}: root minProperties but no title; cannot map to a class" + sys.stderr.write( + f" ! {path}: root minProperties but no title; " + "cannot map to a class\n" ) continue found[title] = minimum @@ -114,9 +116,12 @@ def inject_min_properties(source, class_name, minimum): def main(): + """Main entry point to scan schemas and patch generated models.""" constraints = find_root_min_properties(SCHEMA_DIR) if not constraints: - print("postprocess: no root-level minProperties constraints found") + sys.stdout.write( + "postprocess: no root-level minProperties constraints found\n" + ) return 0 patched = 0 for title, minimum in sorted(constraints.items()): @@ -131,13 +136,14 @@ def main(): patched += 1 hits.append(path) label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" - print(f" minProperties={minimum} on '{title}' -> {label}") + sys.stdout.write(f" minProperties={minimum} on '{title}' -> {label}\n") if not hits: - print( - f" ! '{title}' has no generated class; constraint not enforced" + sys.stderr.write( + f" ! '{title}' has no generated class; " + "constraint not enforced\n" ) return 1 - print(f"postprocess: {patched} module(s) patched") + sys.stdout.write(f"postprocess: {patched} module(s) patched\n") return 0 From c5af615e29798ae86d9199ff25d9166f48c21d29 Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 17 Jul 2026 08:12:20 +0000 Subject: [PATCH 4/4] fix: resolve codespell error in postprocess_models.py Replaces propert{y_ies} template variable with properties_noun to avoid codespell false positive on propert. Regenerated models to apply template change. --- postprocess_models.py | 6 +++--- src/ucp_sdk/models/schemas/shopping/types/description.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/postprocess_models.py b/postprocess_models.py index 0a9ea67..f7601f0 100644 --- a/postprocess_models.py +++ b/postprocess_models.py @@ -44,11 +44,11 @@ @model_validator(mode="after") def {marker}(self): """JSON Schema minProperties: require at least {minimum} - provided propert{y_ies}.""" + provided {properties_noun}.""" provided = self.model_fields_set | set(self.model_extra or {{}}) if len(provided) < {minimum}: raise ValueError( - "At least {minimum} propert{y_ies} must be provided " + "At least {minimum} {properties_noun} must be provided " "(schema minProperties={minimum})" ) return self @@ -107,7 +107,7 @@ def inject_min_properties(source, class_name, minimum): method = _VALIDATOR_TEMPLATE.format( marker=_MARKER, minimum=minimum, - y_ies="y" if minimum == 1 else "ies", + properties_noun="property" if minimum == 1 else "properties", ) body = source[:end].rstrip("\n") rest = source[end:] diff --git a/src/ucp_sdk/models/schemas/shopping/types/description.py b/src/ucp_sdk/models/schemas/shopping/types/description.py index 6207d51..4d7fc3b 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/description.py +++ b/src/ucp_sdk/models/schemas/shopping/types/description.py @@ -44,7 +44,8 @@ class Description(BaseModel): @model_validator(mode="after") def _enforce_min_properties(self): - """JSON Schema minProperties: require at least 1 provided property.""" + """JSON Schema minProperties: require at least 1 + provided property.""" provided = self.model_fields_set | set(self.model_extra or {}) if len(provided) < 1: raise ValueError(