feat: adds caching per namespace #67

Merged
HideyoshiNakazone merged 3 commits from feature/cache-per-namespace into main 2025-11-26 18:28:17 +00:00
2 changed files with 150 additions and 11 deletions
Showing only changes of commit fcea994dd6 - Show all commits

View File

@@ -17,10 +17,12 @@ 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.
""" """
def __init__(self, ref_cache: Optional[RefCacheDict] = None) -> None: def __init__(
if ref_cache is None: self, namespace_registry: Optional[dict[str, RefCacheDict]] = None
ref_cache = dict() ) -> None:
self._ref_cache = ref_cache if namespace_registry is None:
namespace_registry = dict()
self._namespace_registry = namespace_registry
def build_with_cache( def build_with_cache(
self, self,
@@ -43,7 +45,8 @@ class SchemaConverter:
if without_cache: if without_cache:
local_ref_cache = dict() local_ref_cache = dict()
elif ref_cache is None: elif ref_cache is None:
local_ref_cache = self._ref_cache namespace = schema.get("$id", "default")
local_ref_cache = self._namespace_registry.setdefault(namespace, dict())
else: else:
local_ref_cache = ref_cache local_ref_cache = ref_cache
@@ -107,19 +110,28 @@ class SchemaConverter:
unsupported_field=unsupported_type, unsupported_field=unsupported_type,
) )
def clear_ref_cache(self) -> None: def clear_ref_cache(self, namespace: Optional[str] = "default") -> None:
""" """
Clears the reference cache. Clears the reference cache.
""" """
self._ref_cache.clear() if namespace is None:
self._namespace_registry.clear()
return
def get_cached_ref(self, ref_name: str): if namespace in self._namespace_registry:
self._namespace_registry[namespace].clear()
def get_cached_ref(
self, ref_name: str, namespace: str = "default"
) -> Optional[type]:
""" """
Gets a cached reference from the reference cache. Gets a cached reference from the reference cache.
:param ref_name: The name of the reference to get. :param ref_name: The name of the reference to get.
:return: The cached reference, or None if not found. :return: The cached reference, or None if not found.
""" """
cached_type = self._ref_cache.get(ref_name) cached_type = self._namespace_registry.get(
namespace, {}
).get(ref_name)
if isinstance(cached_type, type): if isinstance(cached_type, type):
return cached_type return cached_type

View File

@@ -877,7 +877,6 @@ class TestSchemaConverter(TestCase):
converter2 = SchemaConverter(ref_cache) converter2 = SchemaConverter(ref_cache)
model2 = converter2.build_with_cache(schema) model2 = converter2.build_with_cache(schema)
self.assertIs(converter1._ref_cache, converter2._ref_cache)
self.assertIs(model1, model2) self.assertIs(model1, model2)
def test_instance_level_ref_cache_isolation_via_without_cache_param(self): def test_instance_level_ref_cache_isolation_via_without_cache_param(self):
@@ -1041,3 +1040,131 @@ class TestSchemaConverter(TestCase):
with self.assertRaises(InvalidSchemaException): with self.assertRaises(InvalidSchemaException):
self.converter.build_with_cache(schema) self.converter.build_with_cache(schema)
def tests_instance_level_ref_cache_isolation_via_property_id(self):
schema1: JSONSchema = {
"$id": "http://example.com/schemas/person1.json",
"title": "Person",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"emergency_contact": {
"$ref": "#",
},
},
"required": ["name", "age"],
}
model1 = self.converter.build_with_cache(schema1)
schema2: JSONSchema = {
"$id": "http://example.com/schemas/person2.json",
"title": "Person",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"address": {"type": "string"},
},
"required": ["name", "age", "address"],
}
model2 = self.converter.build_with_cache(schema2)
self.assertIsNot(model1, model2)
def tests_instance_level_ref_cache_colision_when_same_property_id(self):
schema1: JSONSchema = {
"$id": "http://example.com/schemas/person.json",
"title": "Person",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"emergency_contact": {
"$ref": "#",
},
},
"required": ["name", "age"],
}
model1 = self.converter.build_with_cache(schema1)
schema2: JSONSchema = {
"$id": "http://example.com/schemas/person.json",
"title": "Person",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"address": {"type": "string"},
},
"required": ["name", "age", "address"],
}
model2 = self.converter.build_with_cache(schema2)
self.assertIs(model1, model2)
def test_namespace_isolation_via_on_call_config(self):
namespace = "namespace1"
schema: JSONSchema = {
"$id": namespace,
"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_cache(schema)
invalid_cached_model = self.converter.get_cached_ref("Person")
self.assertIsNone(invalid_cached_model)
cached_model = self.converter.get_cached_ref("Person", namespace=namespace)
self.assertIs(model, cached_model)
def test_clear_namespace_registry(self):
namespace = "namespace_to_clear"
schema: JSONSchema = {
"$id": namespace,
"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_cache(schema)
cached_model = self.converter.get_cached_ref("Person", namespace=namespace)
self.assertIs(model, cached_model)
self.converter.clear_ref_cache(namespace=namespace)
cleared_cached_model = self.converter.get_cached_ref("Person", namespace=namespace)
self.assertIsNone(cleared_cached_model)