Skip to content

Commit 114d3be

Browse files
committed
Multiple use of forms: fastapi/fastapi#2387 (comment)
1 parent 8455f66 commit 114d3be

File tree

4 files changed

+73
-29
lines changed

4 files changed

+73
-29
lines changed

examples/main.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from fastapi import FastAPI, Depends
1+
from fastapi import FastAPI
22
from fastapi import File
33
from fastapi import UploadFile
44

55
from examples.models import PostContractJSONSchema, PostContractBodySchema
6+
from examples.models import PostContractSmallDoubleBodySchema
67
from pyfa_converter import FormBody
78

89
app = FastAPI()
@@ -20,7 +21,7 @@ async def example_json_body_handler(
2021

2122
@app.post("/form-data-body")
2223
async def example_foo_body_handler(
23-
data: PostContractBodySchema = FormBody(),
24+
data: PostContractBodySchema = FormBody(PostContractBodySchema),
2425
document: UploadFile = File(...),
2526
):
2627
return {
@@ -32,11 +33,18 @@ async def example_foo_body_handler(
3233

3334
@app.post("/form-data-body-two")
3435
async def example_foo_body_handler(
35-
data: PostContractBodySchema = FormBody(),
36+
data: PostContractBodySchema = FormBody(PostContractBodySchema),
3637
document: UploadFile = File(...),
3738
):
3839
return {
3940
"title": data.title,
4041
"date": data.date,
4142
"file_name": document.filename
42-
}
43+
}
44+
45+
46+
@app.post("/test")
47+
async def foo(
48+
data: PostContractSmallDoubleBodySchema = FormBody(PostContractSmallDoubleBodySchema),
49+
):
50+
return {'bar': 'bar'}

examples/models.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ class PostContractSmallBodySchema(PostContractSmallJSONSchema):
4040

4141

4242
@PydanticConverter.body
43-
class PostContractSmallDoubleBodySchema(PostContractSmallJSONSchema):
44-
pass
43+
class PostContractSmallDoubleBodySchema(BaseModel):
44+
id: Optional[int] = Field(None, description='gwa')
45+
title: Optional[str] = Field(None)

pyfa_converter/depends.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from typing import Type
12
from fastapi import Depends
3+
from pydantic import BaseModel
4+
25
from pyfa_converter import PydanticConverter
36

47

58
class FormBody:
6-
def __new__(cls):
7-
return Depends(PydanticConverter.body)
9+
def __new__(cls, model_type: Type[BaseModel | PydanticConverter]):
10+
return Depends(model_type.body)

pyfa_converter/utils.py

+53-21
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from typing import Type
44

55
from fastapi import Body
6+
from fastapi import Depends
67
from fastapi import Form
78
from pydantic import BaseModel
89
from pydantic.fields import ModelField
910

1011

11-
class PydanticConverter:
12-
12+
class PydanticConverterUtils:
1313
@classmethod
1414
def form(cls, field: ModelField) -> Body:
1515
if field.required is True:
@@ -32,28 +32,60 @@ def __form(cls, model_field: ModelField, default: Any):
3232
regex=model_field.field_info.regex or None,
3333
)
3434

35-
@classmethod
36-
def body(cls, parent_cls: Type[BaseModel]):
37-
new_parameters = []
38-
39-
for field_name, model_field in parent_cls.__fields__.items():
40-
model_field: ModelField
41-
42-
new_parameters.append(
43-
inspect.Parameter(
44-
field_name,
45-
inspect.Parameter.POSITIONAL_ONLY,
46-
default=cls.form(field=model_field),
47-
annotation=model_field.outer_type_,
35+
36+
class PydanticConverter:
37+
38+
@staticmethod
39+
def body(cls: Type[BaseModel]) -> Type[BaseModel]:
40+
"""
41+
Adds an `body` class method to decorated models. The `body` class
42+
method can be used with `FastAPI` endpoints.
43+
44+
Args:
45+
cls: The model class to decorate.
46+
47+
Returns:
48+
The decorated class.
49+
"""
50+
51+
def make_form_parameter(field: ModelField) -> Any:
52+
"""
53+
Converts a field from a `Pydantic` model to the appropriate `FastAPI`
54+
parameter type.
55+
56+
Args:
57+
field: The field to convert.
58+
59+
Returns:
60+
Either the result of `Form`, if the field is not a sub-model, or
61+
the result of `Depends` if it is.
62+
63+
"""
64+
if issubclass(field.type_, BaseModel):
65+
# This is a sub-model.
66+
assert hasattr(field.type_, "body"), (
67+
f"Sub-model class for {field.name} field must be decorated with"
68+
f" `as_form` too."
4869
)
70+
return Depends(field.type_.body) # noqa
71+
else:
72+
return PydanticConverterUtils.form(field=field)
73+
74+
new_params = [
75+
inspect.Parameter(
76+
field.alias,
77+
inspect.Parameter.POSITIONAL_ONLY,
78+
default=make_form_parameter(field),
4979
)
80+
for field in cls.__fields__.values()
81+
]
5082

51-
def as_form_func(*args, **kwargs):
52-
return parent_cls(*args, **kwargs) # noqa
83+
async def _as_form(**data):
84+
return cls(**data)
5385

54-
sig = inspect.signature(as_form_func)
55-
sig = sig.replace(parameters=new_parameters)
56-
as_form_func.__signature__ = sig
57-
setattr(cls, 'body', as_form_func)
86+
sig = inspect.signature(_as_form)
87+
sig = sig.replace(parameters=new_params)
88+
_as_form.__signature__ = sig
89+
setattr(cls, "body", _as_form)
5890
return cls
5991

0 commit comments

Comments
 (0)