Skip to content

Commit 2dd7ca6

Browse files
committed
Fixed NO BODY methods with Model
1 parent 8868db5 commit 2dd7ca6

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

fastopenapi/core/router.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ def __init__(
8181

8282
def add_route(self, path: str, method: str, endpoint: Callable):
8383
"""Add a route to the router"""
84-
meta = getattr(endpoint, "__route_meta__", {})
84+
try:
85+
if not hasattr(endpoint, "__route_meta__"):
86+
endpoint.__route_meta__ = {}
87+
endpoint.__route_meta__.setdefault("method", method)
88+
except AttributeError:
89+
pass
90+
meta = getattr(endpoint, "__route_meta__", {"method": method})
8591
route = RouteInfo(path, method, endpoint, meta)
8692
self._routes.append(route)
8793
self._openapi_schema = None
@@ -137,6 +143,7 @@ def _create_route_decorator(self, path: str, method: str, meta: dict):
137143
"""Create a decorator for route registration"""
138144

139145
def decorator(func: Callable):
146+
meta["method"] = method
140147
func.__route_meta__ = meta
141148
self.add_route(path, method, func)
142149
return func

tests/core/test_base_router.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,54 @@ def options_endpoint():
254254
assert hasattr(get_endpoint, "__route_meta__")
255255
assert get_endpoint.__route_meta__["tags"] == ["test"]
256256

257+
def test_decorator_writes_method_to_route_meta(self):
258+
"""Test that decorators write HTTP method into __route_meta__"""
259+
260+
@self.router.get("/get-test")
261+
def get_ep():
262+
pass
263+
264+
@self.router.post("/post-test")
265+
def post_ep():
266+
pass
267+
268+
@self.router.delete("/delete-test")
269+
def delete_ep():
270+
pass
271+
272+
@self.router.head("/head-test")
273+
def head_ep():
274+
pass
275+
276+
assert get_ep.__route_meta__["method"] == "GET"
277+
assert post_ep.__route_meta__["method"] == "POST"
278+
assert delete_ep.__route_meta__["method"] == "DELETE"
279+
assert head_ep.__route_meta__["method"] == "HEAD"
280+
281+
def test_add_route_sets_route_meta(self):
282+
"""Test that add_route sets __route_meta__ on bare functions"""
283+
284+
def bare_func():
285+
pass
286+
287+
assert not hasattr(bare_func, "__route_meta__")
288+
self.router.add_route("/bare", "GET", bare_func)
289+
290+
assert hasattr(bare_func, "__route_meta__")
291+
assert bare_func.__route_meta__["method"] == "GET"
292+
293+
def test_add_route_preserves_existing_route_meta(self):
294+
"""Test that add_route doesn't overwrite existing method in __route_meta__"""
295+
296+
def func_with_meta():
297+
pass
298+
299+
func_with_meta.__route_meta__ = {"method": "POST", "tags": ["custom"]}
300+
self.router.add_route("/meta", "POST", func_with_meta)
301+
302+
assert func_with_meta.__route_meta__["method"] == "POST"
303+
assert func_with_meta.__route_meta__["tags"] == ["custom"]
304+
257305
def test_openapi_property_lazy_loading(self):
258306
# Test the openapi property (lazy loading)
259307

tests/resolution/test_parameter_resolver.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,53 @@ class Model(BaseModel):
320320
source = ParameterResolver._determine_source("data", param, {})
321321
assert source == ParameterSource.BODY
322322

323+
@pytest.mark.parametrize("method", ["GET", "HEAD", "DELETE"])
324+
def test_determine_source_pydantic_model_no_body_methods(self, method) -> None:
325+
"""Test that Pydantic models resolve as QUERY for NO_BODY methods"""
326+
327+
class FilterModel(BaseModel):
328+
search: str
329+
limit: int = 10
330+
331+
param = inspect.Parameter(
332+
"filters",
333+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
334+
annotation=FilterModel,
335+
)
336+
source = ParameterResolver._determine_source(
337+
"filters", param, {}, method=method
338+
)
339+
assert source == ParameterSource.QUERY
340+
341+
@pytest.mark.parametrize("method", ["POST", "PUT", "PATCH"])
342+
def test_determine_source_pydantic_model_body_methods(self, method) -> None:
343+
"""Test that Pydantic models resolve as BODY for body-carrying methods"""
344+
345+
class DataModel(BaseModel):
346+
name: str
347+
348+
param = inspect.Parameter(
349+
"data",
350+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
351+
annotation=DataModel,
352+
)
353+
source = ParameterResolver._determine_source("data", param, {}, method=method)
354+
assert source == ParameterSource.BODY
355+
356+
def test_determine_source_pydantic_model_no_method(self) -> None:
357+
"""Test that Pydantic models resolve as BODY when method is None"""
358+
359+
class DataModel(BaseModel):
360+
name: str
361+
362+
param = inspect.Parameter(
363+
"data",
364+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
365+
annotation=DataModel,
366+
)
367+
source = ParameterResolver._determine_source("data", param, {}, method=None)
368+
assert source == ParameterSource.BODY
369+
323370
def test_determine_source_query_default(self) -> None:
324371
"""Test source determination defaults to query"""
325372
param = inspect.Parameter(

0 commit comments

Comments
 (0)