tests: add lowest dependency tests (#1067)
Co-authored-by: ihrpr <inna@anthropic.com>
This commit is contained in:
parent
adf327164a
commit
3fa7bc7b61
5 changed files with 473 additions and 499 deletions
9
.github/workflows/shared.yml
vendored
9
.github/workflows/shared.yml
vendored
|
|
@ -29,6 +29,7 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
continue-on-error: true
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
@ -48,8 +49,14 @@ jobs:
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
run: uv run --frozen --no-sync pytest
|
run: uv run --frozen --no-sync pytest
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
|
# This must run last as it modifies the environment!
|
||||||
|
- name: Run pytest with lowest versions
|
||||||
|
run: |
|
||||||
|
uv sync --all-extras --upgrade
|
||||||
|
uv run --no-sync pytest
|
||||||
|
env:
|
||||||
|
UV_RESOLUTION: lowest-direct
|
||||||
readme-snippets:
|
readme-snippets:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ dependencies = [
|
||||||
"anyio>=4.5",
|
"anyio>=4.5",
|
||||||
"httpx>=0.27",
|
"httpx>=0.27",
|
||||||
"httpx-sse>=0.4",
|
"httpx-sse>=0.4",
|
||||||
"pydantic>=2.7.2,<3.0.0",
|
"pydantic>=2.8.0,<3.0.0",
|
||||||
"starlette>=0.27",
|
"starlette>=0.27",
|
||||||
"python-multipart>=0.0.9",
|
"python-multipart>=0.0.9",
|
||||||
"sse-starlette>=1.6.1",
|
"sse-starlette>=1.6.1",
|
||||||
|
|
@ -36,14 +36,13 @@ dependencies = [
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
rich = ["rich>=13.9.4"]
|
rich = ["rich>=13.9.4"]
|
||||||
cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"]
|
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
|
||||||
ws = ["websockets>=15.0.1"]
|
ws = ["websockets>=15.0.1"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp = "mcp.cli:app [cli]"
|
mcp = "mcp.cli:app [cli]"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
resolution = "lowest-direct"
|
|
||||||
default-groups = ["dev", "docs"]
|
default-groups = ["dev", "docs"]
|
||||||
required-version = ">=0.7.2"
|
required-version = ">=0.7.2"
|
||||||
|
|
||||||
|
|
@ -58,6 +57,7 @@ dev = [
|
||||||
"pytest-examples>=0.0.14",
|
"pytest-examples>=0.0.14",
|
||||||
"pytest-pretty>=1.2.0",
|
"pytest-pretty>=1.2.0",
|
||||||
"inline-snapshot>=0.23.0",
|
"inline-snapshot>=0.23.0",
|
||||||
|
"dirty-equals>=0.9.0",
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs>=1.6.1",
|
"mkdocs>=1.6.1",
|
||||||
|
|
@ -124,5 +124,5 @@ filterwarnings = [
|
||||||
# This should be fixed on Uvicorn's side.
|
# This should be fixed on Uvicorn's side.
|
||||||
"ignore::DeprecationWarning:websockets",
|
"ignore::DeprecationWarning:websockets",
|
||||||
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
|
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
|
||||||
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel"
|
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,46 @@
|
||||||
import anyio
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from mcp.shared.memory import (
|
from mcp.shared.memory import create_connected_server_and_client_session as create_session
|
||||||
create_connected_server_and_client_session as create_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
_sleep_time_seconds = 0.01
|
|
||||||
_resource_name = "slow://slow_resource"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_messages_are_executed_concurrently():
|
async def test_messages_are_executed_concurrently():
|
||||||
server = FastMCP("test")
|
server = FastMCP("test")
|
||||||
call_timestamps = []
|
event = anyio.Event()
|
||||||
|
tool_started = anyio.Event()
|
||||||
|
call_order = []
|
||||||
|
|
||||||
@server.tool("sleep")
|
@server.tool("sleep")
|
||||||
async def sleep_tool():
|
async def sleep_tool():
|
||||||
call_timestamps.append(("tool_start_time", anyio.current_time()))
|
call_order.append("waiting_for_event")
|
||||||
await anyio.sleep(_sleep_time_seconds)
|
tool_started.set()
|
||||||
call_timestamps.append(("tool_end_time", anyio.current_time()))
|
await event.wait()
|
||||||
|
call_order.append("tool_end")
|
||||||
return "done"
|
return "done"
|
||||||
|
|
||||||
@server.resource(_resource_name)
|
@server.tool("trigger")
|
||||||
async def slow_resource():
|
async def trigger():
|
||||||
call_timestamps.append(("resource_start_time", anyio.current_time()))
|
# Wait for tool to start before setting the event
|
||||||
await anyio.sleep(_sleep_time_seconds)
|
await tool_started.wait()
|
||||||
call_timestamps.append(("resource_end_time", anyio.current_time()))
|
call_order.append("trigger_started")
|
||||||
|
event.set()
|
||||||
|
call_order.append("trigger_end")
|
||||||
return "slow"
|
return "slow"
|
||||||
|
|
||||||
async with create_session(server._mcp_server) as client_session:
|
async with create_session(server._mcp_server) as client_session:
|
||||||
|
# First tool will wait on event, second will set it
|
||||||
async with anyio.create_task_group() as tg:
|
async with anyio.create_task_group() as tg:
|
||||||
for _ in range(10):
|
# Start the tool first (it will wait on event)
|
||||||
tg.start_soon(client_session.call_tool, "sleep")
|
tg.start_soon(client_session.call_tool, "sleep")
|
||||||
tg.start_soon(client_session.read_resource, AnyUrl(_resource_name))
|
# Then the trigger tool will set the event to allow the first tool to continue
|
||||||
|
await client_session.call_tool("trigger")
|
||||||
|
|
||||||
active_calls = 0
|
# Verify that both ran concurrently
|
||||||
max_concurrent_calls = 0
|
assert call_order == [
|
||||||
for call_type, _ in sorted(call_timestamps, key=lambda x: x[1]):
|
"waiting_for_event",
|
||||||
if "start" in call_type:
|
"trigger_started",
|
||||||
active_calls += 1
|
"trigger_end",
|
||||||
max_concurrent_calls = max(max_concurrent_calls, active_calls)
|
"tool_end",
|
||||||
else:
|
], f"Expected concurrent execution, but got: {call_order}"
|
||||||
active_calls -= 1
|
|
||||||
print(f"Max concurrent calls: {max_concurrent_calls}")
|
|
||||||
assert max_concurrent_calls > 1, "No concurrent calls were executed"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
anyio.run(test_messages_are_executed_concurrently)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Annotated, Any, TypedDict
|
||||||
|
|
||||||
import annotated_types
|
import annotated_types
|
||||||
import pytest
|
import pytest
|
||||||
|
from dirty_equals import IsPartialDict
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from mcp.server.fastmcp.utilities.func_metadata import func_metadata
|
from mcp.server.fastmcp.utilities.func_metadata import func_metadata
|
||||||
|
|
@ -202,11 +203,8 @@ def test_structured_output_dict_str_types():
|
||||||
return {"a": 1, "b": "hello", "c": [1, 2, 3]}
|
return {"a": 1, "b": "hello", "c": [1, 2, 3]}
|
||||||
|
|
||||||
meta = func_metadata(func_dict_any)
|
meta = func_metadata(func_dict_any)
|
||||||
assert meta.output_schema == {
|
|
||||||
"additionalProperties": True,
|
assert meta.output_schema == IsPartialDict(type="object", title="func_dict_anyDictOutput")
|
||||||
"type": "object",
|
|
||||||
"title": "func_dict_anyDictOutput",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test dict[str, str]
|
# Test dict[str, str]
|
||||||
def func_dict_str() -> dict[str, str]:
|
def func_dict_str() -> dict[str, str]:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue