Jeremiah Lowin commited on
Commit
b5d856c
·
unverified ·
2 Parent(s): f478715dbd3581

Merge pull request #252 from cutekibry/fix-unsuitable-convert

Browse files
src/fastmcp/utilities/func_metadata.py CHANGED
@@ -7,7 +7,15 @@ from typing import (
7
  ForwardRef,
8
  )
9
 
10
- from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
 
 
 
 
 
 
 
 
11
  from pydantic._internal._typing_extra import eval_type_backport
12
  from pydantic.fields import FieldInfo
13
  from pydantic_core import PydanticUndefined
@@ -80,14 +88,18 @@ class FuncMetadata(BaseModel):
80
  dicts (JSON objects) as JSON strings, which can be pre-parsed here.
81
  """
82
  new_data = data.copy() # Shallow copy
83
- for field_name, _field_info in self.arg_model.model_fields.items():
84
  if field_name not in data.keys():
85
  continue
86
  if isinstance(data[field_name], str):
87
  try:
88
  pre_parsed = json.loads(data[field_name])
89
- except json.JSONDecodeError:
90
- continue # Not JSON - skip
 
 
 
 
91
  if isinstance(pre_parsed, str | int | float):
92
  # This is likely that the raw value is e.g. `"hello"` which we
93
  # Should really be parsed as '"hello"' in Python - but if we parse
 
7
  ForwardRef,
8
  )
9
 
10
+ from pydantic import (
11
+ BaseModel,
12
+ ConfigDict,
13
+ Field,
14
+ TypeAdapter,
15
+ ValidationError,
16
+ WithJsonSchema,
17
+ create_model,
18
+ )
19
  from pydantic._internal._typing_extra import eval_type_backport
20
  from pydantic.fields import FieldInfo
21
  from pydantic_core import PydanticUndefined
 
88
  dicts (JSON objects) as JSON strings, which can be pre-parsed here.
89
  """
90
  new_data = data.copy() # Shallow copy
91
+ for field_name, field_info in self.arg_model.model_fields.items():
92
  if field_name not in data.keys():
93
  continue
94
  if isinstance(data[field_name], str):
95
  try:
96
  pre_parsed = json.loads(data[field_name])
97
+
98
+ # Check if the pre_parsed value is valid for the field
99
+ validator = TypeAdapter(field_info.annotation)
100
+ validator.validate_python(pre_parsed)
101
+ except (json.JSONDecodeError, ValidationError):
102
+ continue # Not JSON or invalid for the field
103
  if isinstance(pre_parsed, str | int | float):
104
  # This is likely that the raw value is e.g. `"hello"` which we
105
  # Should really be parsed as '"hello"' in Python - but if we parse
tests/utilities/test_func_metadata.py CHANGED
@@ -174,6 +174,74 @@ def test_str_vs_list_str():
174
  assert result["str_or_list"] == ["hello", "world"]
175
 
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  def test_skip_names():
178
  """Test that skipped parameters are not included in the model"""
179
 
 
174
  assert result["str_or_list"] == ["hello", "world"]
175
 
176
 
177
+ def test_keep_str_as_str():
178
+ """Test that string arguments are kept as strings"""
179
+
180
+ def func_with_str_types(string: str):
181
+ return string
182
+
183
+ meta = func_metadata(func_with_str_types)
184
+ result = meta.pre_parse_json(
185
+ {"string": "{'nice to meet you': 'hello', 'goodbye': 5}"}
186
+ )
187
+ assert result["string"] == "{'nice to meet you': 'hello', 'goodbye': 5}"
188
+
189
+
190
+ def test_missing_annotation():
191
+ """Test that missing annotations don't cause errors"""
192
+
193
+ def fn(x, y):
194
+ return x + y
195
+
196
+ meta = func_metadata(fn)
197
+ result = meta.pre_parse_json({"x": "1", "y": "2"})
198
+ assert result["x"] == "1"
199
+ assert result["y"] == "2"
200
+
201
+
202
+ def test_keep_str_union_as_str():
203
+ """Test that string arguments are kept as strings"""
204
+
205
+ def func_with_str_types(string: str | dict[int, str] | None):
206
+ return string
207
+
208
+ meta = func_metadata(func_with_str_types)
209
+ result = meta.pre_parse_json(
210
+ {"string": "{'nice to meet you': 'hello', 'goodbye': 5}"}
211
+ )
212
+ assert result["string"] == "{'nice to meet you': 'hello', 'goodbye': 5}"
213
+
214
+
215
+ def test_keep_str_complex_type_as_str():
216
+ """Test that string arguments are kept as strings because it's invalid for the field"""
217
+
218
+ class SomeModel(BaseModel):
219
+ x: int
220
+ y: dict[int, str]
221
+
222
+ def func_with_str_types(string: str | SomeModel | None):
223
+ return string
224
+
225
+ meta = func_metadata(func_with_str_types)
226
+ result = meta.pre_parse_json({"string": '{"x": 1, "y": {"invalid": "hello"}}'})
227
+ assert result["string"] == '{"x": 1, "y": {"invalid": "hello"}}'
228
+
229
+
230
+ def test_convert_str_to_complex_type():
231
+ """Test that string arguments are converted to the complex type because it's valid for the field"""
232
+
233
+ class SomeModel(BaseModel):
234
+ x: int
235
+ y: dict[int, str]
236
+
237
+ def func_with_str_types(string: str | SomeModel | None):
238
+ return string
239
+
240
+ meta = func_metadata(func_with_str_types)
241
+ result = meta.pre_parse_json({"string": '{"x": 1, "y": {"1": "hello"}}'})
242
+ assert result["string"] == {"x": 1, "y": {"1": "hello"}}
243
+
244
+
245
  def test_skip_names():
246
  """Test that skipped parameters are not included in the model"""
247