mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 12:34:17 +02:00
Fix: Include signature modification in create_function_without_params (#2563)
* Fix: Include signature modification in create_function_without_params When excluding parameters via create_function_without_params(), only __annotations__ was being updated but not __signature__. This caused Pydantic's _arguments_schema() to fail when it iterated over signature parameters that didn't exist in the type hints dictionary. The fix adds proper signature reconstruction matching the pattern used in without_injected_parameters(). Fixes KeyError: 'ctx' when using @mcp.tool() with Context parameters. * fix: add regression tests for create_function_without_params The test_pydantic_typeadapter_compatibility test specifically reproduces the issue from #2562 and verifies the fix. * fix: linter for test function --------- Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
parent
8d3deeecc9
commit
b9cde3bdb6
2 changed files with 90 additions and 0 deletions
|
|
@ -206,6 +206,13 @@ def create_function_without_params(
|
|||
k: v for k, v in original_annotations.items() if k not in exclude_params
|
||||
}
|
||||
|
||||
# Create new signature without the excluded parameters
|
||||
sig = inspect.signature(fn)
|
||||
new_params = [
|
||||
param for name, param in sig.parameters.items() if name not in exclude_params
|
||||
]
|
||||
new_sig = inspect.Signature(new_params, return_annotation=sig.return_annotation)
|
||||
|
||||
new_func = types.FunctionType(
|
||||
code,
|
||||
globals_dict,
|
||||
|
|
@ -217,6 +224,7 @@ def create_function_without_params(
|
|||
new_func.__module__ = fn.__module__
|
||||
new_func.__qualname__ = getattr(fn, "__qualname__", fn.__name__) # ty: ignore[unresolved-attribute]
|
||||
new_func.__annotations__ = new_annotations
|
||||
new_func.__signature__ = new_sig # type: ignore[attr-defined]
|
||||
|
||||
if inspect.ismethod(fn):
|
||||
return types.MethodType(new_func, fn.__self__)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from fastmcp.utilities.types import (
|
|||
Audio,
|
||||
File,
|
||||
Image,
|
||||
create_function_without_params,
|
||||
get_cached_typeadapter,
|
||||
is_class_member_of_type,
|
||||
issubclass_safe,
|
||||
|
|
@ -518,6 +519,87 @@ class TestReplaceType:
|
|||
assert replace_type(input, type_map) == expected
|
||||
|
||||
|
||||
class TestCreateFunctionWithoutParams:
|
||||
"""Test create_function_without_params properly removes parameters from both annotations and signature."""
|
||||
|
||||
def test_removes_params_from_both_annotations_and_signature(self):
|
||||
"""Test that excluded parameters are removed from __annotations__ AND __signature__."""
|
||||
import inspect
|
||||
|
||||
def original_func(ctx: str, query: str, limit: int = 10) -> list[str]:
|
||||
return []
|
||||
|
||||
new_func = create_function_without_params(original_func, ["ctx"])
|
||||
|
||||
# Verify removal from annotations
|
||||
assert "ctx" not in new_func.__annotations__
|
||||
assert "query" in new_func.__annotations__
|
||||
assert "limit" in new_func.__annotations__
|
||||
|
||||
# Verify removal from signature (regression test for #2562)
|
||||
sig = inspect.signature(new_func)
|
||||
assert "ctx" not in sig.parameters
|
||||
assert "query" in sig.parameters
|
||||
assert "limit" in sig.parameters
|
||||
|
||||
def test_preserves_return_annotation_in_signature(self):
|
||||
"""Test that return annotation is preserved in both annotations and signature."""
|
||||
import inspect
|
||||
|
||||
def original_func(ctx: str, value: int) -> dict[str, int]:
|
||||
return {}
|
||||
|
||||
new_func = create_function_without_params(original_func, ["ctx"])
|
||||
|
||||
sig = inspect.signature(new_func)
|
||||
assert sig.return_annotation == dict[str, int]
|
||||
assert new_func.__annotations__["return"] == dict[str, int]
|
||||
|
||||
def test_pydantic_typeadapter_compatibility(self):
|
||||
"""Test that modified function works with Pydantic TypeAdapter (regression test for #2562)."""
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Result(BaseModel):
|
||||
name: str
|
||||
|
||||
def tool_function(ctx: str, search_query: str, limit: int = 10) -> list[Result]:
|
||||
return []
|
||||
|
||||
# Remove context parameter (what FastMCP does internally)
|
||||
new_func = create_function_without_params(tool_function, ["ctx"])
|
||||
|
||||
# This raised KeyError: 'ctx' before the fix
|
||||
adapter = get_cached_typeadapter(new_func)
|
||||
schema = adapter.json_schema()
|
||||
|
||||
# Verify schema excludes the removed parameter
|
||||
assert "properties" in schema
|
||||
assert "ctx" not in schema["properties"]
|
||||
assert "search_query" in schema["properties"]
|
||||
assert "limit" in schema["properties"]
|
||||
|
||||
def test_multiple_excluded_parameters(self):
|
||||
"""Test excluding multiple parameters simultaneously."""
|
||||
import inspect
|
||||
|
||||
def func(ctx: str, session: int, query: str, limit: int = 5) -> str:
|
||||
return ""
|
||||
|
||||
new_func = create_function_without_params(func, ["ctx", "session"])
|
||||
|
||||
sig = inspect.signature(new_func)
|
||||
|
||||
# Both excluded params should be removed
|
||||
assert "ctx" not in sig.parameters
|
||||
assert "session" not in sig.parameters
|
||||
assert "ctx" not in new_func.__annotations__
|
||||
assert "session" not in new_func.__annotations__
|
||||
|
||||
# Non-excluded params should remain
|
||||
assert "query" in sig.parameters
|
||||
assert "limit" in sig.parameters
|
||||
|
||||
|
||||
class TestAnnotationStringDescriptions:
|
||||
"""Test the new functionality for string descriptions in Annotated types."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue