diff --git a/postprocess_models.py b/postprocess_models.py index b3db021..b1f43b9 100644 --- a/postprocess_models.py +++ b/postprocess_models.py @@ -248,6 +248,12 @@ def _alias_name(title): return "".join(title.split()) +def _to_camel_case(string): + """Convert a string (snake, kebab, space-separated) to CamelCase.""" + parts = re.split(r"[^a-zA-Z0-9]", string) + return "".join(p.capitalize() for p in parts if p) + + def _snake_name(name): """CamelCase alias -> snake_case suffix for a unique function name.""" return re.sub(r"(? " + f" uniqueItems fields {labels} -> " f"{unique_patched} module(s) patched" f" ({', '.join(str(t) for t in touched) or 'none'})\n" ) diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py index 0e87272..d601a64 100644 --- a/tests/test_codegen_pipeline.py +++ b/tests/test_codegen_pipeline.py @@ -912,6 +912,7 @@ class UniqueItemsInjectorTest(unittest.TestCase): """The uniqueItems post-generation injector's own behavior.""" SCHEMA_TREE = { + "title": "First", "properties": { "tags": { "type": "array", @@ -930,7 +931,7 @@ class UniqueItemsInjectorTest(unittest.TestCase): } }, }, - } + }, } MODULE = ( @@ -955,6 +956,7 @@ class UniqueItemsInjectorTest(unittest.TestCase): " model_config = ConfigDict(\n" ' extra="allow",\n' " )\n" + " tags: list[str] | None = None\n" " count: list[int] | None = None\n" ) @@ -963,7 +965,7 @@ def test_find_unique_items_fields_walks_nested_properties(self) -> None: with tempfile.TemporaryDirectory() as tmp: (Path(tmp) / "schema.json").write_text(json.dumps(self.SCHEMA_TREE)) fields = postprocess_models.find_unique_items_fields(Path(tmp)) - self.assertEqual(fields, {"tags", "codes"}) + self.assertEqual(fields, {"First": {"tags"}, "Nested": {"codes"}}) def test_find_unique_items_fields_ignores_false_and_non_arrays( self, @@ -982,33 +984,43 @@ def test_find_unique_items_fields_ignores_false_and_non_arrays( with tempfile.TemporaryDirectory() as tmp: (Path(tmp) / "s.json").write_text(json.dumps(schema)) fields = postprocess_models.find_unique_items_fields(Path(tmp)) - self.assertEqual(fields, set()) + self.assertEqual(fields, {}) def test_inject_targets_matching_list_fields_only(self) -> None: - """Only list fields named in the set get a validator.""" - out = postprocess_models.inject_unique_items(self.MODULE, {"tags"}) + """Only the declaring class's matching list field gets a validator.""" + out = postprocess_models.inject_unique_items( + self.MODULE, {"First": {"tags"}} + ) self.assertIn("field_validator", out) self.assertIn("_enforce_unique_items_tags", out) + self.assertEqual(out.count("def _enforce_unique_items_tags("), 1) self.assertNotIn("_enforce_unique_items_name", out) self.assertNotIn("_enforce_unique_items_count", out) def test_inject_no_match_leaves_source_unchanged(self) -> None: """No matching list field means the module is untouched.""" self.assertEqual( - postprocess_models.inject_unique_items(self.MODULE, {"missing"}), + postprocess_models.inject_unique_items( + self.MODULE, {"First": {"missing"}} + ), self.MODULE, ) def test_injection_is_idempotent(self) -> None: """Re-running the injector changes nothing.""" - once = postprocess_models.inject_unique_items(self.MODULE, {"tags"}) - twice = postprocess_models.inject_unique_items(once, {"tags"}) + unique_fields = {"First": {"tags"}} + once = postprocess_models.inject_unique_items( + self.MODULE, unique_fields + ) + twice = postprocess_models.inject_unique_items(once, unique_fields) self.assertEqual(once, twice) @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") def test_injected_validator_rejects_duplicates(self) -> None: """The injected field_validator enforces uniqueness at runtime.""" - out = postprocess_models.inject_unique_items(self.MODULE, {"tags"}) + out = postprocess_models.inject_unique_items( + self.MODULE, {"First": {"tags"}} + ) namespace: dict = {} exec(compile(out, "", "exec"), namespace) # noqa: S102 first = namespace["First"]