diff --git a/jambo/parser/ref_type_parser.py b/jambo/parser/ref_type_parser.py index c27f62c..8f7ff7c 100644 --- a/jambo/parser/ref_type_parser.py +++ b/jambo/parser/ref_type_parser.py @@ -4,7 +4,7 @@ from jambo.types.type_parser_options import TypeParserOptions from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack -RefType = TypeVar("RefType", bound=Union[int, str]) +RefType = TypeVar("RefType", bound=Union[type, ForwardRef]) class RefTypeParser(GenericTypeParser): @@ -30,16 +30,13 @@ class RefTypeParser(GenericTypeParser): "Look into $defs and # for recursive references." ) - ref_type = None - mapped_properties = {} - if properties["$ref"] == "#": if "title" not in context: raise ValueError( "RefTypeParser: Missing title in properties for $ref #" ) - ref_type = ForwardRef(context["title"]) + return ForwardRef(context["title"]), {} elif properties["$ref"].startswith("#/$defs/"): target_name = None @@ -56,17 +53,8 @@ class RefTypeParser(GenericTypeParser): if target_name is None or target_property is None: raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}") - ref_type, mapped_properties = GenericTypeParser.type_from_properties( + return GenericTypeParser.type_from_properties( target_name, target_property, **kwargs ) - else: - raise ValueError( - "RefTypeParser: Invalid $ref format. " - "Only local references are supported." - ) - - if not required: - mapped_properties["default"] = None - - return ref_type, mapped_properties + raise ValueError(f"RefTypeParser: Unsupported $ref {properties['$ref']}") diff --git a/jambo/types/type_parser_options.py b/jambo/types/type_parser_options.py index ae96338..eb20db2 100644 --- a/jambo/types/type_parser_options.py +++ b/jambo/types/type_parser_options.py @@ -1,7 +1,9 @@ -from typing_extensions import Any, NotRequired, TypedDict +from jambo.types.json_schema_type import JSONSchema + +from typing_extensions import NotRequired, TypedDict class TypeParserOptions(TypedDict): required: bool - context: dict[str, Any] + context: JSONSchema ref_cache: NotRequired[dict[str, type]] diff --git a/tests/parser/test_ref_type_parser.py b/tests/parser/test_ref_type_parser.py index 5ab96f5..2b276bd 100644 --- a/tests/parser/test_ref_type_parser.py +++ b/tests/parser/test_ref_type_parser.py @@ -1,10 +1,12 @@ -from jambo.parser import RefTypeParser +from jambo.parser import ObjectTypeParser, RefTypeParser + +from typing_extensions import ForwardRef, get_type_hints from unittest import TestCase class TestRefTypeParser(TestCase): - def test_ref_type_parser_local_ref(self): + def test_ref_type_parser_with_def(self): properties = { "title": "person", "$ref": "#/$defs/person", @@ -20,8 +22,8 @@ class TestRefTypeParser(TestCase): } type_parsing, type_validator = RefTypeParser().from_properties( - properties=properties, - name="placeholder", + "person", + properties, context=properties, required=True, ) @@ -32,3 +34,50 @@ class TestRefTypeParser(TestCase): self.assertEqual(obj.name, "John") self.assertEqual(obj.age, 30) + + def test_ref_type_parser_with_forward_ref(self): + properties = { + "title": "person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "emergency_contact": { + "$ref": "#", + }, + }, + } + + type_parsing, type_validator = ObjectTypeParser().from_properties( + "person", + properties, + context=properties, + required=True, + ) + type_parsing.update_forward_refs(person=type_parsing) + + self.assertIsInstance(type_parsing, type) + + type_hints = get_type_hints(type_parsing, globals(), locals()) + + self.assertIsInstance(type_hints["emergency_contact"], ForwardRef) + + """ + This is a example of how to resolve ForwardRef in a dynamic model: + ```python + from typing import get_type_hints + + # Make sure your dynamic model has a name + model = type_parsing + model.update_forward_refs(person=model) # 👈 resolve the ForwardRef("person") + + # Inject into globals manually + globalns = globals().copy() + globalns['person'] = model + + # Now you can get the resolved hints + type_hints = get_type_hints(model, globalns=globalns) + ``` + Use `TypeParserOptions.ref_cache` option to cache and resolve ForwardRefs + inside the ObjectTypeParser.to_model method. + """