diff --git a/jambo/parser/__init__.py b/jambo/parser/__init__.py index 44b4424..0de9f00 100644 --- a/jambo/parser/__init__.py +++ b/jambo/parser/__init__.py @@ -29,4 +29,4 @@ __all__ = [ "OneOfTypeParser", "StringTypeParser", "RefTypeParser", -] \ No newline at end of file +] diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index 6c5cdc9..9350c25 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -127,23 +127,9 @@ class GenericTypeParser(ABC, Generic[T]): @staticmethod def _has_meaningful_constraints(field_props): - """ - Check if field properties contain meaningful constraints that require Field wrapping. - - Returns False if: - - field_props is None or empty - - field_props only contains {'default': None} - - Returns True if: - - field_props contains a non-None default value - - field_props contains other constraint properties (min_length, max_length, pattern, etc.) - """ if not field_props: return False - # If only default is set and it's None, no meaningful constraints if field_props == {"default": None}: return False - - # If there are multiple properties or non-None default, that's meaningful return True diff --git a/jambo/parser/array_type_parser.py b/jambo/parser/array_type_parser.py index 0faa37e..52e3bcf 100644 --- a/jambo/parser/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -35,17 +35,14 @@ class ArrayTypeParser(GenericTypeParser): mapped_properties = self.mappings_properties_builder(properties, **kwargs) - # Only set default_factory if the field is not required OR if there's an actual default if not kwargs.get("required", False) and "default" not in mapped_properties: mapped_properties["default_factory"] = self._build_default_factory( properties.get("default"), wrapper_type ) elif "default" in properties: - # If there's a default value specified, set the default_factory mapped_properties["default_factory"] = self._build_default_factory( properties["default"], wrapper_type ) - # Remove the regular default since we're using default_factory mapped_properties.pop("default", None) return field_type, mapped_properties diff --git a/jambo/parser/const_type_parser.py b/jambo/parser/const_type_parser.py index 1e4ce84..4865df0 100644 --- a/jambo/parser/const_type_parser.py +++ b/jambo/parser/const_type_parser.py @@ -33,14 +33,10 @@ class ConstTypeParser(GenericTypeParser): return const_type, parsed_properties def _build_const_type(self, const_value): - # Try to use Literal for hashable types (required for discriminated unions) - # Fall back to validator approach for non-hashable types try: - # Test if the value is hashable (can be used in Literal) hash(const_value) return Literal[const_value] except TypeError: - # Non-hashable type (like list, dict), use validator approach def _validate_const_value(value: Any) -> Any: if value != const_value: raise ValueError( @@ -48,4 +44,4 @@ class ConstTypeParser(GenericTypeParser): ) return value - return Annotated[type(const_value), AfterValidator(_validate_const_value)] \ No newline at end of file + return Annotated[type(const_value), AfterValidator(_validate_const_value)] diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 0f746fb..04f1e98 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -28,7 +28,7 @@ class StringTypeParser(GenericTypeParser): "time": time, "date-time": datetime, "binary": bytes, - "file-path": FilePath, # Added file-path format + "file-path": FilePath, } format_pattern_mapping = { @@ -57,4 +57,4 @@ class StringTypeParser(GenericTypeParser): mapped_properties["json_schema_extra"] = {} mapped_properties["json_schema_extra"]["format"] = format_type - return mapped_type, mapped_properties \ No newline at end of file + return mapped_type, mapped_properties diff --git a/tests/parser/test_array_type_parser.py b/tests/parser/test_array_type_parser.py index 9f3337a..ee09987 100644 --- a/tests/parser/test_array_type_parser.py +++ b/tests/parser/test_array_type_parser.py @@ -99,16 +99,13 @@ class TestArrayTypeParser(TestCase): parser.from_properties("placeholder", properties) def test_array_parser_required_without_default(self): - """Regression test: Required array fields without defaults should be required""" parser = ArrayTypeParser() properties = {"items": {"type": "string"}} - # Test with required=True (should be required) type_parsing, type_validator = parser.from_properties( "test_array", properties, required=True ) - # Should NOT have default_factory when required and no default specified self.assertNotIn("default_factory", type_validator) self.assertNotIn("default", type_validator) diff --git a/tests/parser/test_const_type_parser.py b/tests/parser/test_const_type_parser.py index 5a8c9c1..8f3661c 100644 --- a/tests/parser/test_const_type_parser.py +++ b/tests/parser/test_const_type_parser.py @@ -7,7 +7,6 @@ from unittest import TestCase class TestConstTypeParser(TestCase): def test_const_type_parser_hashable_value(self): - """Test const parser with hashable values (uses Literal)""" parser = ConstTypeParser() expected_const_value = "United States of America" @@ -17,31 +16,27 @@ class TestConstTypeParser(TestCase): "country", properties ) - # Check that we get a Literal type for hashable values self.assertEqual(get_origin(parsed_type), Literal) self.assertEqual(get_args(parsed_type), (expected_const_value,)) self.assertEqual(parsed_properties["default"], expected_const_value) def test_const_type_parser_non_hashable_value(self): - """Test const parser with non-hashable values (uses Annotated with validator)""" parser = ConstTypeParser() - expected_const_value = [1, 2, 3] # Lists are not hashable + expected_const_value = [1, 2, 3] properties = {"const": expected_const_value} parsed_type, parsed_properties = parser.from_properties_impl( "list_const", properties ) - # Check that we get an Annotated type for non-hashable values self.assertEqual(get_origin(parsed_type), Annotated) self.assertIn(list, get_args(parsed_type)) self.assertEqual(parsed_properties["default"], expected_const_value) def test_const_type_parser_integer_value(self): - """Test const parser with integer values (uses Literal)""" parser = ConstTypeParser() expected_const_value = 42 @@ -51,14 +46,12 @@ class TestConstTypeParser(TestCase): "int_const", properties ) - # Check that we get a Literal type for hashable values self.assertEqual(get_origin(parsed_type), Literal) self.assertEqual(get_args(parsed_type), (expected_const_value,)) self.assertEqual(parsed_properties["default"], expected_const_value) def test_const_type_parser_boolean_value(self): - """Test const parser with boolean values (uses Literal)""" parser = ConstTypeParser() expected_const_value = True @@ -68,7 +61,6 @@ class TestConstTypeParser(TestCase): "bool_const", properties ) - # Check that we get a Literal type for hashable values self.assertEqual(get_origin(parsed_type), Literal) self.assertEqual(get_args(parsed_type), (expected_const_value,)) @@ -99,4 +91,4 @@ class TestConstTypeParser(TestCase): self.assertIn( "Const type invalid_country must have 'const' value of allowed types", str(context.exception), - ) \ No newline at end of file + ) diff --git a/tests/parser/test_oneof_type_parser.py b/tests/parser/test_oneof_type_parser.py index 8c75f04..cea0a5e 100644 --- a/tests/parser/test_oneof_type_parser.py +++ b/tests/parser/test_oneof_type_parser.py @@ -351,32 +351,26 @@ class TestOneOfTypeParser(TestCase): } } ], - "discriminator": {} # discriminator without propertyName + "discriminator": {} } } } Model = SchemaConverter.build(schema) - # Should succeed because input matches exactly one schema (the first one) - # The first schema matches: type="a" matches const("a"), value="test" is a string - # The second schema doesn't match: type="a" does not match const("b") obj = Model(value={"type": "a", "value": "test", "extra": "invalid"}) self.assertEqual(obj.value.type, "a") self.assertEqual(obj.value.value, "test") - # Test with input that matches the second schema obj2 = Model(value={"type": "b", "value": 42}) self.assertEqual(obj2.value.type, "b") self.assertEqual(obj2.value.value, 42) - # Test with input that matches neither schema (should fail) with self.assertRaises(ValueError) as cm: Model(value={"type": "c", "value": "test"}) self.assertIn("does not match any of the oneOf schemas", str(cm.exception)) def test_oneof_multiple_matches_without_discriminator(self): - """Test case where input genuinely matches multiple oneOf schemas""" schema = { "title": "Test", "type": "object", @@ -397,21 +391,18 @@ class TestOneOfTypeParser(TestCase): } } ], - "discriminator": {} # discriminator without propertyName + "discriminator": {} } } } Model = SchemaConverter.build(schema) - # This input matches both schemas since both accept data as string - # and neither requires specific additional properties with self.assertRaises(ValueError) as cm: Model(value={"data": "test"}) self.assertIn("matches multiple oneOf schemas", str(cm.exception)) def test_oneof_overlapping_strings_from_docs(self): - """Test the overlapping strings example from documentation""" schema = { "title": "SimpleExample", "type": "object", @@ -428,21 +419,17 @@ class TestOneOfTypeParser(TestCase): Model = SchemaConverter.build(schema) - # Valid: Short string (matches first schema only) obj1 = Model(value="hi") self.assertEqual(obj1.value, "hi") - # Valid: Long string (matches second schema only) obj2 = Model(value="very long string") self.assertEqual(obj2.value, "very long string") - # Invalid: Medium string (matches BOTH schemas - violates oneOf) with self.assertRaises(ValueError) as cm: Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4 self.assertIn("matches multiple oneOf schemas", str(cm.exception)) def test_oneof_shapes_discriminator_from_docs(self): - """Test the shapes discriminator example from documentation""" schema = { "title": "Shape", "type": "object", @@ -477,17 +464,14 @@ class TestOneOfTypeParser(TestCase): Model = SchemaConverter.build(schema) - # Valid: Circle circle = Model(shape={"type": "circle", "radius": 5.0}) self.assertEqual(circle.shape.type, "circle") self.assertEqual(circle.shape.radius, 5.0) - # Valid: Rectangle rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20}) self.assertEqual(rectangle.shape.type, "rectangle") self.assertEqual(rectangle.shape.width, 10) self.assertEqual(rectangle.shape.height, 20) - # Invalid: Wrong properties for the type with self.assertRaises(ValueError): Model(shape={"type": "circle", "width": 10})