feature: add instance level ref cache #63
@@ -31,7 +31,7 @@ class AnyOfTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
sub_types = [
|
sub_types = [
|
||||||
GenericTypeParser.type_from_properties(
|
GenericTypeParser.type_from_properties(
|
||||||
f"{name}_sub{i}", subProperty, **kwargs
|
f"{name}.sub{i}", subProperty, **kwargs
|
||||||
)
|
)
|
||||||
for i, subProperty in enumerate(sub_properties)
|
for i, subProperty in enumerate(sub_properties)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from jambo.exceptions import InternalAssertionException
|
||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.types.json_schema_type import JSONSchema
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
from jambo.types.type_parser_options import TypeParserOptions
|
from jambo.types.type_parser_options import TypeParserOptions
|
||||||
@@ -6,6 +7,8 @@ from pydantic import BaseModel, ConfigDict, Field, create_model
|
|||||||
from pydantic.fields import FieldInfo
|
from pydantic.fields import FieldInfo
|
||||||
from typing_extensions import Unpack
|
from typing_extensions import Unpack
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeParser(GenericTypeParser):
|
class ObjectTypeParser(GenericTypeParser):
|
||||||
mapped_type = object
|
mapped_type = object
|
||||||
@@ -54,14 +57,32 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
: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.
|
||||||
"""
|
"""
|
||||||
model_config = ConfigDict(validate_assignment=True)
|
ref_cache = kwargs.get("ref_cache")
|
||||||
fields = cls._parse_properties(properties, required_keys, **kwargs)
|
if ref_cache is None:
|
||||||
|
raise InternalAssertionException(
|
||||||
|
"`ref_cache` must be provided in kwargs for ObjectTypeParser"
|
||||||
|
)
|
||||||
|
|
||||||
return create_model(name, __config__=model_config, **fields) # type: ignore
|
if (model := ref_cache.get(name)) is not None and isinstance(model, type):
|
||||||
|
warnings.warn(
|
||||||
|
f"Type '{name}' is already in the ref_cache and therefore cached value will be used."
|
||||||
|
" This may indicate a namming collision in the schema or just a normal optimization,"
|
||||||
|
" if this behavior is desired pass a clean ref_cache or use the param `without_cache`"
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
|
||||||
|
model_config = ConfigDict(validate_assignment=True)
|
||||||
|
fields = cls._parse_properties(name, properties, required_keys, **kwargs)
|
||||||
|
|
||||||
|
model = create_model(name, __config__=model_config, **fields) # type: ignore
|
||||||
|
ref_cache[name] = model
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_properties(
|
def _parse_properties(
|
||||||
cls,
|
cls,
|
||||||
|
name: str,
|
||||||
properties: dict[str, JSONSchema],
|
properties: dict[str, JSONSchema],
|
||||||
required_keys: list[str],
|
required_keys: list[str],
|
||||||
**kwargs: Unpack[TypeParserOptions],
|
**kwargs: Unpack[TypeParserOptions],
|
||||||
@@ -69,15 +90,15 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
required_keys = required_keys or []
|
required_keys = required_keys or []
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
for name, prop in properties.items():
|
for field_name, field_prop in properties.items():
|
||||||
sub_property: TypeParserOptions = kwargs.copy()
|
sub_property: TypeParserOptions = kwargs.copy()
|
||||||
sub_property["required"] = name in required_keys
|
sub_property["required"] = field_name in required_keys
|
||||||
|
|
||||||
parsed_type, parsed_properties = GenericTypeParser.type_from_properties(
|
parsed_type, parsed_properties = GenericTypeParser.type_from_properties(
|
||||||
name,
|
f"{name}.{field_name}",
|
||||||
prop,
|
field_prop,
|
||||||
**sub_property, # type: ignore
|
**sub_property, # type: ignore
|
||||||
)
|
)
|
||||||
fields[name] = (parsed_type, Field(**parsed_properties))
|
fields[field_name] = (parsed_type, Field(**parsed_properties))
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from jambo.exceptions import InternalAssertionException, InvalidSchemaException
|
from jambo.exceptions import InternalAssertionException, InvalidSchemaException
|
||||||
from jambo.parser import GenericTypeParser
|
from jambo.parser import GenericTypeParser
|
||||||
|
from jambo.types import RefCacheDict
|
||||||
from jambo.types.json_schema_type import JSONSchema
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
from jambo.types.type_parser_options import TypeParserOptions
|
from jambo.types.type_parser_options import TypeParserOptions
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ class RefTypeParser(GenericTypeParser):
|
|||||||
return mapped_type
|
return mapped_type
|
||||||
|
|
||||||
def _get_ref_from_cache(
|
def _get_ref_from_cache(
|
||||||
self, ref_name: str, ref_cache: dict[str, ForwardRef | type | None]
|
self, ref_name: str, ref_cache: RefCacheDict
|
||||||
) -> RefType | type | None:
|
) -> RefType | type | None:
|
||||||
try:
|
try:
|
||||||
ref_state = ref_cache[ref_name]
|
ref_state = ref_cache[ref_name]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException
|
from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException
|
||||||
from jambo.parser import ObjectTypeParser, RefTypeParser
|
from jambo.parser import ObjectTypeParser, RefTypeParser
|
||||||
from jambo.types import JSONSchema
|
from jambo.types import JSONSchema, RefCacheDict
|
||||||
|
|
||||||
from jsonschema.exceptions import SchemaError
|
from jsonschema.exceptions import SchemaError
|
||||||
from jsonschema.validators import validator_for
|
from jsonschema.validators import validator_for
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing_extensions import Optional
|
||||||
|
|
||||||
|
|
||||||
class SchemaConverter:
|
class SchemaConverter:
|
||||||
@@ -16,13 +17,50 @@ class SchemaConverter:
|
|||||||
fields and types. The generated model can be used for data validation and serialization.
|
fields and types. The generated model can be used for data validation and serialization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
def __init__(self, ref_cache: Optional[RefCacheDict] = None) -> None:
|
||||||
def build(schema: JSONSchema) -> type[BaseModel]:
|
if ref_cache is None:
|
||||||
|
ref_cache = dict()
|
||||||
|
self._ref_cache = ref_cache
|
||||||
|
|
||||||
|
def build_with_instance(
|
||||||
|
self,
|
||||||
|
schema: JSONSchema,
|
||||||
|
ref_cache: Optional[RefCacheDict] = None,
|
||||||
|
without_cache: bool = False,
|
||||||
|
) -> type[BaseModel]:
|
||||||
"""
|
"""
|
||||||
Converts a JSON Schema to a Pydantic model.
|
Converts a JSON Schema to a Pydantic model.
|
||||||
:param schema: The JSON Schema to convert.
|
This is the instance method version of `build` and uses the instance's reference cache if none is provided.
|
||||||
:return: A Pydantic model class.
|
Use this method if you want to utilize the instance's reference cache.
|
||||||
|
|
||||||
|
:param schema: The JSON Schema to convert.
|
||||||
|
:param ref_cache: An optional reference cache to use during conversion.
|
||||||
|
:param without_cache: Whether to use a clean reference cache for this conversion.
|
||||||
|
:return: The generated Pydantic model.
|
||||||
"""
|
"""
|
||||||
|
local_ref_cache: RefCacheDict
|
||||||
|
|
||||||
|
if without_cache:
|
||||||
|
local_ref_cache = dict()
|
||||||
|
elif ref_cache is None:
|
||||||
|
local_ref_cache = self._ref_cache
|
||||||
|
else:
|
||||||
|
local_ref_cache = ref_cache
|
||||||
|
|
||||||
|
return self.build(schema, local_ref_cache)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build(
|
||||||
|
schema: JSONSchema, ref_cache: Optional[RefCacheDict] = None
|
||||||
|
) -> type[BaseModel]:
|
||||||
|
"""
|
||||||
|
Converts a JSON Schema to a Pydantic model.
|
||||||
|
:param schema: The JSON Schema to convert.
|
||||||
|
:param ref_cache: An optional reference cache to use during conversion, if provided `with_clean_cache` will be ignored.
|
||||||
|
:return: The generated Pydantic model.
|
||||||
|
"""
|
||||||
|
if ref_cache is None:
|
||||||
|
ref_cache = dict()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validator = validator_for(schema)
|
validator = validator_for(schema)
|
||||||
@@ -46,7 +84,7 @@ class SchemaConverter:
|
|||||||
schema.get("properties", {}),
|
schema.get("properties", {}),
|
||||||
schema.get("required", []),
|
schema.get("required", []),
|
||||||
context=schema,
|
context=schema,
|
||||||
ref_cache=dict(),
|
ref_cache=ref_cache,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +93,7 @@ class SchemaConverter:
|
|||||||
schema["title"],
|
schema["title"],
|
||||||
schema,
|
schema,
|
||||||
context=schema,
|
context=schema,
|
||||||
ref_cache=dict(),
|
ref_cache=ref_cache,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
return parsed_model
|
return parsed_model
|
||||||
@@ -68,6 +106,25 @@ class SchemaConverter:
|
|||||||
unsupported_field=unsupported_type,
|
unsupported_field=unsupported_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clear_ref_cache(self) -> None:
|
||||||
|
"""
|
||||||
|
Clears the reference cache.
|
||||||
|
"""
|
||||||
|
self._ref_cache.clear()
|
||||||
|
|
||||||
|
def get_cached_ref(self, ref_name: str):
|
||||||
|
"""
|
||||||
|
Gets a cached reference from the reference cache.
|
||||||
|
:param ref_name: The name of the reference to get.
|
||||||
|
:return: The cached reference, or None if not found.
|
||||||
|
"""
|
||||||
|
cached_type = self._ref_cache.get(ref_name)
|
||||||
|
|
||||||
|
if isinstance(cached_type, type):
|
||||||
|
return cached_type
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_schema_type(schema: JSONSchema) -> str | None:
|
def _get_schema_type(schema: JSONSchema) -> str | None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from .json_schema_type import (
|
|||||||
JSONSchemaType,
|
JSONSchemaType,
|
||||||
JSONType,
|
JSONType,
|
||||||
)
|
)
|
||||||
from .type_parser_options import TypeParserOptions
|
from .type_parser_options import RefCacheDict, TypeParserOptions
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -12,5 +12,6 @@ __all__ = [
|
|||||||
"JSONSchemaNativeTypes",
|
"JSONSchemaNativeTypes",
|
||||||
"JSONType",
|
"JSONType",
|
||||||
"JSONSchema",
|
"JSONSchema",
|
||||||
|
"RefCacheDict",
|
||||||
"TypeParserOptions",
|
"TypeParserOptions",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from jambo.types.json_schema_type import JSONSchema
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
|
|
||||||
from typing_extensions import ForwardRef, TypedDict
|
from typing_extensions import ForwardRef, MutableMapping, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
RefCacheDict = MutableMapping[str, ForwardRef | type | None]
|
||||||
|
|
||||||
|
|
||||||
class TypeParserOptions(TypedDict):
|
class TypeParserOptions(TypedDict):
|
||||||
required: bool
|
required: bool
|
||||||
context: JSONSchema
|
context: JSONSchema
|
||||||
ref_cache: dict[str, ForwardRef | type | None]
|
ref_cache: RefCacheDict
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties, ref_cache={}
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -87,7 +87,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties, ref_cache={}
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -116,7 +116,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties, ref_cache={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, str)
|
self.assertEqual(type_parsing, str)
|
||||||
@@ -137,7 +137,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties, ref_cache={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, str)
|
self.assertEqual(type_parsing, str)
|
||||||
@@ -158,7 +158,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
AllOfTypeParser().from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties, ref_cache={})
|
||||||
|
|
||||||
def test_all_of_invalid_type_not_present(self):
|
def test_all_of_invalid_type_not_present(self):
|
||||||
properties = {
|
properties = {
|
||||||
@@ -171,7 +171,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
AllOfTypeParser().from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties, ref_cache={})
|
||||||
|
|
||||||
def test_all_of_invalid_type_in_fields(self):
|
def test_all_of_invalid_type_in_fields(self):
|
||||||
properties = {
|
properties = {
|
||||||
@@ -184,7 +184,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
AllOfTypeParser().from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties, ref_cache={})
|
||||||
|
|
||||||
def test_all_of_invalid_type_not_all_equal(self):
|
def test_all_of_invalid_type_not_all_equal(self):
|
||||||
"""
|
"""
|
||||||
@@ -200,7 +200,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
AllOfTypeParser().from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties, ref_cache={})
|
||||||
|
|
||||||
def test_all_of_description_field(self):
|
def test_all_of_description_field(self):
|
||||||
"""
|
"""
|
||||||
@@ -237,7 +237,9 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
type_parsing, _ = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties, ref_cache={}
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
type_parsing.model_json_schema()["properties"]["name"]["description"],
|
type_parsing.model_json_schema()["properties"]["name"]["description"],
|
||||||
@@ -275,7 +277,9 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
type_parsing, _ = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties, ref_cache={}
|
||||||
|
)
|
||||||
obj = type_parsing()
|
obj = type_parsing()
|
||||||
self.assertEqual(obj.name, "John")
|
self.assertEqual(obj.name, "John")
|
||||||
self.assertEqual(obj.age, 30)
|
self.assertEqual(obj.age, 30)
|
||||||
@@ -308,7 +312,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
AllOfTypeParser().from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties, ref_cache={})
|
||||||
|
|
||||||
def test_all_of_with_root_examples(self):
|
def test_all_of_with_root_examples(self):
|
||||||
"""
|
"""
|
||||||
@@ -344,7 +348,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsed, type_properties = AllOfTypeParser().from_properties(
|
type_parsed, type_properties = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties, ref_cache={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
from jambo.exceptions import InternalAssertionException
|
||||||
from jambo.parser import ObjectTypeParser
|
from jambo.parser import ObjectTypeParser
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestObjectTypeParser(TestCase):
|
class TestObjectTypeParser(TestCase):
|
||||||
|
def test_object_type_parser_throws_without_ref_cache(self):
|
||||||
|
parser = ObjectTypeParser()
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(InternalAssertionException):
|
||||||
|
parser.from_properties_impl("placeholder", properties)
|
||||||
|
|
||||||
def test_object_type_parser(self):
|
def test_object_type_parser(self):
|
||||||
parser = ObjectTypeParser()
|
parser = ObjectTypeParser()
|
||||||
|
|
||||||
@@ -15,7 +30,9 @@ class TestObjectTypeParser(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Model, _args = parser.from_properties_impl("placeholder", properties)
|
Model, _args = parser.from_properties_impl(
|
||||||
|
"placeholder", properties, ref_cache={}
|
||||||
|
)
|
||||||
|
|
||||||
obj = Model(name="name", age=10)
|
obj = Model(name="name", age=10)
|
||||||
|
|
||||||
@@ -39,7 +56,9 @@ class TestObjectTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
_, type_validator = parser.from_properties_impl("placeholder", properties)
|
_, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties, ref_cache={}
|
||||||
|
)
|
||||||
|
|
||||||
test_example = type_validator["examples"][0]
|
test_example = type_validator["examples"][0]
|
||||||
|
|
||||||
@@ -61,7 +80,9 @@ class TestObjectTypeParser(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, type_validator = parser.from_properties_impl("placeholder", properties)
|
_, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties, ref_cache={}
|
||||||
|
)
|
||||||
|
|
||||||
# Check default value
|
# Check default value
|
||||||
default_obj = type_validator["default_factory"]()
|
default_obj = type_validator["default_factory"]()
|
||||||
@@ -71,3 +92,18 @@ class TestObjectTypeParser(TestCase):
|
|||||||
# Chekc default factory new object id
|
# Chekc default factory new object id
|
||||||
new_obj = type_validator["default_factory"]()
|
new_obj = type_validator["default_factory"]()
|
||||||
self.assertNotEqual(id(default_obj), id(new_obj))
|
self.assertNotEqual(id(default_obj), id(new_obj))
|
||||||
|
|
||||||
|
def test_object_type_parser_warns_if_object_override_in_cache(self):
|
||||||
|
ref_cache = {}
|
||||||
|
|
||||||
|
parser = ObjectTypeParser()
|
||||||
|
|
||||||
|
properties = {"type": "object", "properties": {}}
|
||||||
|
|
||||||
|
with self.assertWarns(UserWarning):
|
||||||
|
_, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties, ref_cache=ref_cache
|
||||||
|
)
|
||||||
|
_, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties, ref_cache=ref_cache
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ def is_pydantic_model(cls):
|
|||||||
|
|
||||||
|
|
||||||
class TestSchemaConverter(TestCase):
|
class TestSchemaConverter(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.converter = SchemaConverter()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.converter.clear_ref_cache()
|
||||||
|
|
||||||
def test_invalid_schema(self):
|
def test_invalid_schema(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": 1,
|
"title": 1,
|
||||||
@@ -27,7 +33,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
def test_invalid_schema_type(self):
|
def test_invalid_schema_type(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -41,7 +47,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
def test_build_expects_title(self):
|
def test_build_expects_title(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -54,7 +60,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
def test_build_expects_object(self):
|
def test_build_expects_object(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -64,7 +70,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(UnsupportedSchemaException):
|
with self.assertRaises(UnsupportedSchemaException):
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
def test_is_invalid_field(self):
|
def test_is_invalid_field(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -80,7 +86,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException) as context:
|
with self.assertRaises(InvalidSchemaException) as context:
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
self.assertTrue("Unknown type" in str(context.exception))
|
self.assertTrue("Unknown type" in str(context.exception))
|
||||||
|
|
||||||
def test_jsonschema_to_pydantic(self):
|
def test_jsonschema_to_pydantic(self):
|
||||||
@@ -95,7 +101,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertTrue(is_pydantic_model(model))
|
self.assertTrue(is_pydantic_model(model))
|
||||||
|
|
||||||
@@ -116,7 +122,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(model(name="John", age=30).name, "John")
|
self.assertEqual(model(name="John", age=30).name, "John")
|
||||||
|
|
||||||
@@ -147,7 +153,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["age"],
|
"required": ["age"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(model(age=30).age, 30)
|
self.assertEqual(model(age=30).age, 30)
|
||||||
|
|
||||||
@@ -172,7 +178,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["age"],
|
"required": ["age"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(model(age=30).age, 30.0)
|
self.assertEqual(model(age=30).age, 30.0)
|
||||||
|
|
||||||
@@ -193,7 +199,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["is_active"],
|
"required": ["is_active"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(model(is_active=True).is_active, True)
|
self.assertEqual(model(is_active=True).is_active, True)
|
||||||
|
|
||||||
@@ -216,7 +222,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["friends"],
|
"required": ["friends"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"}
|
model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"}
|
||||||
@@ -229,26 +235,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
model(friends=["John", "Jane", "Invalid"])
|
model(friends=["John", "Jane", "Invalid"])
|
||||||
|
|
||||||
def test_validation_list_with_missing_items(self):
|
def test_validation_list_with_missing_items(self):
|
||||||
model = SchemaConverter.build(
|
model = self.converter.build_with_instance(
|
||||||
{
|
|
||||||
"title": "Person",
|
|
||||||
"description": "A person",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"friends": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"minItems": 1,
|
|
||||||
"maxItems": 2,
|
|
||||||
"default": ["John", "Jane"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(model().friends, ["John", "Jane"])
|
|
||||||
|
|
||||||
model = SchemaConverter.build(
|
|
||||||
{
|
{
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
"description": "A person",
|
"description": "A person",
|
||||||
@@ -286,7 +273,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["address"],
|
"required": ["address"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = model(address={"street": "123 Main St", "city": "Springfield"})
|
obj = model(address={"street": "123 Main St", "city": "Springfield"})
|
||||||
|
|
||||||
@@ -310,7 +297,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = model(name="John")
|
obj = model(name="John")
|
||||||
|
|
||||||
@@ -333,7 +320,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
SchemaConverter.build(schema_max_length)
|
self.converter.build_with_instance(schema_max_length)
|
||||||
|
|
||||||
def test_default_for_list(self):
|
def test_default_for_list(self):
|
||||||
schema_list = {
|
schema_list = {
|
||||||
@@ -350,10 +337,11 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["friends"],
|
"required": ["friends"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model_list = SchemaConverter.build(schema_list)
|
model_list = self.converter.build_with_instance(schema_list)
|
||||||
|
|
||||||
self.assertEqual(model_list().friends, ["John", "Jane"])
|
self.assertEqual(model_list().friends, ["John", "Jane"])
|
||||||
|
|
||||||
|
def test_default_for_list_with_unique_items(self):
|
||||||
# Test for default with uniqueItems
|
# Test for default with uniqueItems
|
||||||
schema_set = {
|
schema_set = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
@@ -370,7 +358,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["friends"],
|
"required": ["friends"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model_set = SchemaConverter.build(schema_set)
|
model_set = self.converter.build_with_instance(schema_set)
|
||||||
|
|
||||||
self.assertEqual(model_set().friends, {"John", "Jane"})
|
self.assertEqual(model_set().friends, {"John", "Jane"})
|
||||||
|
|
||||||
@@ -392,7 +380,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["address"],
|
"required": ["address"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = model(address={"street": "123 Main St", "city": "Springfield"})
|
obj = model(address={"street": "123 Main St", "city": "Springfield"})
|
||||||
|
|
||||||
@@ -416,7 +404,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model(
|
obj = Model(
|
||||||
name="J",
|
name="J",
|
||||||
@@ -445,7 +433,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model(id=1)
|
obj = Model(id=1)
|
||||||
self.assertEqual(obj.id, 1)
|
self.assertEqual(obj.id, 1)
|
||||||
@@ -469,7 +457,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"email": {"type": "string", "format": "email"}},
|
"properties": {"email": {"type": "string", "format": "email"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(model(email="test@example.com").email, "test@example.com")
|
self.assertEqual(model(email="test@example.com").email, "test@example.com")
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -482,7 +470,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"website": {"type": "string", "format": "uri"}},
|
"properties": {"website": {"type": "string", "format": "uri"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(website="https://example.com").website, AnyUrl("https://example.com")
|
model(website="https://example.com").website, AnyUrl("https://example.com")
|
||||||
)
|
)
|
||||||
@@ -497,7 +485,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"ip": {"type": "string", "format": "ipv4"}},
|
"properties": {"ip": {"type": "string", "format": "ipv4"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(model(ip="192.168.1.1").ip, IPv4Address("192.168.1.1"))
|
self.assertEqual(model(ip="192.168.1.1").ip, IPv4Address("192.168.1.1"))
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -510,7 +498,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"ip": {"type": "string", "format": "ipv6"}},
|
"properties": {"ip": {"type": "string", "format": "ipv6"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(ip="2001:0db8:85a3:0000:0000:8a2e:0370:7334").ip,
|
model(ip="2001:0db8:85a3:0000:0000:8a2e:0370:7334").ip,
|
||||||
IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||||
@@ -526,7 +514,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"id": {"type": "string", "format": "uuid"}},
|
"properties": {"id": {"type": "string", "format": "uuid"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(id="123e4567-e89b-12d3-a456-426614174000").id,
|
model(id="123e4567-e89b-12d3-a456-426614174000").id,
|
||||||
@@ -543,7 +531,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"hostname": {"type": "string", "format": "hostname"}},
|
"properties": {"hostname": {"type": "string", "format": "hostname"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(model(hostname="example.com").hostname, "example.com")
|
self.assertEqual(model(hostname="example.com").hostname, "example.com")
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -556,7 +544,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"timestamp": {"type": "string", "format": "date-time"}},
|
"properties": {"timestamp": {"type": "string", "format": "date-time"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(timestamp="2024-01-01T12:00:00Z").timestamp.isoformat(),
|
model(timestamp="2024-01-01T12:00:00Z").timestamp.isoformat(),
|
||||||
"2024-01-01T12:00:00+00:00",
|
"2024-01-01T12:00:00+00:00",
|
||||||
@@ -572,7 +560,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"properties": {"time": {"type": "string", "format": "time"}},
|
"properties": {"time": {"type": "string", "format": "time"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
model(time="20:20:39+00:00").time.isoformat(), "20:20:39+00:00"
|
model(time="20:20:39+00:00").time.isoformat(), "20:20:39+00:00"
|
||||||
)
|
)
|
||||||
@@ -588,7 +576,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(InvalidSchemaException):
|
with self.assertRaises(InvalidSchemaException):
|
||||||
SchemaConverter.build(schema)
|
self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
def test_ref_with_root_ref(self):
|
def test_ref_with_root_ref(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -604,7 +592,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name", "age"],
|
"required": ["name", "age"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = model(
|
obj = model(
|
||||||
name="John",
|
name="John",
|
||||||
@@ -639,7 +627,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = model(
|
obj = model(
|
||||||
name="John",
|
name="John",
|
||||||
@@ -678,7 +666,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model(
|
obj = Model(
|
||||||
name="John",
|
name="John",
|
||||||
@@ -704,7 +692,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["status"],
|
"required": ["status"],
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model(status="active")
|
obj = Model(status="active")
|
||||||
self.assertEqual(obj.status.value, "active")
|
self.assertEqual(obj.status.value, "active")
|
||||||
@@ -723,7 +711,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["status"],
|
"required": ["status"],
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model()
|
obj = Model()
|
||||||
self.assertEqual(obj.status.value, "active")
|
self.assertEqual(obj.status.value, "active")
|
||||||
@@ -740,7 +728,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model()
|
obj = Model()
|
||||||
self.assertEqual(obj.name, "United States of America")
|
self.assertEqual(obj.name, "United States of America")
|
||||||
@@ -763,7 +751,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model()
|
obj = Model()
|
||||||
self.assertEqual(obj.name, ["Brazil"])
|
self.assertEqual(obj.name, ["Brazil"])
|
||||||
@@ -783,7 +771,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Model = SchemaConverter.build(schema)
|
Model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
obj = Model()
|
obj = Model()
|
||||||
self.assertIsNone(obj.a_thing)
|
self.assertIsNone(obj.a_thing)
|
||||||
@@ -825,7 +813,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
schema_type = SchemaConverter.build(schema)
|
schema_type = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
# check for me that the types generated by the oneOf in the typing.Annotated have different names
|
# check for me that the types generated by the oneOf in the typing.Annotated have different names
|
||||||
operating_system_field = schema_type.model_fields["operating_system"]
|
operating_system_field = schema_type.model_fields["operating_system"]
|
||||||
@@ -839,7 +827,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
def test_object_invalid_require(self):
|
def test_object_invalid_require(self):
|
||||||
# https://github.com/HideyoshiNakazone/jambo/issues/60
|
# https://github.com/HideyoshiNakazone/jambo/issues/60
|
||||||
object_ = SchemaConverter.build(
|
object_ = self.converter.build_with_instance(
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"title": "TEST",
|
"title": "TEST",
|
||||||
@@ -866,3 +854,147 @@ class TestSchemaConverter(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(object_.model_fields["description"].is_required()) # FAIL
|
self.assertFalse(object_.model_fields["description"].is_required()) # FAIL
|
||||||
|
|
||||||
|
def test_instance_level_ref_cache(self):
|
||||||
|
ref_cache = {}
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"emergency_contact": {
|
||||||
|
"$ref": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "age"],
|
||||||
|
}
|
||||||
|
|
||||||
|
converter1 = SchemaConverter(ref_cache)
|
||||||
|
model1 = converter1.build_with_instance(schema)
|
||||||
|
|
||||||
|
converter2 = SchemaConverter(ref_cache)
|
||||||
|
model2 = converter2.build_with_instance(schema)
|
||||||
|
|
||||||
|
self.assertIs(converter1._ref_cache, converter2._ref_cache)
|
||||||
|
self.assertIs(model1, model2)
|
||||||
|
|
||||||
|
def test_instance_level_ref_cache_isolation_via_without_cache_param(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"emergency_contact": {
|
||||||
|
"$ref": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "age"],
|
||||||
|
}
|
||||||
|
|
||||||
|
model1 = self.converter.build_with_instance(schema, without_cache=True)
|
||||||
|
model2 = self.converter.build_with_instance(schema, without_cache=True)
|
||||||
|
|
||||||
|
self.assertIsNot(model1, model2)
|
||||||
|
|
||||||
|
def test_instance_level_ref_cache_isolation_via_provided_cache(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"emergency_contact": {
|
||||||
|
"$ref": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "age"],
|
||||||
|
}
|
||||||
|
|
||||||
|
model1 = self.converter.build_with_instance(schema, ref_cache={})
|
||||||
|
model2 = self.converter.build_with_instance(schema, ref_cache={})
|
||||||
|
|
||||||
|
self.assertIsNot(model1, model2)
|
||||||
|
|
||||||
|
def test_get_type_from_cache(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"emergency_contact": {
|
||||||
|
"$ref": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "age"],
|
||||||
|
}
|
||||||
|
|
||||||
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
|
cached_model = self.converter.get_cached_ref("Person")
|
||||||
|
|
||||||
|
self.assertIs(model, cached_model)
|
||||||
|
|
||||||
|
def test_get_type_from_cache_not_found(self):
|
||||||
|
cached_model = self.converter.get_cached_ref("NonExistentModel")
|
||||||
|
|
||||||
|
self.assertIsNone(cached_model)
|
||||||
|
|
||||||
|
def test_get_type_from_cache_nested_type(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"address": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"street": {"type": "string"},
|
||||||
|
"city": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["street", "city"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "age", "address"],
|
||||||
|
}
|
||||||
|
|
||||||
|
model = self.converter.build_with_instance(schema)
|
||||||
|
|
||||||
|
cached_model = self.converter.get_cached_ref("Person.address")
|
||||||
|
|
||||||
|
self.assertIsNotNone(cached_model)
|
||||||
|
self.assertIs(model.model_fields["address"].annotation, cached_model)
|
||||||
|
|
||||||
|
def test_get_type_from_cache_with_def(self):
|
||||||
|
schema = {
|
||||||
|
"title": "person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"address": {"$ref": "#/$defs/address"},
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"address": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"street": {"type": "string"},
|
||||||
|
"city": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["street", "city"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
person_model = self.converter.build_with_instance(schema)
|
||||||
|
cached_person_model = self.converter.get_cached_ref("person")
|
||||||
|
|
||||||
|
self.assertIs(person_model, cached_person_model)
|
||||||
|
|
||||||
|
cached_address_model = self.converter.get_cached_ref("address")
|
||||||
|
|
||||||
|
self.assertIsNotNone(cached_address_model)
|
||||||
|
|||||||
Reference in New Issue
Block a user