clan-cli: Add support for ForwardRef type in type_to_jsonschema and tests
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import copy
|
||||
import dataclasses
|
||||
import importlib
|
||||
import inspect
|
||||
import pathlib
|
||||
from dataclasses import MISSING
|
||||
@@ -9,6 +10,7 @@ from types import NoneType, UnionType
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
ForwardRef,
|
||||
Literal,
|
||||
NewType,
|
||||
NotRequired,
|
||||
@@ -118,6 +120,18 @@ def type_to_dict(
|
||||
if t is None:
|
||||
return {"type": "null"}
|
||||
|
||||
# Handle string type annotations (forward references represented as strings)
|
||||
if isinstance(t, str):
|
||||
# Special case for JSONValue which represents arbitrary JSON values
|
||||
# This is a recursive type that can't be fully represented in JSON Schema without $ref
|
||||
if t == "JSONValue":
|
||||
# Return a permissive schema that allows any JSON-compatible value
|
||||
# This matches the intent of JSONValue = str | int | float | bool | None | list[JSONValue] | dict[str, JSONValue]
|
||||
return {} # Empty schema allows any type
|
||||
|
||||
msg = f"{scope} - String type annotation '{t}' cannot be resolved. This may indicate a forward reference or recursive type."
|
||||
raise JSchemaTypeError(msg)
|
||||
|
||||
if inspect.isclass(t) and t.__name__ == "Unknown":
|
||||
# Empty should represent unknown
|
||||
# We don't know anything about this type
|
||||
@@ -335,5 +349,31 @@ def type_to_dict(
|
||||
|
||||
msg = f"{scope} - Basic type '{t!s}' is not supported"
|
||||
raise JSchemaTypeError(msg)
|
||||
|
||||
# Handle ForwardRef types by resolving them to their actual types
|
||||
if isinstance(t, ForwardRef):
|
||||
# Get the module name and type name from the forward ref
|
||||
module_name = getattr(t, "__forward_module__", None)
|
||||
type_name = getattr(t, "__forward_arg__", None)
|
||||
|
||||
if module_name and type_name:
|
||||
try:
|
||||
# Import the module to get its namespace
|
||||
module = importlib.import_module(module_name)
|
||||
namespace = {**vars(module), "__builtins__": __builtins__}
|
||||
|
||||
# Evaluate the type string in the namespace (handles both module types and builtins)
|
||||
resolved_type = eval(type_name, namespace) # noqa: S307
|
||||
return type_to_dict(
|
||||
resolved_type, scope, type_map, narrow_unsupported_union_types
|
||||
)
|
||||
|
||||
except (ImportError, NameError, AttributeError, SyntaxError) as e:
|
||||
msg = f"{scope} - Could not resolve ForwardRef('{type_name}', module='{module_name}'): {e}"
|
||||
raise JSchemaTypeError(msg) from e
|
||||
|
||||
msg = f"{scope} - ForwardRef without module or type name: {t}"
|
||||
raise JSchemaTypeError(msg)
|
||||
|
||||
msg = f"{scope} - Type '{t!s}' is not supported"
|
||||
raise JSchemaTypeError(msg)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Generic, NotRequired, Required, TypedDict, TypeVar
|
||||
from typing import Any, ForwardRef, Generic, NotRequired, Required, TypedDict, TypeVar
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -379,3 +379,63 @@ def test_type_alias() -> None:
|
||||
"additionalProperties": False,
|
||||
"required": ["input_name", "readmes"],
|
||||
}
|
||||
|
||||
|
||||
def test_string_type_annotation_jsonvalue() -> None:
|
||||
# Test that "JSONValue" string type annotation returns permissive schema
|
||||
result = type_to_dict("JSONValue")
|
||||
assert result == {}, "JSONValue should return empty schema allowing any type"
|
||||
|
||||
|
||||
def test_string_type_annotation_error() -> None:
|
||||
# Test that other string type annotations raise an error
|
||||
with pytest.raises(JSchemaTypeError) as exc_info:
|
||||
type_to_dict("SomeUnknownType")
|
||||
assert "String type annotation 'SomeUnknownType' cannot be resolved" in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
|
||||
def test_forwardref_resolution() -> None:
|
||||
# Create a ForwardRef that references a built-in type
|
||||
forward_ref = ForwardRef("int")
|
||||
forward_ref.__forward_module__ = "builtins"
|
||||
|
||||
result = type_to_dict(forward_ref)
|
||||
assert result == {"type": "integer"}
|
||||
|
||||
# Test ForwardRef to str
|
||||
forward_ref_str = ForwardRef("str")
|
||||
forward_ref_str.__forward_module__ = "builtins"
|
||||
|
||||
result_str = type_to_dict(forward_ref_str)
|
||||
assert result_str == {"type": "string"}
|
||||
|
||||
|
||||
def test_forwardref_resolution_from_module() -> None:
|
||||
# Create a ForwardRef that references a complex type from typing module
|
||||
forward_ref = ForwardRef("list[str]")
|
||||
forward_ref.__forward_module__ = "builtins"
|
||||
|
||||
# This test verifies that we can resolve complex type expressions
|
||||
result = type_to_dict(forward_ref)
|
||||
assert result == {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
|
||||
def test_forwardref_without_module() -> None:
|
||||
# Test that ForwardRef without module info raises an error
|
||||
forward_ref = ForwardRef("SomeType")
|
||||
|
||||
with pytest.raises(JSchemaTypeError) as exc_info:
|
||||
type_to_dict(forward_ref)
|
||||
assert "ForwardRef without module or type name" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_forwardref_invalid_type() -> None:
|
||||
# Test that ForwardRef with invalid type name raises an error
|
||||
forward_ref = ForwardRef("NonExistentType")
|
||||
forward_ref.__forward_module__ = "builtins"
|
||||
|
||||
with pytest.raises(JSchemaTypeError) as exc_info:
|
||||
type_to_dict(forward_ref)
|
||||
assert "Could not resolve ForwardRef" in str(exc_info.value)
|
||||
|
||||
Reference in New Issue
Block a user