mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
3b28dc756e
`_type_to_parameter` in `python/composio/utils/openapi.py` assumed a
property's
`type` was a scalar string. JSON Schema Draft 2020-12 and OpenAPI 3.1
can express
a nullable field as a list of types instead, for example
`{"type": ["string", "null"]}`. Passing that list to the scalar
membership check
raised `TypeError: unhashable type: 'list'`, which could break tool
signature
generation in providers such as `google_adk`.
## Changes
- Handle list-valued `type` definitions as unions.
- Re-run every member through `_type_to_parameter` with a copy of the
full
property schema and only `type` replaced. This preserves sibling fields
such
as an array's `items`, so `{"type": ["array", "null"], "items": ...}`
resolves
to `Optional[List[...]]`.
- Preserve scalar error behavior for invalid member types: an unknown
type in a
list raises `InvalidSchemaError` instead of silently becoming `Any`.
- Keep the existing empty-list fallback to `Any`.
- Add regression coverage for nullable arrays and scalar/list
unknown-type
parity alongside the existing list-valued type cases.
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## Verification
From `python/`:
- `uv run pytest tests/test_openapi.py -q` — 18 passed.
- `make chk` — Ruff and mypy passed across the Python SDK, providers,
tests,
examples, and scripts.
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed (not applicable: internal
helper)
- [x] I added regression tests
- [ ] I added a changeset (not applicable: Python-only change)
---------
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: jkomyno <alberto@composio.dev>
Co-authored-by: jkomyno <alberto@composio.dev>
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""Tests for :func:`composio.utils.openapi.function_signature_from_jsonschema`.
|
|
|
|
This builder is what the ``google_adk`` provider uses to attach a
|
|
``__signature__`` to wrapped tools, so a crash here fails tool wrapping. The
|
|
top-level "no type" property already falls back to ``Any``; these tests cover
|
|
the nested combiner options (``oneOf``/``anyOf``/``allOf``) that used to raise.
|
|
"""
|
|
|
|
import typing as t
|
|
|
|
import pytest
|
|
|
|
from composio.exceptions import InvalidSchemaError
|
|
from composio.utils.openapi import function_signature_from_jsonschema
|
|
|
|
|
|
def _annotation(prop: t.Dict[str, t.Any]) -> t.Any:
|
|
params = function_signature_from_jsonschema({"properties": {"x": prop}})
|
|
return params[0].annotation
|
|
|
|
|
|
class TestTypelessCombinerOptions:
|
|
"""A combiner option without a ``type`` key should default to Any, not raise."""
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_typeless_any_of_option_defaults_to_any(self):
|
|
annotation = _annotation(
|
|
{"anyOf": [{"description": "free form"}, {"type": "string"}]}
|
|
)
|
|
assert annotation == t.Union[t.Any, str]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_typeless_one_of_option_defaults_to_any(self):
|
|
annotation = _annotation(
|
|
{"oneOf": [{"type": "string"}, {"description": "free form"}]}
|
|
)
|
|
assert annotation == t.Union[str, t.Any]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_empty_all_of_resolves_to_any(self):
|
|
assert _annotation({"allOf": []}) is t.Any
|
|
|
|
|
|
class TestEmptyCombinerLists:
|
|
"""An empty oneOf/anyOf has no options to union; fall back to Any."""
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_empty_any_of(self):
|
|
assert _annotation({"anyOf": []}) is t.Any
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_empty_one_of(self):
|
|
assert _annotation({"oneOf": []}) is t.Any
|
|
|
|
|
|
class TestExistingShapesUnchanged:
|
|
"""Shapes that already worked must keep resolving identically."""
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_nullable_any_of_is_optional(self):
|
|
assert (
|
|
_annotation({"anyOf": [{"type": "string"}, {"type": "null"}]})
|
|
== t.Optional[str]
|
|
)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_multi_member_one_of(self):
|
|
annotation = _annotation(
|
|
{
|
|
"oneOf": [
|
|
{"type": "string"},
|
|
{"type": "integer"},
|
|
{"type": "boolean"},
|
|
{"type": "number"},
|
|
]
|
|
}
|
|
)
|
|
assert annotation == t.Union[str, int, bool, float]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_enum(self):
|
|
assert (
|
|
_annotation({"type": "string", "enum": ["a", "b"]}) == t.Literal["a", "b"]
|
|
)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_array(self):
|
|
assert (
|
|
_annotation({"type": "array", "items": {"type": "string"}}) == t.List[str]
|
|
)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_object(self):
|
|
assert _annotation({"type": "object"}) == t.Dict[str, t.Any]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_typeless_property_is_any(self):
|
|
assert _annotation({"description": "free form"}) is t.Any
|
|
|
|
|
|
class TestListValuedType:
|
|
"""A list-valued ``type`` (e.g. ``["string", "null"]``) resolves to a Union.
|
|
|
|
JSON Schema Draft 2020-12 / OpenAPI 3.1 express a nullable field as a list of
|
|
types rather than an ``anyOf``. A list is unhashable, so the older
|
|
``p_type in OPENAPI_TO_PYTHON`` membership test raised
|
|
``TypeError: unhashable type: 'list'`` and crashed tool wrapping.
|
|
"""
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_nullable_list_is_optional(self):
|
|
assert _annotation({"type": ["string", "null"]}) == t.Optional[str]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_single_member_list_resolves_to_that_type(self):
|
|
assert _annotation({"type": ["integer"]}) is int
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_multiple_non_null_members_build_a_union(self):
|
|
assert _annotation({"type": ["string", "integer"]}) == t.Union[str, int]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_nullable_array_preserves_items_schema(self):
|
|
assert (
|
|
_annotation({"type": ["array", "null"], "items": {"type": "string"}})
|
|
== t.Optional[t.List[str]]
|
|
)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
@pytest.mark.parametrize("property_type", ["file", ["file", "null"]])
|
|
def test_unrecognized_member_raises_invalid_schema(self, property_type: t.Any):
|
|
with pytest.raises(InvalidSchemaError, match="Invalid property type file"):
|
|
_annotation({"type": property_type})
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
def test_empty_list_resolves_to_any(self):
|
|
assert _annotation({"type": []}) is t.Any
|