clan-cli: Add support for ForwardRef type in type_to_jsonschema and tests

This commit is contained in:
Qubasa
2025-10-22 14:38:13 +02:00
parent 9851993b82
commit c7ec9a9715
2 changed files with 101 additions and 1 deletions

View File

@@ -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)

View File

@@ -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)