Introduce ResourceResult as canonical resource return type (#2734)

This commit is contained in:
Jeremiah Lowin 2025-12-25 21:21:55 -05:00 committed by GitHub
commit 648684d2bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1190 additions and 555 deletions

View file

@ -1,7 +1,7 @@
import pytest
from pydantic import AnyUrl, BaseModel
from fastmcp.resources.resource import FunctionResource, ResourceContent
from fastmcp.resources.resource import FunctionResource, ResourceContent, ResourceResult
class TestFunctionResource:
@ -36,10 +36,16 @@ class TestFunctionResource:
name="test",
fn=get_data,
)
# read() returns raw value
result = await resource.read()
assert isinstance(result, ResourceContent)
assert result.content == "Hello, world!"
assert result.mime_type == "text/plain"
assert result == "Hello, world!"
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "Hello, world!"
assert result.contents[0].mime_type == "text/plain"
async def test_read_binary(self):
"""Test reading binary data from a FunctionResource."""
@ -52,12 +58,17 @@ class TestFunctionResource:
name="test",
fn=get_data,
)
# read() returns raw value
result = await resource.read()
assert isinstance(result, ResourceContent)
assert result.content == b"Hello, world!"
assert result == b"Hello, world!"
async def test_json_conversion(self):
"""Test automatic JSON conversion of non-string results."""
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert result.contents[0].content == b"Hello, world!"
async def test_dict_return_raises_type_error(self):
"""Returning dict from read() raises TypeError - use ResourceResult."""
def get_data() -> dict:
return {"key": "value"}
@ -67,10 +78,13 @@ class TestFunctionResource:
name="test",
fn=get_data,
)
# read() returns raw value (no type checking at runtime)
result = await resource.read()
assert isinstance(result, ResourceContent)
assert isinstance(result.content, str)
assert '"key":"value"' in result.content
assert result == {"key": "value"}
# _read() raises TypeError - must return str, bytes, or ResourceResult
with pytest.raises(TypeError, match="must be str, bytes, or list"):
await resource._read()
async def test_error_handling(self):
"""Test error handling in FunctionResource."""
@ -86,8 +100,8 @@ class TestFunctionResource:
with pytest.raises(ValueError, match="Test error"):
await resource.read()
async def test_basemodel_conversion(self):
"""Test handling of BaseModel types."""
async def test_basemodel_return_raises_type_error(self):
"""Returning BaseModel from read() raises TypeError - use ResourceResult."""
class MyModel(BaseModel):
name: str
@ -97,12 +111,16 @@ class TestFunctionResource:
name="test",
fn=lambda: MyModel(name="test"),
)
result = await resource.read()
assert isinstance(result, ResourceContent)
assert result.content == '{"name":"test"}'
# read() returns raw value (no type checking at runtime)
raw_result = await resource.read()
assert isinstance(raw_result, MyModel)
async def test_custom_type_conversion(self):
"""Test handling of custom types."""
# _read() raises TypeError - must return str, bytes, or ResourceResult
with pytest.raises(TypeError, match="must be str, bytes, or list"):
await resource._read()
async def test_custom_type_return_raises_type_error(self):
"""Returning custom type from read() raises TypeError - use ResourceResult."""
class CustomData:
def __str__(self) -> str:
@ -116,9 +134,13 @@ class TestFunctionResource:
name="test",
fn=get_data,
)
result = await resource.read()
assert isinstance(result, ResourceContent)
assert isinstance(result.content, str)
# read() returns raw value (no type checking at runtime)
raw_result = await resource.read()
assert isinstance(raw_result, CustomData)
# _read() raises TypeError - must return str, bytes, or ResourceResult
with pytest.raises(TypeError, match="must be str, bytes, or list"):
await resource._read()
async def test_async_read_text(self):
"""Test reading text from async FunctionResource."""
@ -131,10 +153,15 @@ class TestFunctionResource:
name="test",
fn=get_data,
)
# read() returns raw value
result = await resource.read()
assert isinstance(result, ResourceContent)
assert result.content == "Hello, world!"
assert result.mime_type == "text/plain"
assert result == "Hello, world!"
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Hello, world!"
assert result.contents[0].mime_type == "text/plain"
async def test_resource_content_text(self):
"""Test returning ResourceContent with text content."""
@ -178,21 +205,11 @@ class TestFunctionResource:
assert result.meta is None
async def test_resource_content_without_meta(self):
"""Test returning ResourceContent without meta."""
def get_data() -> ResourceContent:
return ResourceContent(content="plain text")
resource = FunctionResource(
uri=AnyUrl("function://test"),
name="test",
fn=get_data,
)
result = await resource.read()
assert isinstance(result, ResourceContent)
assert result.content == "plain text"
assert result.mime_type is None
assert result.meta is None
"""Test that ResourceContent auto-sets mime_type defaults."""
content = ResourceContent(content="plain text")
assert content.content == "plain text"
assert content.mime_type == "text/plain" # Auto-set for string content
assert content.meta is None
async def test_async_resource_content(self):
"""Test async function returning ResourceContent."""