Better Object Internal Structure and Type Selection #16
@@ -12,7 +12,7 @@ T = TypeVar("T")
|
|||||||
class GenericTypeParser(ABC, Generic[T]):
|
class GenericTypeParser(ABC, Generic[T]):
|
||||||
json_schema_type: str = None
|
json_schema_type: str = None
|
||||||
|
|
||||||
type_mappings: dict[str, str] = None
|
type_mappings: dict[str, str] = {}
|
||||||
|
|
||||||
default_mappings = {
|
default_mappings = {
|
||||||
"default": "default",
|
"default": "default",
|
||||||
@@ -20,14 +20,51 @@ class GenericTypeParser(ABC, Generic[T]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@abstractmethod
|
@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(
|
def from_properties(
|
||||||
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
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
|
@classmethod
|
||||||
def type_from_properties(
|
def type_from_properties(
|
||||||
cls, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
cls, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
||||||
) -> tuple[type, dict]:
|
) -> 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)
|
parser = cls._get_impl(properties)
|
||||||
|
|
||||||
return parser().from_properties(name=name, properties=properties, **kwargs)
|
return parser().from_properties(name=name, properties=properties, **kwargs)
|
||||||
@@ -62,9 +99,6 @@ class GenericTypeParser(ABC, Generic[T]):
|
|||||||
def mappings_properties_builder(
|
def mappings_properties_builder(
|
||||||
self, properties, **kwargs: Unpack[TypeParserOptions]
|
self, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if self.type_mappings is None:
|
|
||||||
raise NotImplementedError("Type mappings not defined")
|
|
||||||
|
|
||||||
if not kwargs.get("required", False):
|
if not kwargs.get("required", False):
|
||||||
properties["default"] = properties.get("default", None)
|
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
|
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
|
||||||
field = Annotated[field_type, Field(**field_prop)]
|
def _validate_default(field_type: type, field_prop: dict) -> bool:
|
||||||
TypeAdapter(field).validate_python(value)
|
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"
|
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", [])
|
sub_properties = properties.get("allOf", [])
|
||||||
|
|
||||||
root_type = properties.get("type")
|
root_type = properties.get("type")
|
||||||
@@ -23,7 +25,7 @@ class AllOfTypeParser(GenericTypeParser):
|
|||||||
sub_properties
|
sub_properties
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser().from_properties(name, combined_properties, **kwargs)
|
return parser().from_properties_impl(name, combined_properties, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_type_parser(
|
def _get_type_parser(
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ class AnyOfTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "anyOf"
|
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:
|
if "anyOf" not in properties:
|
||||||
raise ValueError(f"Invalid JSON Schema: {properties}")
|
raise ValueError(f"Invalid JSON Schema: {properties}")
|
||||||
|
|
||||||
if not isinstance(properties["anyOf"], list):
|
if not isinstance(properties["anyOf"], list):
|
||||||
raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}")
|
raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}")
|
||||||
|
|
||||||
mapped_properties = dict()
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
|
||||||
sub_properties = properties["anyOf"]
|
sub_properties = properties["anyOf"]
|
||||||
|
|
||||||
@@ -26,21 +28,6 @@ class AnyOfTypeParser(GenericTypeParser):
|
|||||||
for subProperty in sub_properties
|
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):
|
if not kwargs.get("required", False):
|
||||||
mapped_properties["default"] = mapped_properties.get("default")
|
mapped_properties["default"] = mapped_properties.get("default")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.types.type_parser_options import TypeParserOptions
|
from jambo.types.type_parser_options import TypeParserOptions
|
||||||
|
|
||||||
from typing_extensions import TypeVar, Unpack
|
from typing_extensions import Iterable, TypeVar, Unpack
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
@@ -21,7 +21,9 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
"minItems": "min_length",
|
"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 = kwargs.copy()
|
||||||
item_properties["required"] = True
|
item_properties["required"] = True
|
||||||
_item_type, _item_args = GenericTypeParser.type_from_properties(
|
_item_type, _item_args = GenericTypeParser.type_from_properties(
|
||||||
@@ -33,21 +35,20 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
|
||||||
default_list = properties.pop("default", None)
|
if "default" not in mapped_properties:
|
||||||
if default_list is not None:
|
mapped_properties["default_factory"] = self._build_default_factory(
|
||||||
self.validate_default(
|
properties.get("default"), wrapper_type
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
return field_type, mapped_properties
|
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",
|
"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)
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
|
||||||
default_value = properties.get("default")
|
default_value = properties.get("default")
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ class FloatTypeParser(GenericTypeParser):
|
|||||||
"default": "default",
|
"default": "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
|
def from_properties_impl(
|
||||||
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
|
):
|
||||||
default_value = mapped_properties.get("default")
|
return float, self.mappings_properties_builder(properties, **kwargs)
|
||||||
if default_value is not None:
|
|
||||||
self.validate_default(float, mapped_properties, default_value)
|
|
||||||
|
|
||||||
return float, mapped_properties
|
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ class IntTypeParser(GenericTypeParser):
|
|||||||
"default": "default",
|
"default": "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]):
|
def from_properties_impl(
|
||||||
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
|
):
|
||||||
default_value = mapped_properties.get("default")
|
return int, self.mappings_properties_builder(properties, **kwargs)
|
||||||
if default_value is not None:
|
|
||||||
self.validate_default(int, mapped_properties, default_value)
|
|
||||||
|
|
||||||
return int, mapped_properties
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "type:object"
|
json_schema_type = "type:object"
|
||||||
|
|
||||||
def from_properties(
|
def from_properties_impl(
|
||||||
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
||||||
):
|
):
|
||||||
type_parsing = self.to_model(
|
type_parsing = self.to_model(
|
||||||
@@ -39,7 +39,7 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
"""
|
"""
|
||||||
Converts JSON Schema object properties to a Pydantic model.
|
Converts JSON Schema object properties to a Pydantic model.
|
||||||
:param name: The name of the 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.
|
:param required_keys: List of required keys in the schema.
|
||||||
:return: A Pydantic model class.
|
: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])?)*$",
|
"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]):
|
def from_properties_impl(
|
||||||
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
|
):
|
||||||
|
mapped_properties = self.mappings_properties_builder(
|
||||||
|
properties, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
format_type = properties.get("format")
|
format_type = properties.get("format")
|
||||||
if format_type:
|
if not format_type:
|
||||||
if format_type in self.format_type_mapping:
|
return str, mapped_properties
|
||||||
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 format_type not in self.format_type_mapping:
|
||||||
if default_value is not None:
|
raise ValueError(f"Unsupported string format: {format_type}")
|
||||||
self.validate_default(mapped_type, mapped_properties, default_value)
|
|
||||||
|
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
|
return mapped_type, mapped_properties
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class TestArrayTypeParser(TestCase):
|
|||||||
def test_array_parser_with_invalid_default_type(self):
|
def test_array_parser_with_invalid_default_type(self):
|
||||||
parser = ArrayTypeParser()
|
parser = ArrayTypeParser()
|
||||||
|
|
||||||
properties = {"items": {"type": "string"}, "default": "not_a_list"}
|
properties = {"items": {"type": "string"}, "default": 000}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class TestBoolTypeParser(TestCase):
|
|||||||
|
|
||||||
properties = {"type": "boolean"}
|
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_parsing, bool)
|
||||||
self.assertEqual(type_validator, {"default": None})
|
self.assertEqual(type_validator, {"default": None})
|
||||||
@@ -22,7 +24,9 @@ class TestBoolTypeParser(TestCase):
|
|||||||
"default": True,
|
"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_parsing, bool)
|
||||||
self.assertEqual(type_validator["default"], True)
|
self.assertEqual(type_validator["default"], True)
|
||||||
@@ -36,4 +40,4 @@ class TestBoolTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
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)
|
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
|
# Check default value
|
||||||
default_obj = type_validator["default_factory"]()
|
default_obj = type_validator["default_factory"]()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def with_test_parser():
|
|||||||
mapped_type = str
|
mapped_type = str
|
||||||
json_schema_type = "type:invalid"
|
json_schema_type = "type:invalid"
|
||||||
|
|
||||||
def from_properties(
|
def from_properties_impl(
|
||||||
self, name: str, properties: dict[str, any], required: bool = False
|
self, name: str, properties: dict[str, any], required: bool = False
|
||||||
): ...
|
): ...
|
||||||
|
|
||||||
@@ -39,11 +39,3 @@ class TestGenericTypeParser(TestCase):
|
|||||||
):
|
):
|
||||||
InvalidGenericTypeParser.json_schema_type = None
|
InvalidGenericTypeParser.json_schema_type = None
|
||||||
GenericTypeParser._get_impl({"type": "another_invalid_type"})
|
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")
|
self.assertEqual(obj.name, "John")
|
||||||
|
|
||||||
|
def test_invalid_default_for_string(self):
|
||||||
# Test for default with maxLength
|
# Test for default with maxLength
|
||||||
schema_max_length = {
|
schema_max_length = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
|
|||||||
Reference in New Issue
Block a user