Fixes Default Values in StringTypeParser
This commit is contained in:
@@ -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:
|
||||
@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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 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
|
||||
if "default" not in mapped_properties:
|
||||
mapped_properties["default_factory"] = self._build_default_factory(
|
||||
properties.get("default"), wrapper_type
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
if not format_type:
|
||||
return str, mapped_properties
|
||||
|
||||
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
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unsupported string format: {format_type}")
|
||||
else:
|
||||
mapped_type = str
|
||||
|
||||
default_value = properties.get("default")
|
||||
if default_value is not None:
|
||||
self.validate_default(mapped_type, mapped_properties, default_value)
|
||||
mapped_properties["pattern"] = self.format_pattern_mapping[format_type]
|
||||
|
||||
return mapped_type, mapped_properties
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user