Fixes Default Values in StringTypeParser

This commit is contained in:
2025-06-03 22:40:21 -03:00
parent 3273fd84bf
commit 4bbb896c46
14 changed files with 121 additions and 94 deletions

View File

@@ -12,7 +12,7 @@ T = TypeVar("T")
class GenericTypeParser(ABC, Generic[T]):
json_schema_type: str = None
type_mappings: dict[str, str] = None
type_mappings: dict[str, str] = {}
default_mappings = {
"default": "default",
@@ -20,14 +20,51 @@ class GenericTypeParser(ABC, Generic[T]):
}
@abstractmethod
def from_properties_impl(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
) -> tuple[T, dict]:
"""
Abstract method to convert properties to a type and its fields properties.
:param name: The name of the type.
:param properties: The properties of the type.
:param kwargs: Additional options for type parsing.
:return: A tuple containing the type and its properties.
"""
pass
def from_properties(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
) -> tuple[T, dict]: ...
) -> tuple[type, dict]:
"""
Converts properties to a type and its fields properties.
:param name: The name of the type.
:param properties: The properties of the type.
:param kwargs: Additional options for type parsing.
:return: A tuple containing the type and its properties.
"""
parsed_type, parsed_properties = self.from_properties_impl(
name, properties, **kwargs
)
if not self._validate_default(parsed_type, parsed_properties):
raise ValueError(
f"Default value {properties.get('default')} is not valid for type {parsed_type.__name__}"
)
return parsed_type, parsed_properties
@classmethod
def type_from_properties(
cls, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
) -> tuple[type, dict]:
"""
Factory method to fetch the appropriate type parser based on properties
and generates the equivalent type and fields.
:param name: The name of the type to be created.
:param properties: The properties that define the type.
:param kwargs: Additional options for type parsing.
:return: A tuple containing the type and its properties.
"""
parser = cls._get_impl(properties)
return parser().from_properties(name=name, properties=properties, **kwargs)
@@ -62,9 +99,6 @@ class GenericTypeParser(ABC, Generic[T]):
def mappings_properties_builder(
self, properties, **kwargs: Unpack[TypeParserOptions]
) -> dict[str, Any]:
if self.type_mappings is None:
raise NotImplementedError("Type mappings not defined")
if not kwargs.get("required", False):
properties["default"] = properties.get("default", None)
@@ -74,6 +108,20 @@ class GenericTypeParser(ABC, Generic[T]):
mappings[key]: value for key, value in properties.items() if key in mappings
}
def validate_default(self, field_type: type, field_prop: dict, value) -> None:
field = Annotated[field_type, Field(**field_prop)]
TypeAdapter(field).validate_python(value)
@staticmethod
def _validate_default(field_type: type, field_prop: dict) -> bool:
value = field_prop.get("default")
if value is None and field_prop.get("default_factory") is not None:
value = field_prop["default_factory"]()
if value is None:
return True
try:
field = Annotated[field_type, Field(**field_prop)]
TypeAdapter(field).validate_python(value)
except Exception as _:
return False
return True

View File

@@ -9,7 +9,9 @@ class AllOfTypeParser(GenericTypeParser):
json_schema_type = "allOf"
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
sub_properties = properties.get("allOf", [])
root_type = properties.get("type")
@@ -23,7 +25,7 @@ class AllOfTypeParser(GenericTypeParser):
sub_properties
)
return parser().from_properties(name, combined_properties, **kwargs)
return parser().from_properties_impl(name, combined_properties, **kwargs)
@staticmethod
def _get_type_parser(

View File

@@ -10,14 +10,16 @@ class AnyOfTypeParser(GenericTypeParser):
json_schema_type = "anyOf"
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
if "anyOf" not in properties:
raise ValueError(f"Invalid JSON Schema: {properties}")
if not isinstance(properties["anyOf"], list):
raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}")
mapped_properties = dict()
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
sub_properties = properties["anyOf"]
@@ -26,21 +28,6 @@ class AnyOfTypeParser(GenericTypeParser):
for subProperty in sub_properties
]
default_value = properties.get("default")
if default_value is not None:
for sub_type, sub_property in sub_types:
try:
self.validate_default(sub_type, sub_property, default_value)
break
except ValueError:
continue
else:
raise ValueError(
f"Invalid default value {default_value} for anyOf types: {sub_types}"
)
mapped_properties["default"] = default_value
if not kwargs.get("required", False):
mapped_properties["default"] = mapped_properties.get("default")

View File

@@ -1,7 +1,7 @@
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
from typing_extensions import TypeVar, Unpack
from typing_extensions import Iterable, TypeVar, Unpack
import copy
@@ -21,7 +21,9 @@ class ArrayTypeParser(GenericTypeParser):
"minItems": "min_length",
}
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
item_properties = kwargs.copy()
item_properties["required"] = True
_item_type, _item_args = GenericTypeParser.type_from_properties(
@@ -33,21 +35,20 @@ class ArrayTypeParser(GenericTypeParser):
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
default_list = properties.pop("default", None)
if default_list is not None:
self.validate_default(
field_type,
mapped_properties,
default_list,
if "default" not in mapped_properties:
mapped_properties["default_factory"] = self._build_default_factory(
properties.get("default"), wrapper_type
)
if wrapper_type is list:
mapped_properties["default_factory"] = lambda: copy.deepcopy(
wrapper_type(default_list)
)
else:
mapped_properties["default_factory"] = lambda: wrapper_type(
default_list
)
return field_type, mapped_properties
def _build_default_factory(self, default_list, wrapper_type):
if default_list is None:
return lambda: None
if not isinstance(default_list, Iterable):
raise ValueError(
f"Default value for array must be an iterable, got {type(default_list)}"
)
return lambda: copy.deepcopy(wrapper_type(default_list))

View File

@@ -13,7 +13,9 @@ class BooleanTypeParser(GenericTypeParser):
"default": "default",
}
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
default_value = properties.get("default")

View File

@@ -18,11 +18,7 @@ class FloatTypeParser(GenericTypeParser):
"default": "default",
}
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
default_value = mapped_properties.get("default")
if default_value is not None:
self.validate_default(float, mapped_properties, default_value)
return float, mapped_properties
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
return float, self.mappings_properties_builder(properties, **kwargs)

View File

@@ -18,11 +18,7 @@ class IntTypeParser(GenericTypeParser):
"default": "default",
}
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
default_value = mapped_properties.get("default")
if default_value is not None:
self.validate_default(int, mapped_properties, default_value)
return int, mapped_properties
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
return int, self.mappings_properties_builder(properties, **kwargs)

View File

@@ -11,7 +11,7 @@ class ObjectTypeParser(GenericTypeParser):
json_schema_type = "type:object"
def from_properties(
def from_properties_impl(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
):
type_parsing = self.to_model(
@@ -39,7 +39,7 @@ class ObjectTypeParser(GenericTypeParser):
"""
Converts JSON Schema object properties to a Pydantic model.
:param name: The name of the model.
:param properties: The properties of the JSON Schema object.
:param schema: The properties of the JSON Schema object.
:param required_keys: List of required keys in the schema.
:return: A Pydantic model class.
"""

View File

@@ -34,24 +34,22 @@ class StringTypeParser(GenericTypeParser):
"hostname": r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$",
}
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
mapped_properties = self.mappings_properties_builder(
properties, **kwargs
)
format_type = properties.get("format")
if format_type:
if format_type in self.format_type_mapping:
mapped_type = self.format_type_mapping[format_type]
if format_type in self.format_pattern_mapping:
mapped_properties["pattern"] = self.format_pattern_mapping[
format_type
]
else:
raise ValueError(f"Unsupported string format: {format_type}")
else:
mapped_type = str
if not format_type:
return str, mapped_properties
default_value = properties.get("default")
if default_value is not None:
self.validate_default(mapped_type, mapped_properties, default_value)
if format_type not in self.format_type_mapping:
raise ValueError(f"Unsupported string format: {format_type}")
mapped_type = self.format_type_mapping[format_type]
if format_type in self.format_pattern_mapping:
mapped_properties["pattern"] = self.format_pattern_mapping[format_type]
return mapped_type, mapped_properties

View File

@@ -73,7 +73,7 @@ class TestArrayTypeParser(TestCase):
def test_array_parser_with_invalid_default_type(self):
parser = ArrayTypeParser()
properties = {"items": {"type": "string"}, "default": "not_a_list"}
properties = {"items": {"type": "string"}, "default": 000}
with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties)

View File

@@ -9,7 +9,9 @@ class TestBoolTypeParser(TestCase):
properties = {"type": "boolean"}
type_parsing, type_validator = parser.from_properties("placeholder", properties)
type_parsing, type_validator = parser.from_properties_impl(
"placeholder", properties
)
self.assertEqual(type_parsing, bool)
self.assertEqual(type_validator, {"default": None})
@@ -22,7 +24,9 @@ class TestBoolTypeParser(TestCase):
"default": True,
}
type_parsing, type_validator = parser.from_properties("placeholder", properties)
type_parsing, type_validator = parser.from_properties_impl(
"placeholder", properties
)
self.assertEqual(type_parsing, bool)
self.assertEqual(type_validator["default"], True)
@@ -36,4 +40,4 @@ class TestBoolTypeParser(TestCase):
}
with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties)
parser.from_properties_impl("placeholder", properties)

View File

@@ -15,7 +15,7 @@ class TestObjectTypeParser(TestCase):
},
}
Model, _args = parser.from_properties("placeholder", properties)
Model, _args = parser.from_properties_impl("placeholder", properties)
obj = Model(name="name", age=10)
@@ -37,7 +37,7 @@ class TestObjectTypeParser(TestCase):
},
}
_, type_validator = parser.from_properties("placeholder", properties)
_, type_validator = parser.from_properties_impl("placeholder", properties)
# Check default value
default_obj = type_validator["default_factory"]()

View File

@@ -11,7 +11,7 @@ def with_test_parser():
mapped_type = str
json_schema_type = "type:invalid"
def from_properties(
def from_properties_impl(
self, name: str, properties: dict[str, any], required: bool = False
): ...
@@ -39,11 +39,3 @@ class TestGenericTypeParser(TestCase):
):
InvalidGenericTypeParser.json_schema_type = None
GenericTypeParser._get_impl({"type": "another_invalid_type"})
def test_invalid_mappings_properties_builder(self):
with (
with_test_parser() as InvalidGenericTypeParser,
self.assertRaises(NotImplementedError),
):
parser = InvalidGenericTypeParser()
parser.mappings_properties_builder({}, required=False)

View File

@@ -255,6 +255,7 @@ class TestSchemaConverter(TestCase):
self.assertEqual(obj.name, "John")
def test_invalid_default_for_string(self):
# Test for default with maxLength
schema_max_length = {
"title": "Person",