mirror of https://github.com/tiangolo/fastapi.git
Moved conversion logic to `_extract_form_body`, added test to cover case with dependency
This commit is contained in:
parent
689c11b535
commit
fcdad3a183
|
|
@ -54,6 +54,7 @@ from fastapi.concurrency import (
|
||||||
asynccontextmanager,
|
asynccontextmanager,
|
||||||
contextmanager_in_threadpool,
|
contextmanager_in_threadpool,
|
||||||
)
|
)
|
||||||
|
from fastapi.datastructures import UploadFile as FastAPIUploadFile
|
||||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||||
from fastapi.exceptions import DependencyScopeError
|
from fastapi.exceptions import DependencyScopeError
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
@ -875,17 +876,19 @@ async def _extract_form_body(
|
||||||
for field in body_fields:
|
for field in body_fields:
|
||||||
value = _get_multidict_value(field, received_body)
|
value = _get_multidict_value(field, received_body)
|
||||||
field_info = field.field_info
|
field_info = field.field_info
|
||||||
if (
|
if ( # fmt: skip
|
||||||
isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
|
isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
|
||||||
and is_bytes_field(field)
|
|
||||||
and isinstance(value, UploadFile)
|
and isinstance(value, UploadFile)
|
||||||
):
|
):
|
||||||
|
if is_bytes_field(field):
|
||||||
value = await value.read()
|
value = await value.read()
|
||||||
elif (
|
else:
|
||||||
is_bytes_sequence_field(field)
|
value = FastAPIUploadFile.from_starlette(value)
|
||||||
and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
|
elif ( # fmt: skip
|
||||||
|
isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
|
||||||
and value_is_sequence(value)
|
and value_is_sequence(value)
|
||||||
):
|
):
|
||||||
|
if is_bytes_sequence_field(field):
|
||||||
# For types
|
# For types
|
||||||
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
||||||
results: List[Union[bytes, str]] = []
|
results: List[Union[bytes, str]] = []
|
||||||
|
|
@ -900,6 +903,12 @@ async def _extract_form_body(
|
||||||
for sub_value in value:
|
for sub_value in value:
|
||||||
tg.start_soon(process_fn, sub_value.read)
|
tg.start_soon(process_fn, sub_value.read)
|
||||||
value = serialize_sequence_value(field=field, value=results)
|
value = serialize_sequence_value(field=field, value=results)
|
||||||
|
else:
|
||||||
|
value = [
|
||||||
|
FastAPIUploadFile.from_starlette(sub_value)
|
||||||
|
for sub_value in value
|
||||||
|
if isinstance(sub_value, UploadFile)
|
||||||
|
]
|
||||||
if value is not None:
|
if value is not None:
|
||||||
values[field.alias] = value
|
values[field.alias] = value
|
||||||
for key, value in received_body.items():
|
for key, value in received_body.items():
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ from fastapi._compat import (
|
||||||
_normalize_errors,
|
_normalize_errors,
|
||||||
lenient_issubclass,
|
lenient_issubclass,
|
||||||
)
|
)
|
||||||
from fastapi.datastructures import Default, DefaultPlaceholder, UploadFile
|
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||||
from fastapi.dependencies.models import Dependant
|
from fastapi.dependencies.models import Dependant
|
||||||
from fastapi.dependencies.utils import (
|
from fastapi.dependencies.utils import (
|
||||||
_should_embed_body_fields,
|
_should_embed_body_fields,
|
||||||
|
|
@ -65,7 +65,6 @@ from starlette import routing
|
||||||
from starlette._exception_handler import wrap_app_handling_exceptions
|
from starlette._exception_handler import wrap_app_handling_exceptions
|
||||||
from starlette._utils import is_async_callable
|
from starlette._utils import is_async_callable
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
from starlette.datastructures import UploadFile as StarletteUploadFile
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import JSONResponse, Response
|
from starlette.responses import JSONResponse, Response
|
||||||
|
|
@ -287,19 +286,6 @@ async def run_endpoint_function(
|
||||||
# facilitate profiling endpoints, since inner functions are harder to profile.
|
# facilitate profiling endpoints, since inner functions are harder to profile.
|
||||||
assert dependant.call is not None, "dependant.call must be a function"
|
assert dependant.call is not None, "dependant.call must be a function"
|
||||||
|
|
||||||
# Convert all Starlette UploadFiles to FastAPI UploadFiles
|
|
||||||
for key, value in values.items():
|
|
||||||
if isinstance(value, StarletteUploadFile) and not isinstance(value, UploadFile):
|
|
||||||
values[key] = UploadFile.from_starlette(value)
|
|
||||||
elif isinstance(value, list):
|
|
||||||
values[key] = [
|
|
||||||
UploadFile.from_starlette(item)
|
|
||||||
if isinstance(item, StarletteUploadFile)
|
|
||||||
and not isinstance(item, UploadFile)
|
|
||||||
else item
|
|
||||||
for item in value
|
|
||||||
]
|
|
||||||
|
|
||||||
if is_coroutine:
|
if is_coroutine:
|
||||||
return await dependant.call(**values)
|
return await dependant.call(**values)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import io
|
import io
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from fastapi import FastAPI, File, UploadFile
|
import pytest
|
||||||
|
from fastapi import Depends, FastAPI, File, UploadFile
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from starlette.datastructures import UploadFile as StarletteUploadFile
|
from starlette.datastructures import UploadFile as StarletteUploadFile
|
||||||
|
|
||||||
|
|
@ -33,10 +34,48 @@ async def uploadfiles(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_uploadfile_type() -> None:
|
async def get_uploadfile_info(uploadfile: UploadFile = File(...)) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"filename": uploadfile.filename,
|
||||||
|
"is_fastapi_uploadfile": isinstance(uploadfile, UploadFile),
|
||||||
|
"is_starlette_uploadfile": isinstance(uploadfile, StarletteUploadFile),
|
||||||
|
"class": f"{uploadfile.__class__.__module__}.{uploadfile.__class__.__name__}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/uploadfile-dep")
|
||||||
|
async def uploadfile_dep(
|
||||||
|
uploadfile_info: Dict[str, Any] = Depends(get_uploadfile_info),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return uploadfile_info
|
||||||
|
|
||||||
|
|
||||||
|
async def get_uploadfiles_info(
|
||||||
|
uploadfiles: List[UploadFile] = File(...),
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"filename": uploadfile.filename,
|
||||||
|
"is_fastapi_uploadfile": isinstance(uploadfile, UploadFile),
|
||||||
|
"is_starlette_uploadfile": isinstance(uploadfile, StarletteUploadFile),
|
||||||
|
"class": f"{uploadfile.__class__.__module__}.{uploadfile.__class__.__name__}",
|
||||||
|
}
|
||||||
|
for uploadfile in uploadfiles
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/uploadfiles-dep")
|
||||||
|
async def uploadfiles_dep(
|
||||||
|
uploadfiles_info: List[Dict[str, Any]] = Depends(get_uploadfiles_info),
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
return uploadfiles_info
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("endpoint", ["/uploadfile", "/uploadfile-dep"])
|
||||||
|
def test_uploadfile_type(endpoint: str) -> None:
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
files = {"uploadfile": ("example.txt", io.BytesIO(b"test content"), "text/plain")}
|
files = {"uploadfile": ("example.txt", io.BytesIO(b"test content"), "text/plain")}
|
||||||
response = client.post("/uploadfile/", files=files)
|
response = client.post(f"{endpoint}", files=files)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
assert data["filename"] == "example.txt"
|
assert data["filename"] == "example.txt"
|
||||||
|
|
@ -45,19 +84,26 @@ def test_uploadfile_type() -> None:
|
||||||
assert data["class"].startswith("fastapi.")
|
assert data["class"].startswith("fastapi.")
|
||||||
|
|
||||||
|
|
||||||
def test_uploadfiles_type() -> None:
|
@pytest.mark.parametrize("endpoint", ["/uploadfiles", "/uploadfiles-dep"])
|
||||||
|
def test_uploadfiles_type(endpoint: str) -> None:
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
files = [
|
files = [
|
||||||
("uploadfiles", ("example.txt", io.BytesIO(b"test content"), "text/plain"))
|
("uploadfiles", ("example.txt", io.BytesIO(b"test content"), "text/plain")),
|
||||||
|
("uploadfiles", ("example2.txt", io.BytesIO(b"test content"), "text/plain")),
|
||||||
]
|
]
|
||||||
response = client.post("/uploadfiles/", files=files)
|
response = client.post(f"{endpoint}", files=files)
|
||||||
files_data = response.json()
|
files_data = response.json()
|
||||||
|
|
||||||
assert len(files_data) == 1
|
assert len(files_data) == 2
|
||||||
|
|
||||||
data = files_data[0]
|
file1 = files_data[0]
|
||||||
|
assert file1["filename"] == "example.txt"
|
||||||
|
assert file1["is_fastapi_uploadfile"] is True
|
||||||
|
assert file1["is_starlette_uploadfile"] is True
|
||||||
|
assert file1["class"].startswith("fastapi.")
|
||||||
|
|
||||||
assert data["filename"] == "example.txt"
|
file2 = files_data[1]
|
||||||
assert data["is_fastapi_uploadfile"] is True
|
assert file2["filename"] == "example2.txt"
|
||||||
assert data["is_starlette_uploadfile"] is True
|
assert file2["is_fastapi_uploadfile"] is True
|
||||||
assert data["class"].startswith("fastapi.")
|
assert file2["is_starlette_uploadfile"] is True
|
||||||
|
assert file2["class"].startswith("fastapi.")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue