Jeremiah Lowin commited on
Commit
b581701
·
unverified ·
2 Parent(s): 834521b411d72c

Merge pull request #727 from jlowin/decorators

Browse files
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
- **Don't do this** (it doesn't work properly):
 
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 add the bound methods
57
  obj = MyClass()
58
- mcp.add_tool(obj.add)
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.call_tool('add', {'x': 1, 'y': 2}) # Returns 3
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
- Similar to instance methods, decorating class methods directly doesn't work properly:
76
 
77
- **Don't do this**:
 
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 correctly
87
- def from_string(cls, s):
 
 
 
 
 
88
  return cls(s)
89
  ```
 
90
 
91
- The problem here is that the FastMCP decorator is applied before the `@classmethod` decorator (Python applies decorators bottom-to-top). So it captures the function before it's transformed into a class method, leading to incorrect behavior.
 
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
- # Add the class method after the class is defined
106
- mcp.add_tool(MyClass.from_string)
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
- Unlike instance and class methods, static methods work fine with FastMCP decorators:
 
 
 
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 approach works because:
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
- Alternatively, you can use the same pattern as the other methods:
 
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.add_tool(MyClass.utility)
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.add_tool(self.tool_method)
173
- mcp_instance.add_resource_fn(self.resource_method, uri="resource://data")
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
- While FastMCP's decorator pattern works seamlessly with regular functions and static methods, for instance methods and class methods, you should add them after creating the instance or class. This ensures that the methods are properly bound before being registered.
 
 
 
 
 
 
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], AnyFunction] | 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 callable(name_or_fn):
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 function
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 fn
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], 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 callable(uri):
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) -> 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], AnyFunction] | 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 callable(name_or_fn):
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
- # If name is provided, this is a direct call, return original function for consistency with tools
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 is standalone_function
330
 
331
  # Verify the tool was registered correctly
332
  tools = await mcp.get_tools()
333
- assert "direct_call_tool" in tools
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 is standalone_function
861
 
862
  # Verify the prompt was registered correctly
863
  prompts = await mcp.get_prompts()
864
- assert "direct_call_prompt" in prompts
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), {})]