"""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)