feat: support optional params jsonschema conversion in exchange (#188)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import uuid
|
import uuid
|
||||||
from importlib.metadata import entry_points
|
from importlib.metadata import entry_points
|
||||||
from typing import get_args, get_origin
|
from typing import Literal, get_args, get_origin, Any, Union
|
||||||
|
|
||||||
from griffe import (
|
from griffe import (
|
||||||
Docstring,
|
Docstring,
|
||||||
@@ -107,16 +107,27 @@ def json_schema(func: any) -> dict[str, any]: # noqa: ANN401
|
|||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def _map_type_to_schema(py_type: type) -> dict[str, any]: # noqa: ANN401
|
def _map_type_to_schema(py_type: type) -> dict[str, Any]:
|
||||||
origin = get_origin(py_type)
|
origin = get_origin(py_type)
|
||||||
args = get_args(py_type)
|
args = get_args(py_type)
|
||||||
|
|
||||||
if origin is list or origin is tuple:
|
if origin is Union:
|
||||||
return {"type": "array", "items": _map_type_to_schema(args[0] if args else any)}
|
# Handle Optional[X], which is Union[X, NoneType]
|
||||||
|
non_none_args = [arg for arg in args if arg is not type(None)]
|
||||||
|
if len(non_none_args) == 1:
|
||||||
|
# This is Optional[X]
|
||||||
|
return _map_type_to_schema(non_none_args[0])
|
||||||
|
else:
|
||||||
|
# General Union
|
||||||
|
return {"anyOf": [_map_type_to_schema(arg) for arg in non_none_args]}
|
||||||
|
elif origin is Literal:
|
||||||
|
return {"enum": list(args)}
|
||||||
|
elif origin in (list, tuple):
|
||||||
|
return {"type": "array", "items": _map_type_to_schema(args[0] if args else Any)}
|
||||||
elif origin is dict:
|
elif origin is dict:
|
||||||
return {
|
return {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": _map_type_to_schema(args[1] if len(args) > 1 else any),
|
"additionalProperties": _map_type_to_schema(args[1] if len(args) > 1 else Any),
|
||||||
}
|
}
|
||||||
elif py_type is int:
|
elif py_type is int:
|
||||||
return {"type": "integer"}
|
return {"type": "integer"}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Literal
|
||||||
import pytest
|
import pytest
|
||||||
from exchange import utils
|
from exchange import utils
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -65,6 +66,34 @@ def test_parse_docstring_no_description() -> None:
|
|||||||
assert "Attempted to load from a function" in str(e.value)
|
assert "Attempted to load from a function" in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_docstring_with_optional_params() -> None:
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
def dummy_func(a: int, b: List[int], c: Literal["foo", "bar"] = "foo", d: Optional[str] = None) -> None:
|
||||||
|
"""This function does something.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a (int): The first required parameter.
|
||||||
|
b (List[int]): The second parameter.
|
||||||
|
c (Literal["foo", "bar"], optional): A parameter with a literal default value. Defaults to "foo".
|
||||||
|
d (Optional[str], optional): Optional fourth parameter. Defaults to None.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
description, parameters = utils.parse_docstring(dummy_func)
|
||||||
|
assert description == "This function does something."
|
||||||
|
assert parameters == [
|
||||||
|
{"name": "a", "annotation": "int", "description": "The first required parameter."},
|
||||||
|
{"name": "b", "annotation": "List[int]", "description": "The second parameter."},
|
||||||
|
{
|
||||||
|
"name": "c",
|
||||||
|
"annotation": 'Literal["foo", "bar"]',
|
||||||
|
"description": 'A parameter with a literal default value. Defaults to "foo".',
|
||||||
|
},
|
||||||
|
{"name": "d", "annotation": "Optional[str]", "description": "Optional fourth parameter. Defaults to None."},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_json_schema() -> None:
|
def test_json_schema() -> None:
|
||||||
def dummy_func(a: int, b: str, c: list) -> None:
|
def dummy_func(a: int, b: str, c: list) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -82,6 +111,31 @@ def test_json_schema() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_schema_with_optional_params() -> None:
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
def dummy_func(
|
||||||
|
a: int,
|
||||||
|
b: Literal["foo", "bar"] = "foo",
|
||||||
|
c: Optional[List[int]] = None,
|
||||||
|
d: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
schema = utils.json_schema(dummy_func)
|
||||||
|
|
||||||
|
assert schema == {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"a": {"type": "integer"},
|
||||||
|
"b": {"enum": ["foo", "bar"], "default": "foo"},
|
||||||
|
"c": {"type": "array", "items": {"type": "integer"}, "default": None},
|
||||||
|
"d": {"type": "string", "default": None},
|
||||||
|
},
|
||||||
|
"required": ["a"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_load_plugins() -> None:
|
def test_load_plugins() -> None:
|
||||||
class DummyEntryPoint:
|
class DummyEntryPoint:
|
||||||
def __init__(self, name, plugin):
|
def __init__(self, name, plugin):
|
||||||
|
|||||||
Reference in New Issue
Block a user