Files

155 lines
4.9 KiB
Python
Raw Permalink Normal View History

fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710) ### Summary `function_signature_from_jsonschema` in `python/composio/utils/openapi.py` builds the `__signature__` for tools wrapped by the `google_adk` provider (`providers/google_adk/composio_google_adk/provider.py:66`). It already falls back to `Any` for a top-level property with no `type`, but the nested combiner helpers do not, so real combiner shapes crash at wrap time: - a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'` - an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no types` - an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`) This extends the existing "default to Any" intent to nested options. Follow-up to #3708, which fixed the same bug family in `shared.py` and noted this `openapi.py` case for a separate PR. Fixes # ### Changes - `_type_to_parameter`: read `schema.get("type")` instead of `schema["type"]`; when the type is missing, return `t.Any` (mirrors the top-level fallback). An unknown *string* type still raises `InvalidSchemaError`. - `_handle_composite_type`: return `t.Any` for an empty option list instead of building an empty `Union`. Empty `allOf` merges to `{}` and resolves to `Any` via the above. - New `python/tests/test_openapi.py` (11 tests): this builder had none. Asserts the five former crashes now resolve to `Any`, and that existing shapes are unchanged. ### Type of change - [x] Bug fix - [ ] New feature - [ ] Refactor/Chore - [ ] Documentation - [ ] Breaking change ### How Has This Been Tested? From `python/`: ``` uv run pytest tests/test_openapi.py -q # 11 passed make chk # ruff + mypy -> clean ``` To confirm the tests guard the fix, the five former crashes (typeless `anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the unmodified file and now resolve to `Any`; regressions cover `anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array, object, and a typeless property. ### 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 (internal util) - [x] I added tests (new test_openapi.py) - [ ] I added a changeset (Python-only, not needed) Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 13:10:35 +05:30
"""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
fix(py): handle list-valued JSON Schema type in openapi signatures (#3822) `_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>
2026-07-16 01:04:26 +05:30
from composio.exceptions import InvalidSchemaError
fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710) ### Summary `function_signature_from_jsonschema` in `python/composio/utils/openapi.py` builds the `__signature__` for tools wrapped by the `google_adk` provider (`providers/google_adk/composio_google_adk/provider.py:66`). It already falls back to `Any` for a top-level property with no `type`, but the nested combiner helpers do not, so real combiner shapes crash at wrap time: - a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'` - an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no types` - an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`) This extends the existing "default to Any" intent to nested options. Follow-up to #3708, which fixed the same bug family in `shared.py` and noted this `openapi.py` case for a separate PR. Fixes # ### Changes - `_type_to_parameter`: read `schema.get("type")` instead of `schema["type"]`; when the type is missing, return `t.Any` (mirrors the top-level fallback). An unknown *string* type still raises `InvalidSchemaError`. - `_handle_composite_type`: return `t.Any` for an empty option list instead of building an empty `Union`. Empty `allOf` merges to `{}` and resolves to `Any` via the above. - New `python/tests/test_openapi.py` (11 tests): this builder had none. Asserts the five former crashes now resolve to `Any`, and that existing shapes are unchanged. ### Type of change - [x] Bug fix - [ ] New feature - [ ] Refactor/Chore - [ ] Documentation - [ ] Breaking change ### How Has This Been Tested? From `python/`: ``` uv run pytest tests/test_openapi.py -q # 11 passed make chk # ruff + mypy -> clean ``` To confirm the tests guard the fix, the five former crashes (typeless `anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the unmodified file and now resolve to `Any`; regressions cover `anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array, object, and a typeless property. ### 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 (internal util) - [x] I added tests (new test_openapi.py) - [ ] I added a changeset (Python-only, not needed) Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 13:10:35 +05:30
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
fix(py): handle list-valued JSON Schema type in openapi signatures (#3822) `_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>
2026-07-16 01:04:26 +05:30
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