60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
142 lines
4.4 KiB
Python
142 lines
4.4 KiB
Python
"""Tests for fastmcp.utilities.async_utils."""
|
|
|
|
import functools
|
|
|
|
import pytest
|
|
from exceptiongroup import BaseExceptionGroup
|
|
|
|
from fastmcp import Client, FastMCP
|
|
from fastmcp.prompts import prompt
|
|
from fastmcp.resources import resource
|
|
from fastmcp.tools import tool
|
|
from fastmcp.utilities.async_utils import gather, is_coroutine_function
|
|
|
|
|
|
async def _async_fn(x: int) -> int:
|
|
return x
|
|
|
|
|
|
def _sync_fn(x: int) -> int:
|
|
return x
|
|
|
|
|
|
class TestIsCoroutineFunction:
|
|
def test_plain_async(self) -> None:
|
|
assert is_coroutine_function(_async_fn) is True
|
|
|
|
def test_plain_sync(self) -> None:
|
|
assert is_coroutine_function(_sync_fn) is False
|
|
|
|
def test_partial_async(self) -> None:
|
|
p = functools.partial(_async_fn, x=1)
|
|
assert is_coroutine_function(p) is True
|
|
|
|
def test_partial_sync(self) -> None:
|
|
p = functools.partial(_sync_fn, x=1)
|
|
assert is_coroutine_function(p) is False
|
|
|
|
def test_nested_partial_async(self) -> None:
|
|
p = functools.partial(functools.partial(_async_fn, x=1))
|
|
assert is_coroutine_function(p) is True
|
|
|
|
def test_nested_partial_sync(self) -> None:
|
|
p = functools.partial(functools.partial(_sync_fn, x=1))
|
|
assert is_coroutine_function(p) is False
|
|
|
|
def test_lambda(self) -> None:
|
|
assert is_coroutine_function(lambda: None) is False
|
|
|
|
def test_non_callable(self) -> None:
|
|
assert is_coroutine_function(42) is False
|
|
|
|
|
|
class TestGather:
|
|
async def test_returns_results_in_input_order(self) -> None:
|
|
async def value(result: int) -> int:
|
|
return result
|
|
|
|
assert await gather(value(1), value(2), value(3)) == [1, 2, 3]
|
|
|
|
async def test_raises_by_default(self) -> None:
|
|
async def fail() -> int:
|
|
raise RuntimeError("boom")
|
|
|
|
with pytest.raises(BaseExceptionGroup) as exc_info:
|
|
await gather(fail())
|
|
|
|
assert len(exc_info.value.exceptions) == 1
|
|
assert isinstance(exc_info.value.exceptions[0], RuntimeError)
|
|
|
|
async def test_return_exceptions_collects_exceptions(self) -> None:
|
|
async def fail() -> int:
|
|
raise ValueError("bad")
|
|
|
|
async def value() -> int:
|
|
return 1
|
|
|
|
result = await gather(fail(), value(), return_exceptions=True)
|
|
|
|
assert isinstance(result[0], ValueError)
|
|
assert result[1] == 1
|
|
|
|
|
|
class TestAsyncPartialIntegration:
|
|
async def test_async_partial_tool_runs(self) -> None:
|
|
async def greet(greeting: str, name: str) -> str:
|
|
return f"{greeting}, {name}!"
|
|
|
|
greet_tool = tool(name="greet")(functools.partial(greet, "Hello"))
|
|
|
|
mcp = FastMCP()
|
|
mcp.add_tool(greet_tool)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.call_tool("greet", {"name": "world"})
|
|
assert result.content[0].text == "Hello, world!"
|
|
|
|
async def test_async_partial_resource_reads(self) -> None:
|
|
async def make_greeting(greeting: str) -> str:
|
|
return f"{greeting}, resource!"
|
|
|
|
greet_resource = resource("test://greet")(
|
|
functools.partial(make_greeting, "Hi")
|
|
)
|
|
|
|
mcp = FastMCP()
|
|
mcp.add_resource(greet_resource)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource("test://greet")
|
|
assert result[0].text == "Hi, resource!"
|
|
|
|
async def test_async_partial_prompt_renders(self) -> None:
|
|
async def make_prompt(prefix: str) -> str:
|
|
return f"{prefix}: prompt content"
|
|
|
|
note_prompt = prompt(name="note")(functools.partial(make_prompt, "Note"))
|
|
|
|
mcp = FastMCP()
|
|
mcp.add_prompt(note_prompt)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.get_prompt("note")
|
|
assert "Note: prompt content" in result.messages[0].content.text
|
|
|
|
async def test_async_partial_with_task_true_does_not_raise(self) -> None:
|
|
async def slow_task(prefix: str, x: int) -> str:
|
|
return f"{prefix}-{x}"
|
|
|
|
slow_tool = tool(name="slow", task=True)(functools.partial(slow_task, "ok"))
|
|
|
|
mcp = FastMCP()
|
|
mcp.add_tool(slow_tool)
|
|
|
|
async def test_sync_partial_with_task_true_raises(self) -> None:
|
|
def sync_task(prefix: str, x: int) -> str:
|
|
return f"{prefix}-{x}"
|
|
|
|
mcp = FastMCP()
|
|
with pytest.raises(ValueError, match="sync function"):
|
|
decorated = tool(name="slow", task=True)(functools.partial(sync_task, "ok"))
|
|
mcp.add_tool(decorated)
|