Spaces:
Running
Running
Merge pull request #727 from jlowin/decorators
Browse files- docs/patterns/decorating-methods.mdx +71 -47
- src/fastmcp/prompts/prompt.py +3 -0
- src/fastmcp/resources/template.py +4 -0
- src/fastmcp/server/server.py +96 -20
- src/fastmcp/tools/tool.py +3 -0
- src/fastmcp/utilities/decorators.py +0 -101
- tests/server/test_server.py +86 -10
- tests/server/test_server_interactions.py +0 -50
- tests/utilities/test_decorated_function.py +0 -222
docs/patterns/decorating-methods.mdx
CHANGED
|
@@ -16,11 +16,36 @@ When you apply a FastMCP decorator like `@tool`, `@resource()`, or `@prompt()` t
|
|
| 16 |
|
| 17 |
This means directly decorating these methods doesn't work as expected. In practice, the LLM would see parameters like `self` or `cls` that it cannot provide values for.
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
## Recommended Patterns
|
| 20 |
|
| 21 |
### Instance Methods
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
```python
|
| 26 |
from fastmcp import FastMCP
|
|
@@ -28,17 +53,14 @@ from fastmcp import FastMCP
|
|
| 28 |
mcp = FastMCP()
|
| 29 |
|
| 30 |
class MyClass:
|
| 31 |
-
@mcp.tool() # This won't work correctly
|
| 32 |
def add(self, x, y):
|
| 33 |
return x + y
|
| 34 |
-
|
| 35 |
-
@mcp.resource("resource://{param}") # This won't work correctly
|
| 36 |
-
def get_resource(self, param: str):
|
| 37 |
-
return f"Resource data for {param}"
|
| 38 |
```
|
| 39 |
-
|
| 40 |
When the decorator is applied this way, it captures the unbound method. When the LLM later tries to use this component, it will see `self` as a required parameter, but it won't know what to provide for it, causing errors or unexpected behavior.
|
| 41 |
|
|
|
|
| 42 |
**Do this instead**:
|
| 43 |
|
| 44 |
```python
|
|
@@ -49,21 +71,15 @@ mcp = FastMCP()
|
|
| 49 |
class MyClass:
|
| 50 |
def add(self, x, y):
|
| 51 |
return x + y
|
| 52 |
-
|
| 53 |
-
def get_resource(self, param: str):
|
| 54 |
-
return f"Resource data for {param}"
|
| 55 |
|
| 56 |
-
# Create an instance first, then
|
| 57 |
obj = MyClass()
|
| 58 |
-
mcp.
|
| 59 |
-
mcp.add_resource_fn(obj.get_resource, uri="resource://{param}") # For resources or templates
|
| 60 |
-
|
| 61 |
-
# Note: FastMCP provides add_resource() for adding Resource objects directly and
|
| 62 |
-
# add_resource_fn() for adding functions that generate resources or templates
|
| 63 |
|
| 64 |
# Now you can call it without 'self' showing up as a parameter
|
| 65 |
-
await mcp.
|
| 66 |
```
|
|
|
|
| 67 |
|
| 68 |
This approach works because:
|
| 69 |
1. You first create an instance of the class (`obj`)
|
|
@@ -72,9 +88,10 @@ This approach works because:
|
|
| 72 |
|
| 73 |
### Class Methods
|
| 74 |
|
| 75 |
-
|
| 76 |
|
| 77 |
-
|
|
|
|
| 78 |
|
| 79 |
```python
|
| 80 |
from fastmcp import FastMCP
|
|
@@ -83,13 +100,21 @@ mcp = FastMCP()
|
|
| 83 |
|
| 84 |
class MyClass:
|
| 85 |
@classmethod
|
| 86 |
-
@mcp.tool() # This won't work
|
| 87 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
return cls(s)
|
| 89 |
```
|
|
|
|
| 90 |
|
| 91 |
-
|
|
|
|
| 92 |
|
|
|
|
| 93 |
**Do this instead**:
|
| 94 |
|
| 95 |
```python
|
|
@@ -102,9 +127,10 @@ class MyClass:
|
|
| 102 |
def from_string(cls, s):
|
| 103 |
return cls(s)
|
| 104 |
|
| 105 |
-
#
|
| 106 |
-
mcp.
|
| 107 |
```
|
|
|
|
| 108 |
|
| 109 |
This works because:
|
| 110 |
1. The `@classmethod` decorator is applied properly during class definition
|
|
@@ -113,7 +139,10 @@ This works because:
|
|
| 113 |
|
| 114 |
### Static Methods
|
| 115 |
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
```python
|
| 119 |
from fastmcp import FastMCP
|
|
@@ -121,23 +150,17 @@ from fastmcp import FastMCP
|
|
| 121 |
mcp = FastMCP()
|
| 122 |
|
| 123 |
class MyClass:
|
|
|
|
| 124 |
@staticmethod
|
| 125 |
-
@mcp.tool() # This works!
|
| 126 |
def utility(x, y):
|
| 127 |
return x + y
|
| 128 |
-
|
| 129 |
-
@staticmethod
|
| 130 |
-
@mcp.resource("resource://data") # This works too!
|
| 131 |
-
def get_data():
|
| 132 |
-
return "Static resource data"
|
| 133 |
```
|
|
|
|
| 134 |
|
| 135 |
-
This
|
| 136 |
-
1. The `@staticmethod` decorator is applied first (executed last), transforming the method into a regular function
|
| 137 |
-
2. When the FastMCP decorator is applied, it's capturing what is effectively just a regular function
|
| 138 |
-
3. A static method doesn't have any binding requirements - it doesn't receive a `self` or `cls` parameter
|
| 139 |
|
| 140 |
-
|
|
|
|
| 141 |
|
| 142 |
```python
|
| 143 |
from fastmcp import FastMCP
|
|
@@ -150,10 +173,9 @@ class MyClass:
|
|
| 150 |
return x + y
|
| 151 |
|
| 152 |
# This also works
|
| 153 |
-
mcp.
|
| 154 |
```
|
| 155 |
-
|
| 156 |
-
This works for the same reason - a static method is essentially just a function in a class namespace.
|
| 157 |
|
| 158 |
## Additional Patterns
|
| 159 |
|
|
@@ -169,8 +191,8 @@ mcp = FastMCP()
|
|
| 169 |
class ComponentProvider:
|
| 170 |
def __init__(self, mcp_instance):
|
| 171 |
# Register methods
|
| 172 |
-
mcp_instance.
|
| 173 |
-
mcp_instance.
|
| 174 |
|
| 175 |
def tool_method(self, x):
|
| 176 |
return x * 2
|
|
@@ -191,11 +213,13 @@ The class automatically registers its methods during initialization, ensuring th
|
|
| 191 |
|
| 192 |
## Summary
|
| 193 |
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
-
These patterns apply to all FastMCP decorators and registration methods:
|
| 197 |
-
- `@tool()` and `add_tool`
|
| 198 |
-
- `@resource()` and `add_resource_fn()`
|
| 199 |
-
- `@prompt()` and `add_prompt()`
|
| 200 |
|
| 201 |
-
Understanding these patterns allows you to effectively organize your components into classes while maintaining proper method binding, giving you the benefits of object-oriented design without sacrificing the simplicity of FastMCP's decorator system.
|
|
|
|
| 16 |
|
| 17 |
This means directly decorating these methods doesn't work as expected. In practice, the LLM would see parameters like `self` or `cls` that it cannot provide values for.
|
| 18 |
|
| 19 |
+
Additionally, **FastMCP decorators return objects (Tool, Resource, or Prompt instances) rather than the original function**. This means that when you decorate a method directly, the method becomes the returned object and is no longer callable by your code:
|
| 20 |
+
|
| 21 |
+
<Warning>
|
| 22 |
+
**Don't do this!**
|
| 23 |
+
|
| 24 |
+
The method will no longer be callable from Python, and the tool won't be callable by LLMs.
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
|
| 28 |
+
from fastmcp import FastMCP
|
| 29 |
+
mcp = FastMCP()
|
| 30 |
+
|
| 31 |
+
class MyClass:
|
| 32 |
+
@mcp.tool()
|
| 33 |
+
def my_method(self, x: int) -> int:
|
| 34 |
+
return x * 2
|
| 35 |
+
|
| 36 |
+
obj = MyClass()
|
| 37 |
+
obj.my_method(5) # Fails - my_method is a Tool, not a function
|
| 38 |
+
```
|
| 39 |
+
</Warning>
|
| 40 |
+
|
| 41 |
+
This is another important reason to register methods functionally after defining the class.
|
| 42 |
+
|
| 43 |
## Recommended Patterns
|
| 44 |
|
| 45 |
### Instance Methods
|
| 46 |
|
| 47 |
+
<Warning>
|
| 48 |
+
**Don't do this!**
|
| 49 |
|
| 50 |
```python
|
| 51 |
from fastmcp import FastMCP
|
|
|
|
| 53 |
mcp = FastMCP()
|
| 54 |
|
| 55 |
class MyClass:
|
| 56 |
+
@mcp.tool() # This won't work correctly
|
| 57 |
def add(self, x, y):
|
| 58 |
return x + y
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
```
|
| 60 |
+
</Warning>
|
| 61 |
When the decorator is applied this way, it captures the unbound method. When the LLM later tries to use this component, it will see `self` as a required parameter, but it won't know what to provide for it, causing errors or unexpected behavior.
|
| 62 |
|
| 63 |
+
<Check>
|
| 64 |
**Do this instead**:
|
| 65 |
|
| 66 |
```python
|
|
|
|
| 71 |
class MyClass:
|
| 72 |
def add(self, x, y):
|
| 73 |
return x + y
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
# Create an instance first, then register the bound methods
|
| 76 |
obj = MyClass()
|
| 77 |
+
mcp.tool(obj.add)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
# Now you can call it without 'self' showing up as a parameter
|
| 80 |
+
await mcp._mcp_call_tool('add', {'x': 1, 'y': 2}) # Returns 3
|
| 81 |
```
|
| 82 |
+
</Check>
|
| 83 |
|
| 84 |
This approach works because:
|
| 85 |
1. You first create an instance of the class (`obj`)
|
|
|
|
| 88 |
|
| 89 |
### Class Methods
|
| 90 |
|
| 91 |
+
The behavior of decorating class methods depends on the order of decorators:
|
| 92 |
|
| 93 |
+
<Warning>
|
| 94 |
+
**Don't do this** (decorator order matters):
|
| 95 |
|
| 96 |
```python
|
| 97 |
from fastmcp import FastMCP
|
|
|
|
| 100 |
|
| 101 |
class MyClass:
|
| 102 |
@classmethod
|
| 103 |
+
@mcp.tool() # This won't work but won't raise an error
|
| 104 |
+
def from_string_v1(cls, s):
|
| 105 |
+
return cls(s)
|
| 106 |
+
|
| 107 |
+
@mcp.tool()
|
| 108 |
+
@classmethod # This will raise a helpful ValueError
|
| 109 |
+
def from_string_v2(cls, s):
|
| 110 |
return cls(s)
|
| 111 |
```
|
| 112 |
+
</Warning>
|
| 113 |
|
| 114 |
+
- If `@classmethod` comes first, then `@mcp.tool()`: No error is raised, but it won't work correctly
|
| 115 |
+
- If `@mcp.tool()` comes first, then `@classmethod`: FastMCP will detect this and raise a helpful `ValueError` with guidance
|
| 116 |
|
| 117 |
+
<Check>
|
| 118 |
**Do this instead**:
|
| 119 |
|
| 120 |
```python
|
|
|
|
| 127 |
def from_string(cls, s):
|
| 128 |
return cls(s)
|
| 129 |
|
| 130 |
+
# Register the class method after the class is defined
|
| 131 |
+
mcp.tool(MyClass.from_string)
|
| 132 |
```
|
| 133 |
+
</Check>
|
| 134 |
|
| 135 |
This works because:
|
| 136 |
1. The `@classmethod` decorator is applied properly during class definition
|
|
|
|
| 139 |
|
| 140 |
### Static Methods
|
| 141 |
|
| 142 |
+
Static methods "work" with FastMCP decorators, but this is not recommended because the FastMCP decorator will not return a callable method. Therefore, you should register static methods the same way as other methods.
|
| 143 |
+
|
| 144 |
+
<Warning>
|
| 145 |
+
**This is not recommended, though it will work.**
|
| 146 |
|
| 147 |
```python
|
| 148 |
from fastmcp import FastMCP
|
|
|
|
| 150 |
mcp = FastMCP()
|
| 151 |
|
| 152 |
class MyClass:
|
| 153 |
+
@mcp.tool()
|
| 154 |
@staticmethod
|
|
|
|
| 155 |
def utility(x, y):
|
| 156 |
return x + y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
```
|
| 158 |
+
</Warning>
|
| 159 |
|
| 160 |
+
This works because `@staticmethod` converts the method to a regular function, which the FastMCP decorator can then properly process. However, this is not recommended because the FastMCP decorator will not return a callable staticmethod. Therefore, you should register static methods the same way as other methods.
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
<Check>
|
| 163 |
+
**Prefer this pattern:**
|
| 164 |
|
| 165 |
```python
|
| 166 |
from fastmcp import FastMCP
|
|
|
|
| 173 |
return x + y
|
| 174 |
|
| 175 |
# This also works
|
| 176 |
+
mcp.tool(MyClass.utility)
|
| 177 |
```
|
| 178 |
+
</Check>
|
|
|
|
| 179 |
|
| 180 |
## Additional Patterns
|
| 181 |
|
|
|
|
| 191 |
class ComponentProvider:
|
| 192 |
def __init__(self, mcp_instance):
|
| 193 |
# Register methods
|
| 194 |
+
mcp_instance.tool(self.tool_method)
|
| 195 |
+
mcp_instance.resource("resource://data")(self.resource_method)
|
| 196 |
|
| 197 |
def tool_method(self, x):
|
| 198 |
return x * 2
|
|
|
|
| 213 |
|
| 214 |
## Summary
|
| 215 |
|
| 216 |
+
The current behavior of FastMCP decorators with methods is:
|
| 217 |
+
|
| 218 |
+
- **Static methods**: Can be decorated directly and work perfectly with all FastMCP decorators
|
| 219 |
+
- **Class methods**: Cannot be decorated directly and will raise a helpful `ValueError` with guidance
|
| 220 |
+
- **Instance methods**: Should be registered after creating an instance using the decorator calls
|
| 221 |
+
|
| 222 |
+
For class and instance methods, you should register them after creating the instance or class to ensure proper method binding. This ensures that the methods are properly bound before being registered.
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
+
Understanding these patterns allows you to effectively organize your components into classes while maintaining proper method binding, giving you the benefits of object-oriented design without sacrificing the simplicity of FastMCP's decorator system.
|
src/fastmcp/prompts/prompt.py
CHANGED
|
@@ -171,6 +171,9 @@ class FunctionPrompt(Prompt):
|
|
| 171 |
# if the fn is a callable class, we need to get the __call__ method from here out
|
| 172 |
if not inspect.isroutine(fn):
|
| 173 |
fn = fn.__call__
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
type_adapter = get_cached_typeadapter(fn)
|
| 176 |
parameters = type_adapter.json_schema()
|
|
|
|
| 171 |
# if the fn is a callable class, we need to get the __call__ method from here out
|
| 172 |
if not inspect.isroutine(fn):
|
| 173 |
fn = fn.__call__
|
| 174 |
+
# if the fn is a staticmethod, we need to work with the underlying function
|
| 175 |
+
if isinstance(fn, staticmethod):
|
| 176 |
+
fn = fn.__func__
|
| 177 |
|
| 178 |
type_adapter = get_cached_typeadapter(fn)
|
| 179 |
parameters = type_adapter.json_schema()
|
src/fastmcp/resources/template.py
CHANGED
|
@@ -225,8 +225,12 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
| 225 |
|
| 226 |
description = description or fn.__doc__
|
| 227 |
|
|
|
|
| 228 |
if not inspect.isroutine(fn):
|
| 229 |
fn = fn.__call__
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
type_adapter = get_cached_typeadapter(fn)
|
| 232 |
parameters = type_adapter.json_schema()
|
|
|
|
| 225 |
|
| 226 |
description = description or fn.__doc__
|
| 227 |
|
| 228 |
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
| 229 |
if not inspect.isroutine(fn):
|
| 230 |
fn = fn.__call__
|
| 231 |
+
# if the fn is a staticmethod, we need to work with the underlying function
|
| 232 |
+
if isinstance(fn, staticmethod):
|
| 233 |
+
fn = fn.__func__
|
| 234 |
|
| 235 |
type_adapter = get_cached_typeadapter(fn)
|
| 236 |
parameters = type_adapter.json_schema()
|
src/fastmcp/server/server.py
CHANGED
|
@@ -14,7 +14,7 @@ from contextlib import (
|
|
| 14 |
)
|
| 15 |
from functools import partial
|
| 16 |
from pathlib import Path
|
| 17 |
-
from typing import TYPE_CHECKING, Any, Generic, Literal
|
| 18 |
|
| 19 |
import anyio
|
| 20 |
import httpx
|
|
@@ -45,6 +45,7 @@ import fastmcp.server
|
|
| 45 |
import fastmcp.settings
|
| 46 |
from fastmcp.exceptions import NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
|
|
|
| 48 |
from fastmcp.resources import Resource, ResourceManager
|
| 49 |
from fastmcp.resources.template import ResourceTemplate
|
| 50 |
from fastmcp.server.auth.auth import OAuthProvider
|
|
@@ -55,9 +56,8 @@ from fastmcp.server.http import (
|
|
| 55 |
create_streamable_http_app,
|
| 56 |
)
|
| 57 |
from fastmcp.tools import ToolManager
|
| 58 |
-
from fastmcp.tools.tool import Tool
|
| 59 |
from fastmcp.utilities.cache import TimedCache
|
| 60 |
-
from fastmcp.utilities.decorators import DecoratedFunction
|
| 61 |
from fastmcp.utilities.logging import get_logger
|
| 62 |
from fastmcp.utilities.mcp_config import MCPConfig
|
| 63 |
|
|
@@ -511,6 +511,30 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 511 |
self._tool_manager.remove_tool(name)
|
| 512 |
self._cache.clear()
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
def tool(
|
| 515 |
self,
|
| 516 |
name_or_fn: str | AnyFunction | None = None,
|
|
@@ -520,7 +544,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 520 |
tags: set[str] | None = None,
|
| 521 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 522 |
exclude_args: list[str] | None = None,
|
| 523 |
-
) -> Callable[[AnyFunction],
|
| 524 |
"""Decorator to register a tool.
|
| 525 |
|
| 526 |
Tools can optionally request a Context object by adding a parameter with the
|
|
@@ -565,14 +589,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 565 |
if isinstance(annotations, dict):
|
| 566 |
annotations = ToolAnnotations(**annotations)
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
# Determine the actual name and function based on the calling pattern
|
| 569 |
-
if
|
| 570 |
# Case 1: @tool (without parens) - function passed directly
|
| 571 |
# Case 2: direct call like tool(fn, name="something")
|
| 572 |
fn = name_or_fn
|
| 573 |
tool_name = name # Use keyword name if provided, otherwise None
|
| 574 |
|
| 575 |
-
# Register the tool immediately and return the
|
| 576 |
tool = Tool.from_function(
|
| 577 |
fn,
|
| 578 |
name=tool_name,
|
|
@@ -583,7 +619,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 583 |
serializer=self._tool_serializer,
|
| 584 |
)
|
| 585 |
self.add_tool(tool)
|
| 586 |
-
return
|
| 587 |
|
| 588 |
elif isinstance(name_or_fn, str):
|
| 589 |
# Case 3: @tool("custom_name") - name passed as first argument
|
|
@@ -675,7 +711,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 675 |
description: str | None = None,
|
| 676 |
mime_type: str | None = None,
|
| 677 |
tags: set[str] | None = None,
|
| 678 |
-
) -> Callable[[AnyFunction],
|
| 679 |
"""Decorator to register a function as a resource.
|
| 680 |
|
| 681 |
The function will be called when the resource is read to generate its content.
|
|
@@ -723,15 +759,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 723 |
return f"Weather for {city}: {data}"
|
| 724 |
"""
|
| 725 |
# Check if user passed function directly instead of calling decorator
|
| 726 |
-
if
|
| 727 |
raise TypeError(
|
| 728 |
"The @resource decorator was used incorrectly. "
|
| 729 |
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
| 730 |
)
|
| 731 |
|
| 732 |
-
def decorator(fn: AnyFunction) ->
|
| 733 |
from fastmcp.server.context import Context
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
# Check if this should be a template
|
| 736 |
has_uri_params = "{" in uri and "}" in uri
|
| 737 |
# check if the function has any parameters (other than injected context)
|
|
@@ -751,6 +799,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 751 |
tags=tags,
|
| 752 |
)
|
| 753 |
self.add_template(template)
|
|
|
|
| 754 |
elif not has_uri_params and not has_func_params:
|
| 755 |
resource = Resource.from_function(
|
| 756 |
fn=fn,
|
|
@@ -761,14 +810,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 761 |
tags=tags,
|
| 762 |
)
|
| 763 |
self.add_resource(resource)
|
|
|
|
| 764 |
else:
|
| 765 |
raise ValueError(
|
| 766 |
"Invalid resource or template definition due to a "
|
| 767 |
"mismatch between URI parameters and function parameters."
|
| 768 |
)
|
| 769 |
|
| 770 |
-
return fn
|
| 771 |
-
|
| 772 |
return decorator
|
| 773 |
|
| 774 |
def add_prompt(self, prompt: Prompt) -> None:
|
|
@@ -780,6 +828,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 780 |
self._prompt_manager.add_prompt(prompt)
|
| 781 |
self._cache.clear()
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
def prompt(
|
| 784 |
self,
|
| 785 |
name_or_fn: str | AnyFunction | None = None,
|
|
@@ -787,7 +855,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 787 |
name: str | None = None,
|
| 788 |
description: str | None = None,
|
| 789 |
tags: set[str] | None = None,
|
| 790 |
-
) -> Callable[[AnyFunction],
|
| 791 |
"""Decorator to register a prompt.
|
| 792 |
|
| 793 |
Prompts can optionally request a Context object by adding a parameter with the
|
|
@@ -852,8 +920,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 852 |
# Direct function call
|
| 853 |
server.prompt(my_function, name="custom_name")
|
| 854 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
# Determine the actual name and function based on the calling pattern
|
| 856 |
-
if
|
| 857 |
# Case 1: @prompt (without parens) - function passed directly as decorator
|
| 858 |
# Case 2: direct call like prompt(fn, name="something")
|
| 859 |
fn = name_or_fn
|
|
@@ -868,12 +949,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 868 |
)
|
| 869 |
self.add_prompt(prompt)
|
| 870 |
|
| 871 |
-
|
| 872 |
-
# If name is None, this is @prompt without parens, return DecoratedFunction for proper method handling
|
| 873 |
-
if name is not None:
|
| 874 |
-
return fn # Direct function call
|
| 875 |
-
else:
|
| 876 |
-
return DecoratedFunction(fn) # Decorator usage
|
| 877 |
|
| 878 |
elif isinstance(name_or_fn, str):
|
| 879 |
# Case 3: @prompt("custom_name") - name passed as first argument
|
|
|
|
| 14 |
)
|
| 15 |
from functools import partial
|
| 16 |
from pathlib import Path
|
| 17 |
+
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
| 18 |
|
| 19 |
import anyio
|
| 20 |
import httpx
|
|
|
|
| 45 |
import fastmcp.settings
|
| 46 |
from fastmcp.exceptions import NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
| 48 |
+
from fastmcp.prompts.prompt import FunctionPrompt
|
| 49 |
from fastmcp.resources import Resource, ResourceManager
|
| 50 |
from fastmcp.resources.template import ResourceTemplate
|
| 51 |
from fastmcp.server.auth.auth import OAuthProvider
|
|
|
|
| 56 |
create_streamable_http_app,
|
| 57 |
)
|
| 58 |
from fastmcp.tools import ToolManager
|
| 59 |
+
from fastmcp.tools.tool import FunctionTool, Tool
|
| 60 |
from fastmcp.utilities.cache import TimedCache
|
|
|
|
| 61 |
from fastmcp.utilities.logging import get_logger
|
| 62 |
from fastmcp.utilities.mcp_config import MCPConfig
|
| 63 |
|
|
|
|
| 511 |
self._tool_manager.remove_tool(name)
|
| 512 |
self._cache.clear()
|
| 513 |
|
| 514 |
+
@overload
|
| 515 |
+
def tool(
|
| 516 |
+
self,
|
| 517 |
+
name_or_fn: AnyFunction,
|
| 518 |
+
*,
|
| 519 |
+
name: str | None = None,
|
| 520 |
+
description: str | None = None,
|
| 521 |
+
tags: set[str] | None = None,
|
| 522 |
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 523 |
+
exclude_args: list[str] | None = None,
|
| 524 |
+
) -> FunctionTool: ...
|
| 525 |
+
|
| 526 |
+
@overload
|
| 527 |
+
def tool(
|
| 528 |
+
self,
|
| 529 |
+
name_or_fn: str | None = None,
|
| 530 |
+
*,
|
| 531 |
+
name: str | None = None,
|
| 532 |
+
description: str | None = None,
|
| 533 |
+
tags: set[str] | None = None,
|
| 534 |
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 535 |
+
exclude_args: list[str] | None = None,
|
| 536 |
+
) -> Callable[[AnyFunction], FunctionTool]: ...
|
| 537 |
+
|
| 538 |
def tool(
|
| 539 |
self,
|
| 540 |
name_or_fn: str | AnyFunction | None = None,
|
|
|
|
| 544 |
tags: set[str] | None = None,
|
| 545 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 546 |
exclude_args: list[str] | None = None,
|
| 547 |
+
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
| 548 |
"""Decorator to register a tool.
|
| 549 |
|
| 550 |
Tools can optionally request a Context object by adding a parameter with the
|
|
|
|
| 589 |
if isinstance(annotations, dict):
|
| 590 |
annotations = ToolAnnotations(**annotations)
|
| 591 |
|
| 592 |
+
if isinstance(name_or_fn, classmethod):
|
| 593 |
+
raise ValueError(
|
| 594 |
+
inspect.cleandoc(
|
| 595 |
+
"""
|
| 596 |
+
To decorate a classmethod, first define the method and then call
|
| 597 |
+
tool() directly on the method instead of using it as a
|
| 598 |
+
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
| 599 |
+
for examples and more information.
|
| 600 |
+
"""
|
| 601 |
+
)
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
# Determine the actual name and function based on the calling pattern
|
| 605 |
+
if inspect.isroutine(name_or_fn):
|
| 606 |
# Case 1: @tool (without parens) - function passed directly
|
| 607 |
# Case 2: direct call like tool(fn, name="something")
|
| 608 |
fn = name_or_fn
|
| 609 |
tool_name = name # Use keyword name if provided, otherwise None
|
| 610 |
|
| 611 |
+
# Register the tool immediately and return the tool object
|
| 612 |
tool = Tool.from_function(
|
| 613 |
fn,
|
| 614 |
name=tool_name,
|
|
|
|
| 619 |
serializer=self._tool_serializer,
|
| 620 |
)
|
| 621 |
self.add_tool(tool)
|
| 622 |
+
return tool
|
| 623 |
|
| 624 |
elif isinstance(name_or_fn, str):
|
| 625 |
# Case 3: @tool("custom_name") - name passed as first argument
|
|
|
|
| 711 |
description: str | None = None,
|
| 712 |
mime_type: str | None = None,
|
| 713 |
tags: set[str] | None = None,
|
| 714 |
+
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
| 715 |
"""Decorator to register a function as a resource.
|
| 716 |
|
| 717 |
The function will be called when the resource is read to generate its content.
|
|
|
|
| 759 |
return f"Weather for {city}: {data}"
|
| 760 |
"""
|
| 761 |
# Check if user passed function directly instead of calling decorator
|
| 762 |
+
if inspect.isroutine(uri):
|
| 763 |
raise TypeError(
|
| 764 |
"The @resource decorator was used incorrectly. "
|
| 765 |
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
| 766 |
)
|
| 767 |
|
| 768 |
+
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
|
| 769 |
from fastmcp.server.context import Context
|
| 770 |
|
| 771 |
+
if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
|
| 772 |
+
raise ValueError(
|
| 773 |
+
inspect.cleandoc(
|
| 774 |
+
"""
|
| 775 |
+
To decorate a classmethod, first define the method and then call
|
| 776 |
+
resource() directly on the method instead of using it as a
|
| 777 |
+
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
| 778 |
+
for examples and more information.
|
| 779 |
+
"""
|
| 780 |
+
)
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
# Check if this should be a template
|
| 784 |
has_uri_params = "{" in uri and "}" in uri
|
| 785 |
# check if the function has any parameters (other than injected context)
|
|
|
|
| 799 |
tags=tags,
|
| 800 |
)
|
| 801 |
self.add_template(template)
|
| 802 |
+
return template
|
| 803 |
elif not has_uri_params and not has_func_params:
|
| 804 |
resource = Resource.from_function(
|
| 805 |
fn=fn,
|
|
|
|
| 810 |
tags=tags,
|
| 811 |
)
|
| 812 |
self.add_resource(resource)
|
| 813 |
+
return resource
|
| 814 |
else:
|
| 815 |
raise ValueError(
|
| 816 |
"Invalid resource or template definition due to a "
|
| 817 |
"mismatch between URI parameters and function parameters."
|
| 818 |
)
|
| 819 |
|
|
|
|
|
|
|
| 820 |
return decorator
|
| 821 |
|
| 822 |
def add_prompt(self, prompt: Prompt) -> None:
|
|
|
|
| 828 |
self._prompt_manager.add_prompt(prompt)
|
| 829 |
self._cache.clear()
|
| 830 |
|
| 831 |
+
@overload
|
| 832 |
+
def prompt(
|
| 833 |
+
self,
|
| 834 |
+
name_or_fn: AnyFunction,
|
| 835 |
+
*,
|
| 836 |
+
name: str | None = None,
|
| 837 |
+
description: str | None = None,
|
| 838 |
+
tags: set[str] | None = None,
|
| 839 |
+
) -> FunctionPrompt: ...
|
| 840 |
+
|
| 841 |
+
@overload
|
| 842 |
+
def prompt(
|
| 843 |
+
self,
|
| 844 |
+
name_or_fn: str | None = None,
|
| 845 |
+
*,
|
| 846 |
+
name: str | None = None,
|
| 847 |
+
description: str | None = None,
|
| 848 |
+
tags: set[str] | None = None,
|
| 849 |
+
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
| 850 |
+
|
| 851 |
def prompt(
|
| 852 |
self,
|
| 853 |
name_or_fn: str | AnyFunction | None = None,
|
|
|
|
| 855 |
name: str | None = None,
|
| 856 |
description: str | None = None,
|
| 857 |
tags: set[str] | None = None,
|
| 858 |
+
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
| 859 |
"""Decorator to register a prompt.
|
| 860 |
|
| 861 |
Prompts can optionally request a Context object by adding a parameter with the
|
|
|
|
| 920 |
# Direct function call
|
| 921 |
server.prompt(my_function, name="custom_name")
|
| 922 |
"""
|
| 923 |
+
|
| 924 |
+
if isinstance(name_or_fn, classmethod):
|
| 925 |
+
raise ValueError(
|
| 926 |
+
inspect.cleandoc(
|
| 927 |
+
"""
|
| 928 |
+
To decorate a classmethod, first define the method and then call
|
| 929 |
+
prompt() directly on the method instead of using it as a
|
| 930 |
+
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
| 931 |
+
for examples and more information.
|
| 932 |
+
"""
|
| 933 |
+
)
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
# Determine the actual name and function based on the calling pattern
|
| 937 |
+
if inspect.isroutine(name_or_fn):
|
| 938 |
# Case 1: @prompt (without parens) - function passed directly as decorator
|
| 939 |
# Case 2: direct call like prompt(fn, name="something")
|
| 940 |
fn = name_or_fn
|
|
|
|
| 949 |
)
|
| 950 |
self.add_prompt(prompt)
|
| 951 |
|
| 952 |
+
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 953 |
|
| 954 |
elif isinstance(name_or_fn, str):
|
| 955 |
# Case 3: @prompt("custom_name") - name passed as first argument
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -146,6 +146,9 @@ class FunctionTool(Tool):
|
|
| 146 |
# if the fn is a callable class, we need to get the __call__ method from here out
|
| 147 |
if not inspect.isroutine(fn):
|
| 148 |
fn = fn.__call__
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
type_adapter = get_cached_typeadapter(fn)
|
| 151 |
schema = type_adapter.json_schema()
|
|
|
|
| 146 |
# if the fn is a callable class, we need to get the __call__ method from here out
|
| 147 |
if not inspect.isroutine(fn):
|
| 148 |
fn = fn.__call__
|
| 149 |
+
# if the fn is a staticmethod, we need to work with the underlying function
|
| 150 |
+
if isinstance(fn, staticmethod):
|
| 151 |
+
fn = fn.__func__
|
| 152 |
|
| 153 |
type_adapter = get_cached_typeadapter(fn)
|
| 154 |
schema = type_adapter.json_schema()
|
src/fastmcp/utilities/decorators.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
import inspect
|
| 2 |
-
from collections.abc import Callable
|
| 3 |
-
from typing import Generic, ParamSpec, TypeVar, cast, overload
|
| 4 |
-
|
| 5 |
-
from typing_extensions import Self
|
| 6 |
-
|
| 7 |
-
R = TypeVar("R")
|
| 8 |
-
P = ParamSpec("P")
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class DecoratedFunction(Generic[P, R]):
|
| 12 |
-
"""Descriptor for decorated functions.
|
| 13 |
-
|
| 14 |
-
You can return this object from a decorator to ensure that it works across
|
| 15 |
-
all types of functions: vanilla, instance methods, class methods, and static
|
| 16 |
-
methods; both synchronous and asynchronous.
|
| 17 |
-
|
| 18 |
-
This class is used to store the original function and metadata about how to
|
| 19 |
-
register it as a tool.
|
| 20 |
-
|
| 21 |
-
Example usage:
|
| 22 |
-
|
| 23 |
-
```python
|
| 24 |
-
def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]:
|
| 25 |
-
return DecoratedFunction(fn)
|
| 26 |
-
```
|
| 27 |
-
|
| 28 |
-
On a function:
|
| 29 |
-
```python
|
| 30 |
-
@my_decorator
|
| 31 |
-
def my_function(a: int, b: int) -> int:
|
| 32 |
-
return a + b
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
On an instance method:
|
| 36 |
-
```python
|
| 37 |
-
class Test:
|
| 38 |
-
@my_decorator
|
| 39 |
-
def my_function(self, a: int, b: int) -> int:
|
| 40 |
-
return a + b
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
On a class method:
|
| 44 |
-
```python
|
| 45 |
-
class Test:
|
| 46 |
-
@classmethod
|
| 47 |
-
@my_decorator
|
| 48 |
-
def my_function(cls, a: int, b: int) -> int:
|
| 49 |
-
return a + b
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
Note that for classmethods, the decorator must be applied first, then
|
| 53 |
-
`@classmethod` on top.
|
| 54 |
-
|
| 55 |
-
On a static method:
|
| 56 |
-
```python
|
| 57 |
-
class Test:
|
| 58 |
-
@staticmethod
|
| 59 |
-
@my_decorator
|
| 60 |
-
def my_function(a: int, b: int) -> int:
|
| 61 |
-
return a + b
|
| 62 |
-
```
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
def __init__(self, fn: Callable[P, R]):
|
| 66 |
-
self.fn = fn
|
| 67 |
-
|
| 68 |
-
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
| 69 |
-
"""Call the original function."""
|
| 70 |
-
try:
|
| 71 |
-
return self.fn(*args, **kwargs)
|
| 72 |
-
except TypeError as e:
|
| 73 |
-
if "'classmethod' object is not callable" in str(e):
|
| 74 |
-
raise TypeError(
|
| 75 |
-
"To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top."
|
| 76 |
-
)
|
| 77 |
-
raise
|
| 78 |
-
|
| 79 |
-
@overload
|
| 80 |
-
def __get__(self, instance: None, owner: type | None = None) -> Self: ...
|
| 81 |
-
|
| 82 |
-
@overload
|
| 83 |
-
def __get__(
|
| 84 |
-
self, instance: object, owner: type | None = None
|
| 85 |
-
) -> Callable[P, R]: ...
|
| 86 |
-
|
| 87 |
-
def __get__(
|
| 88 |
-
self, instance: object | None, owner: type | None = None
|
| 89 |
-
) -> Self | Callable[P, R]:
|
| 90 |
-
"""Return the original function when accessed from an instance, or self when accessed from the class."""
|
| 91 |
-
if instance is None:
|
| 92 |
-
return self
|
| 93 |
-
# Return the original function bound to the instance
|
| 94 |
-
return cast(Callable[P, R], self.fn.__get__(instance, owner))
|
| 95 |
-
|
| 96 |
-
def __repr__(self) -> str:
|
| 97 |
-
"""Return a representation that matches Python's function representation."""
|
| 98 |
-
module = getattr(self.fn, "__module__", "unknown")
|
| 99 |
-
qualname = getattr(self.fn, "__qualname__", str(self.fn))
|
| 100 |
-
sig_str = str(inspect.signature(self.fn))
|
| 101 |
-
return f"<function {module}.{qualname}{sig_str}>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/server/test_server.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic import Field
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.exceptions import NotFoundError
|
| 9 |
-
from fastmcp.prompts.prompt import Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
| 12 |
MountedServer,
|
|
@@ -179,7 +179,6 @@ class TestToolDecorator:
|
|
| 179 |
def __init__(self, x: int):
|
| 180 |
self.x = x
|
| 181 |
|
| 182 |
-
@mcp.tool
|
| 183 |
def add(self, y: int) -> int:
|
| 184 |
return self.x + y
|
| 185 |
|
|
@@ -206,8 +205,8 @@ class TestToolDecorator:
|
|
| 206 |
mcp = FastMCP()
|
| 207 |
|
| 208 |
class MyClass:
|
| 209 |
-
@staticmethod
|
| 210 |
@mcp.tool
|
|
|
|
| 211 |
def add(x: int, y: int) -> int:
|
| 212 |
return x + y
|
| 213 |
|
|
@@ -224,6 +223,17 @@ class TestToolDecorator:
|
|
| 224 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 225 |
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
async def test_tool_decorator_classmethod_async_function(self):
|
| 228 |
mcp = FastMCP()
|
| 229 |
|
|
@@ -250,6 +260,20 @@ class TestToolDecorator:
|
|
| 250 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 251 |
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
async def test_tool_decorator_with_tags(self):
|
| 254 |
"""Test that the tool decorator properly sets tags."""
|
| 255 |
mcp = FastMCP()
|
|
@@ -326,11 +350,11 @@ class TestToolDecorator:
|
|
| 326 |
result_fn = mcp.tool(standalone_function, name="direct_call_tool")
|
| 327 |
|
| 328 |
# The function should be returned unchanged
|
| 329 |
-
assert result_fn
|
| 330 |
|
| 331 |
# Verify the tool was registered correctly
|
| 332 |
tools = await mcp.get_tools()
|
| 333 |
-
assert "direct_call_tool"
|
| 334 |
|
| 335 |
# Verify it can be called
|
| 336 |
result = await mcp._mcp_call_tool("direct_call_tool", {"x": 5, "y": 3})
|
|
@@ -481,12 +505,23 @@ class TestResourceDecorator:
|
|
| 481 |
result = await client.read_resource("resource://data")
|
| 482 |
assert result[0].text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
| 483 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
async def test_resource_decorator_staticmethod(self):
|
| 485 |
mcp = FastMCP()
|
| 486 |
|
| 487 |
class MyClass:
|
| 488 |
-
@staticmethod
|
| 489 |
@mcp.resource("resource://data")
|
|
|
|
| 490 |
def get_data() -> str:
|
| 491 |
return "Static Hello, world!"
|
| 492 |
|
|
@@ -505,6 +540,20 @@ class TestResourceDecorator:
|
|
| 505 |
result = await client.read_resource("resource://data")
|
| 506 |
assert result[0].text == "Async Hello, world!" # type: ignore[attr-defined]
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
class TestTemplateDecorator:
|
| 510 |
async def test_template_decorator(self):
|
|
@@ -610,8 +659,8 @@ class TestTemplateDecorator:
|
|
| 610 |
mcp = FastMCP()
|
| 611 |
|
| 612 |
class MyClass:
|
| 613 |
-
@staticmethod
|
| 614 |
@mcp.resource("resource://{name}/data")
|
|
|
|
| 615 |
def get_data(name: str) -> str:
|
| 616 |
return f"Static Data for {name}"
|
| 617 |
|
|
@@ -784,12 +833,23 @@ class TestPromptDecorator:
|
|
| 784 |
message = result.messages[0]
|
| 785 |
assert message.content.text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
| 786 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
async def test_prompt_decorator_staticmethod(self):
|
| 788 |
mcp = FastMCP()
|
| 789 |
|
| 790 |
class MyClass:
|
| 791 |
-
@staticmethod
|
| 792 |
@mcp.prompt
|
|
|
|
| 793 |
def test_prompt() -> str:
|
| 794 |
return "Static Hello, world!"
|
| 795 |
|
|
@@ -857,11 +917,11 @@ class TestPromptDecorator:
|
|
| 857 |
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
|
| 858 |
|
| 859 |
# The function should be returned unchanged
|
| 860 |
-
assert result_fn
|
| 861 |
|
| 862 |
# Verify the prompt was registered correctly
|
| 863 |
prompts = await mcp.get_prompts()
|
| 864 |
-
assert "direct_call_prompt"
|
| 865 |
|
| 866 |
# Verify it can be called
|
| 867 |
async with Client(mcp) as client:
|
|
@@ -882,6 +942,22 @@ class TestPromptDecorator:
|
|
| 882 |
def my_function() -> str:
|
| 883 |
return "Hello, world!"
|
| 884 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
|
| 886 |
class TestResourcePrefixHelpers:
|
| 887 |
@pytest.mark.parametrize(
|
|
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.exceptions import NotFoundError
|
| 9 |
+
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
| 12 |
MountedServer,
|
|
|
|
| 179 |
def __init__(self, x: int):
|
| 180 |
self.x = x
|
| 181 |
|
|
|
|
| 182 |
def add(self, y: int) -> int:
|
| 183 |
return self.x + y
|
| 184 |
|
|
|
|
| 205 |
mcp = FastMCP()
|
| 206 |
|
| 207 |
class MyClass:
|
|
|
|
| 208 |
@mcp.tool
|
| 209 |
+
@staticmethod
|
| 210 |
def add(x: int, y: int) -> int:
|
| 211 |
return x + y
|
| 212 |
|
|
|
|
| 223 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 224 |
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 225 |
|
| 226 |
+
async def test_tool_decorator_classmethod_error(self):
|
| 227 |
+
mcp = FastMCP()
|
| 228 |
+
|
| 229 |
+
with pytest.raises(ValueError, match="To decorate a classmethod"):
|
| 230 |
+
|
| 231 |
+
class MyClass:
|
| 232 |
+
@mcp.tool
|
| 233 |
+
@classmethod
|
| 234 |
+
def add(cls, y: int) -> None:
|
| 235 |
+
pass
|
| 236 |
+
|
| 237 |
async def test_tool_decorator_classmethod_async_function(self):
|
| 238 |
mcp = FastMCP()
|
| 239 |
|
|
|
|
| 260 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 261 |
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 262 |
|
| 263 |
+
async def test_tool_decorator_staticmethod_order(self):
|
| 264 |
+
"""Test that the recommended decorator order works for static methods"""
|
| 265 |
+
mcp = FastMCP()
|
| 266 |
+
|
| 267 |
+
class MyClass:
|
| 268 |
+
@mcp.tool
|
| 269 |
+
@staticmethod
|
| 270 |
+
def add_v1(x: int, y: int) -> int:
|
| 271 |
+
return x + y
|
| 272 |
+
|
| 273 |
+
# Test that the recommended order works
|
| 274 |
+
result = await mcp._mcp_call_tool("add_v1", {"x": 1, "y": 2})
|
| 275 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 276 |
+
|
| 277 |
async def test_tool_decorator_with_tags(self):
|
| 278 |
"""Test that the tool decorator properly sets tags."""
|
| 279 |
mcp = FastMCP()
|
|
|
|
| 350 |
result_fn = mcp.tool(standalone_function, name="direct_call_tool")
|
| 351 |
|
| 352 |
# The function should be returned unchanged
|
| 353 |
+
assert isinstance(result_fn, FunctionTool)
|
| 354 |
|
| 355 |
# Verify the tool was registered correctly
|
| 356 |
tools = await mcp.get_tools()
|
| 357 |
+
assert tools["direct_call_tool"] is result_fn
|
| 358 |
|
| 359 |
# Verify it can be called
|
| 360 |
result = await mcp._mcp_call_tool("direct_call_tool", {"x": 5, "y": 3})
|
|
|
|
| 505 |
result = await client.read_resource("resource://data")
|
| 506 |
assert result[0].text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
| 507 |
|
| 508 |
+
async def test_resource_decorator_classmethod_error(self):
|
| 509 |
+
mcp = FastMCP()
|
| 510 |
+
|
| 511 |
+
with pytest.raises(ValueError, match="To decorate a classmethod"):
|
| 512 |
+
|
| 513 |
+
class MyClass:
|
| 514 |
+
@mcp.resource("resource://data")
|
| 515 |
+
@classmethod
|
| 516 |
+
def get_data(cls) -> None:
|
| 517 |
+
pass
|
| 518 |
+
|
| 519 |
async def test_resource_decorator_staticmethod(self):
|
| 520 |
mcp = FastMCP()
|
| 521 |
|
| 522 |
class MyClass:
|
|
|
|
| 523 |
@mcp.resource("resource://data")
|
| 524 |
+
@staticmethod
|
| 525 |
def get_data() -> str:
|
| 526 |
return "Static Hello, world!"
|
| 527 |
|
|
|
|
| 540 |
result = await client.read_resource("resource://data")
|
| 541 |
assert result[0].text == "Async Hello, world!" # type: ignore[attr-defined]
|
| 542 |
|
| 543 |
+
async def test_resource_decorator_staticmethod_order(self):
|
| 544 |
+
"""Test that both decorator orders work for static methods"""
|
| 545 |
+
mcp = FastMCP()
|
| 546 |
+
|
| 547 |
+
class MyClass:
|
| 548 |
+
@mcp.resource("resource://data") # type: ignore[misc] # Type checker warns but runtime works
|
| 549 |
+
@staticmethod
|
| 550 |
+
def get_data() -> str:
|
| 551 |
+
return "Static Hello, world!"
|
| 552 |
+
|
| 553 |
+
async with Client(mcp) as client:
|
| 554 |
+
result = await client.read_resource("resource://data")
|
| 555 |
+
assert result[0].text == "Static Hello, world!" # type: ignore[attr-defined]
|
| 556 |
+
|
| 557 |
|
| 558 |
class TestTemplateDecorator:
|
| 559 |
async def test_template_decorator(self):
|
|
|
|
| 659 |
mcp = FastMCP()
|
| 660 |
|
| 661 |
class MyClass:
|
|
|
|
| 662 |
@mcp.resource("resource://{name}/data")
|
| 663 |
+
@staticmethod
|
| 664 |
def get_data(name: str) -> str:
|
| 665 |
return f"Static Data for {name}"
|
| 666 |
|
|
|
|
| 833 |
message = result.messages[0]
|
| 834 |
assert message.content.text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
| 835 |
|
| 836 |
+
async def test_prompt_decorator_classmethod_error(self):
|
| 837 |
+
mcp = FastMCP()
|
| 838 |
+
|
| 839 |
+
with pytest.raises(ValueError, match="To decorate a classmethod"):
|
| 840 |
+
|
| 841 |
+
class MyClass:
|
| 842 |
+
@mcp.prompt
|
| 843 |
+
@classmethod
|
| 844 |
+
def test_prompt(cls) -> None:
|
| 845 |
+
pass
|
| 846 |
+
|
| 847 |
async def test_prompt_decorator_staticmethod(self):
|
| 848 |
mcp = FastMCP()
|
| 849 |
|
| 850 |
class MyClass:
|
|
|
|
| 851 |
@mcp.prompt
|
| 852 |
+
@staticmethod
|
| 853 |
def test_prompt() -> str:
|
| 854 |
return "Static Hello, world!"
|
| 855 |
|
|
|
|
| 917 |
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
|
| 918 |
|
| 919 |
# The function should be returned unchanged
|
| 920 |
+
assert isinstance(result_fn, FunctionPrompt)
|
| 921 |
|
| 922 |
# Verify the prompt was registered correctly
|
| 923 |
prompts = await mcp.get_prompts()
|
| 924 |
+
assert prompts["direct_call_prompt"] is result_fn
|
| 925 |
|
| 926 |
# Verify it can be called
|
| 927 |
async with Client(mcp) as client:
|
|
|
|
| 942 |
def my_function() -> str:
|
| 943 |
return "Hello, world!"
|
| 944 |
|
| 945 |
+
async def test_prompt_decorator_staticmethod_order(self):
|
| 946 |
+
"""Test that both decorator orders work for static methods"""
|
| 947 |
+
mcp = FastMCP()
|
| 948 |
+
|
| 949 |
+
class MyClass:
|
| 950 |
+
@mcp.prompt # type: ignore[misc] # Type checker warns but runtime works
|
| 951 |
+
@staticmethod
|
| 952 |
+
def test_prompt() -> str:
|
| 953 |
+
return "Static Hello, world!"
|
| 954 |
+
|
| 955 |
+
async with Client(mcp) as client:
|
| 956 |
+
result = await client.get_prompt("test_prompt")
|
| 957 |
+
assert len(result.messages) == 1
|
| 958 |
+
message = result.messages[0]
|
| 959 |
+
assert message.content.text == "Static Hello, world!" # type: ignore[attr-defined]
|
| 960 |
+
|
| 961 |
|
| 962 |
class TestResourcePrefixHelpers:
|
| 963 |
@pytest.mark.parametrize(
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -936,56 +936,6 @@ class TestResourceTemplates:
|
|
| 936 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 937 |
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
| 938 |
|
| 939 |
-
async def test_stacked_resource_template_decorators(self):
|
| 940 |
-
"""Test that resource template decorators can be stacked."""
|
| 941 |
-
mcp = FastMCP()
|
| 942 |
-
|
| 943 |
-
@mcp.resource("users://email/{email}")
|
| 944 |
-
@mcp.resource("users://name/{name}")
|
| 945 |
-
def lookup_user(name: str | None = None, email: str | None = None) -> dict:
|
| 946 |
-
if name:
|
| 947 |
-
return {
|
| 948 |
-
"id": "123",
|
| 949 |
-
"name": name,
|
| 950 |
-
"email": "dummy@example.com",
|
| 951 |
-
"lookup": "name",
|
| 952 |
-
}
|
| 953 |
-
elif email:
|
| 954 |
-
return {
|
| 955 |
-
"id": "123",
|
| 956 |
-
"name": "Test User",
|
| 957 |
-
"email": email,
|
| 958 |
-
"lookup": "email",
|
| 959 |
-
}
|
| 960 |
-
else:
|
| 961 |
-
raise ValueError("Either name or email must be provided")
|
| 962 |
-
|
| 963 |
-
# Verify both templates are registered
|
| 964 |
-
templates_dict = await mcp.get_resource_templates()
|
| 965 |
-
templates = list(templates_dict.values())
|
| 966 |
-
assert len(templates) == 2
|
| 967 |
-
template_uris = {t.uri_template for t in templates}
|
| 968 |
-
assert "users://email/{email}" in template_uris
|
| 969 |
-
assert "users://name/{name}" in template_uris
|
| 970 |
-
|
| 971 |
-
# Test lookup by email
|
| 972 |
-
async with Client(mcp) as client:
|
| 973 |
-
email_result = await client.read_resource(
|
| 974 |
-
AnyUrl("users://email/user@example.com")
|
| 975 |
-
)
|
| 976 |
-
assert email_result[0].text # type: ignore[attr-defined]
|
| 977 |
-
email_data = json.loads(email_result[0].text) # type: ignore[attr-defined]
|
| 978 |
-
assert email_data["lookup"] == "email"
|
| 979 |
-
assert email_data["email"] == "user@example.com"
|
| 980 |
-
|
| 981 |
-
# Test lookup by name
|
| 982 |
-
name_result = await client.read_resource(AnyUrl("users://name/John"))
|
| 983 |
-
assert name_result[0].text # type: ignore[attr-defined]
|
| 984 |
-
name_data = json.loads(name_result[0].text) # type: ignore[attr-defined]
|
| 985 |
-
assert name_data["lookup"] == "name"
|
| 986 |
-
assert name_data["name"] == "John"
|
| 987 |
-
assert name_data["email"] == "dummy@example.com"
|
| 988 |
-
|
| 989 |
async def test_template_decorator_with_tags(self):
|
| 990 |
mcp = FastMCP()
|
| 991 |
|
|
|
|
| 936 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 937 |
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
| 938 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
async def test_template_decorator_with_tags(self):
|
| 940 |
mcp = FastMCP()
|
| 941 |
|
tests/utilities/test_decorated_function.py
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 1 |
-
import functools
|
| 2 |
-
from collections.abc import Callable
|
| 3 |
-
from typing import Any
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
|
| 7 |
-
from fastmcp.utilities.decorators import DecoratedFunction
|
| 8 |
-
|
| 9 |
-
DECORATOR_CALLED = []
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def decorator(fn: Callable[..., Any]) -> DecoratedFunction[..., Any]:
|
| 13 |
-
@functools.wraps(fn)
|
| 14 |
-
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
| 15 |
-
DECORATOR_CALLED.append((args, kwargs))
|
| 16 |
-
return fn(*args, **kwargs)
|
| 17 |
-
|
| 18 |
-
return DecoratedFunction(wrapper)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
@pytest.fixture(autouse=True)
|
| 22 |
-
def reset_decorator_called():
|
| 23 |
-
DECORATOR_CALLED.clear()
|
| 24 |
-
yield
|
| 25 |
-
DECORATOR_CALLED.clear()
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
@decorator
|
| 29 |
-
def add(a: int, b: int) -> int:
|
| 30 |
-
return a + b
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@decorator
|
| 34 |
-
async def add_async(a: int, b: int) -> int:
|
| 35 |
-
return a + b
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class DecoratedClass:
|
| 39 |
-
def __init__(self, x: int):
|
| 40 |
-
self.x = x
|
| 41 |
-
|
| 42 |
-
@decorator
|
| 43 |
-
def add(self, a: int, b: int) -> int:
|
| 44 |
-
return a + b + self.x
|
| 45 |
-
|
| 46 |
-
@decorator
|
| 47 |
-
async def add_async(self, a: int, b: int) -> int:
|
| 48 |
-
return a + b + self.x
|
| 49 |
-
|
| 50 |
-
@classmethod
|
| 51 |
-
@decorator
|
| 52 |
-
def add_classmethod(cls, a: int, b: int) -> int:
|
| 53 |
-
return a + b
|
| 54 |
-
|
| 55 |
-
@staticmethod
|
| 56 |
-
@decorator
|
| 57 |
-
def add_staticmethod(a: int, b: int) -> int:
|
| 58 |
-
return a + b
|
| 59 |
-
|
| 60 |
-
@classmethod
|
| 61 |
-
@decorator
|
| 62 |
-
async def add_classmethod_async(cls, a: int, b: int) -> int:
|
| 63 |
-
return a + b
|
| 64 |
-
|
| 65 |
-
@staticmethod
|
| 66 |
-
@decorator
|
| 67 |
-
async def add_staticmethod_async(a: int, b: int) -> int:
|
| 68 |
-
return a + b
|
| 69 |
-
|
| 70 |
-
@decorator
|
| 71 |
-
@classmethod
|
| 72 |
-
def add_classmethod_reverse_decorator_order(cls, a: int, b: int) -> int:
|
| 73 |
-
return a + b
|
| 74 |
-
|
| 75 |
-
@decorator
|
| 76 |
-
@staticmethod
|
| 77 |
-
def add_staticmethod_reverse_decorator_order(a: int, b: int) -> int:
|
| 78 |
-
return a + b
|
| 79 |
-
|
| 80 |
-
@decorator
|
| 81 |
-
@classmethod
|
| 82 |
-
async def add_classmethod_async_reverse_decorator_order(cls, a: int, b: int) -> int:
|
| 83 |
-
return a + b
|
| 84 |
-
|
| 85 |
-
@decorator
|
| 86 |
-
@staticmethod
|
| 87 |
-
async def add_staticmethod_async_reverse_decorator_order(a: int, b: int) -> int:
|
| 88 |
-
return a + b
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def test_add():
|
| 92 |
-
assert add(1, 2) == 3
|
| 93 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
| 94 |
-
DECORATOR_CALLED.clear()
|
| 95 |
-
|
| 96 |
-
# Test with keyword arguments
|
| 97 |
-
assert add(a=3, b=4) == 7
|
| 98 |
-
assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
async def test_add_async():
|
| 102 |
-
assert await add_async(1, 2) == 3
|
| 103 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
| 104 |
-
DECORATOR_CALLED.clear()
|
| 105 |
-
|
| 106 |
-
# Test with keyword arguments
|
| 107 |
-
assert await add_async(a=3, b=4) == 7
|
| 108 |
-
assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def test_instance_method():
|
| 112 |
-
obj = DecoratedClass(10)
|
| 113 |
-
assert obj.add(2, 3) == 15
|
| 114 |
-
assert DECORATOR_CALLED == [((obj, 2, 3), {})]
|
| 115 |
-
DECORATOR_CALLED.clear()
|
| 116 |
-
|
| 117 |
-
# Test with keyword arguments
|
| 118 |
-
assert obj.add(a=4, b=5) == 19
|
| 119 |
-
assert DECORATOR_CALLED == [((obj,), {"a": 4, "b": 5})]
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
async def test_instance_method_async():
|
| 123 |
-
obj = DecoratedClass(10)
|
| 124 |
-
assert await obj.add_async(2, 3) == 15
|
| 125 |
-
assert DECORATOR_CALLED == [((obj, 2, 3), {})]
|
| 126 |
-
DECORATOR_CALLED.clear()
|
| 127 |
-
|
| 128 |
-
# Test with keyword arguments
|
| 129 |
-
assert await obj.add_async(a=4, b=5) == 19
|
| 130 |
-
assert DECORATOR_CALLED == [((obj,), {"a": 4, "b": 5})]
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def test_classmethod():
|
| 134 |
-
assert DecoratedClass.add_classmethod(1, 2) == 3
|
| 135 |
-
assert DECORATOR_CALLED == [((DecoratedClass, 1, 2), {})]
|
| 136 |
-
DECORATOR_CALLED.clear()
|
| 137 |
-
|
| 138 |
-
# Test with keyword arguments
|
| 139 |
-
assert DecoratedClass.add_classmethod(a=3, b=4) == 7
|
| 140 |
-
assert DECORATOR_CALLED == [((DecoratedClass,), {"a": 3, "b": 4})]
|
| 141 |
-
DECORATOR_CALLED.clear()
|
| 142 |
-
|
| 143 |
-
# Test via instance
|
| 144 |
-
obj = DecoratedClass(10)
|
| 145 |
-
assert obj.add_classmethod(5, 6) == 11
|
| 146 |
-
assert DECORATOR_CALLED == [((DecoratedClass, 5, 6), {})]
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
async def test_classmethod_async():
|
| 150 |
-
assert await DecoratedClass.add_classmethod_async(1, 2) == 3
|
| 151 |
-
assert DECORATOR_CALLED == [((DecoratedClass, 1, 2), {})]
|
| 152 |
-
DECORATOR_CALLED.clear()
|
| 153 |
-
|
| 154 |
-
# Test with keyword arguments
|
| 155 |
-
assert await DecoratedClass.add_classmethod_async(a=3, b=4) == 7
|
| 156 |
-
assert DECORATOR_CALLED == [((DecoratedClass,), {"a": 3, "b": 4})]
|
| 157 |
-
DECORATOR_CALLED.clear()
|
| 158 |
-
|
| 159 |
-
# Test via instance
|
| 160 |
-
obj = DecoratedClass(10)
|
| 161 |
-
assert await obj.add_classmethod_async(5, 6) == 11
|
| 162 |
-
assert DECORATOR_CALLED == [((DecoratedClass, 5, 6), {})]
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def test_classmethod_wrong_order():
|
| 166 |
-
with pytest.raises(
|
| 167 |
-
TypeError,
|
| 168 |
-
match="To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top.",
|
| 169 |
-
):
|
| 170 |
-
DecoratedClass.add_classmethod_reverse_decorator_order(1, 2)
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
async def test_classmethod_async_wrong_order():
|
| 174 |
-
with pytest.raises(
|
| 175 |
-
TypeError,
|
| 176 |
-
match="To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top.",
|
| 177 |
-
):
|
| 178 |
-
await DecoratedClass.add_classmethod_async_reverse_decorator_order(1, 2)
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def test_staticmethod():
|
| 182 |
-
assert DecoratedClass.add_staticmethod(1, 2) == 3
|
| 183 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
| 184 |
-
DECORATOR_CALLED.clear()
|
| 185 |
-
|
| 186 |
-
# Test with keyword arguments
|
| 187 |
-
assert DecoratedClass.add_staticmethod(a=3, b=4) == 7
|
| 188 |
-
assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
|
| 189 |
-
DECORATOR_CALLED.clear()
|
| 190 |
-
|
| 191 |
-
# Test via instance
|
| 192 |
-
obj = DecoratedClass(10)
|
| 193 |
-
assert obj.add_staticmethod(5, 6) == 11
|
| 194 |
-
assert DECORATOR_CALLED == [((5, 6), {})]
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
async def test_staticmethod_async():
|
| 198 |
-
assert await DecoratedClass.add_staticmethod_async(1, 2) == 3
|
| 199 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
| 200 |
-
DECORATOR_CALLED.clear()
|
| 201 |
-
|
| 202 |
-
# Test with keyword arguments
|
| 203 |
-
assert await DecoratedClass.add_staticmethod_async(a=3, b=4) == 7
|
| 204 |
-
assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
|
| 205 |
-
DECORATOR_CALLED.clear()
|
| 206 |
-
|
| 207 |
-
# Test via instance
|
| 208 |
-
obj = DecoratedClass(10)
|
| 209 |
-
assert await obj.add_staticmethod_async(5, 6) == 11
|
| 210 |
-
assert DECORATOR_CALLED == [((5, 6), {})]
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
def test_staticmethod_wrong_order():
|
| 214 |
-
assert DecoratedClass.add_staticmethod_reverse_decorator_order(1, 2) == 3
|
| 215 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
async def test_staticmethod_async_wrong_order():
|
| 219 |
-
assert (
|
| 220 |
-
await DecoratedClass.add_staticmethod_async_reverse_decorator_order(1, 2) == 3
|
| 221 |
-
)
|
| 222 |
-
assert DECORATOR_CALLED == [((1, 2), {})]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|