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 copy
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import pathlib
|
import pathlib
|
||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
@@ -9,6 +10,7 @@ from types import NoneType, UnionType
|
|||||||
from typing import (
|
from typing import (
|
||||||
Annotated,
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
|
ForwardRef,
|
||||||
Literal,
|
Literal,
|
||||||
NewType,
|
NewType,
|
||||||
NotRequired,
|
NotRequired,
|
||||||
@@ -118,6 +120,18 @@ def type_to_dict(
|
|||||||
if t is None:
|
if t is None:
|
||||||
return {"type": "null"}
|
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":
|
if inspect.isclass(t) and t.__name__ == "Unknown":
|
||||||
# Empty should represent unknown
|
# Empty should represent unknown
|
||||||
# We don't know anything about this type
|
# 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"
|
msg = f"{scope} - Basic type '{t!s}' is not supported"
|
||||||
raise JSchemaTypeError(msg)
|
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"
|
msg = f"{scope} - Type '{t!s}' is not supported"
|
||||||
raise JSchemaTypeError(msg)
|
raise JSchemaTypeError(msg)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
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
|
import pytest
|
||||||
|
|
||||||
@@ -379,3 +379,63 @@ def test_type_alias() -> None:
|
|||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
"required": ["input_name", "readmes"],
|
"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