mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-29 09:08:25 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c516c9904b | ||
|
|
b49c05ec22 | ||
|
|
015b4fae9c | ||
|
|
eead41bf4c | ||
|
|
0f613d9051 |
@@ -7,6 +7,13 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.123.3
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix Query\Header\Cookie parameter model alias. PR [#14360](https://github.com/fastapi/fastapi/pull/14360) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
## 0.123.2
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.123.2"
|
||||
__version__ = "0.123.3"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -371,6 +371,13 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation
|
||||
if origin_type is Union: # Handle optional sequences
|
||||
union_args = get_args(field.field_info.annotation)
|
||||
for union_arg in union_args:
|
||||
if union_arg is type(None):
|
||||
continue
|
||||
origin_type = get_origin(union_arg) or union_arg
|
||||
break
|
||||
assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type]
|
||||
return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@@ -787,9 +787,8 @@ def request_params_to_args(
|
||||
)
|
||||
value = _get_multidict_value(field, received_params, alias=alias)
|
||||
if value is not None:
|
||||
params_to_process[field.name] = value
|
||||
params_to_process[field.alias] = value
|
||||
processed_keys.add(alias or field.alias)
|
||||
processed_keys.add(field.name)
|
||||
|
||||
for key in received_params.keys():
|
||||
if key not in processed_keys:
|
||||
|
||||
@@ -136,6 +136,30 @@ def test_is_uploadfile_sequence_annotation():
|
||||
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_serialize_sequence_value_with_optional_list():
|
||||
"""Test that serialize_sequence_value handles optional lists correctly."""
|
||||
from fastapi._compat import v2
|
||||
|
||||
field_info = FieldInfo(annotation=Union[List[str], None])
|
||||
field = v2.ModelField(name="items", field_info=field_info)
|
||||
result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"])
|
||||
assert result == ["a", "b", "c"]
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_serialize_sequence_value_with_none_first_in_union():
|
||||
"""Test that serialize_sequence_value handles Union[None, List[...]] correctly."""
|
||||
from fastapi._compat import v2
|
||||
|
||||
field_info = FieldInfo(annotation=Union[None, List[str]])
|
||||
field = v2.ModelField(name="items", field_info=field_info)
|
||||
result = v2.serialize_sequence_value(field=field, value=["x", "y"])
|
||||
assert result == ["x", "y"]
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@needs_py_lt_314
|
||||
def test_is_pv1_scalar_field():
|
||||
from fastapi._compat import v1
|
||||
|
||||
30
tests/test_optional_file_list.py
Normal file
30
tests/test_optional_file_list.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import FastAPI, File
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files")
|
||||
async def upload_files(files: Optional[List[bytes]] = File(None)):
|
||||
if files is None:
|
||||
return {"files_count": 0}
|
||||
return {"files_count": len(files), "sizes": [len(f) for f in files]}
|
||||
|
||||
|
||||
def test_optional_bytes_list():
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/files",
|
||||
files=[("files", b"content1"), ("files", b"content2")],
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"files_count": 2, "sizes": [8, 8]}
|
||||
|
||||
|
||||
def test_optional_bytes_list_no_files():
|
||||
client = TestClient(app)
|
||||
response = client.post("/files")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"files_count": 0}
|
||||
76
tests/test_request_param_model_by_alias.py
Normal file
76
tests/test_request_param_model_by_alias.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from dirty_equals import IsPartialDict
|
||||
from fastapi import Cookie, FastAPI, Header, Query
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
param: str = Field(alias="param_alias")
|
||||
|
||||
|
||||
@app.get("/query")
|
||||
async def query_model(data: Model = Query()):
|
||||
return {"param": data.param}
|
||||
|
||||
|
||||
@app.get("/header")
|
||||
async def header_model(data: Model = Header()):
|
||||
return {"param": data.param}
|
||||
|
||||
|
||||
@app.get("/cookie")
|
||||
async def cookie_model(data: Model = Cookie()):
|
||||
return {"param": data.param}
|
||||
|
||||
|
||||
def test_query_model_with_alias():
|
||||
client = TestClient(app)
|
||||
response = client.get("/query", params={"param_alias": "value"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"param": "value"}
|
||||
|
||||
|
||||
def test_header_model_with_alias():
|
||||
client = TestClient(app)
|
||||
response = client.get("/header", headers={"param_alias": "value"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"param": "value"}
|
||||
|
||||
|
||||
def test_cookie_model_with_alias():
|
||||
client = TestClient(app)
|
||||
client.cookies.set("param_alias", "value")
|
||||
response = client.get("/cookie")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"param": "value"}
|
||||
|
||||
|
||||
def test_query_model_with_alias_by_name():
|
||||
client = TestClient(app)
|
||||
response = client.get("/query", params={"param": "value"})
|
||||
assert response.status_code == 422, response.text
|
||||
details = response.json()
|
||||
if PYDANTIC_V2:
|
||||
assert details["detail"][0]["input"] == {"param": "value"}
|
||||
|
||||
|
||||
def test_header_model_with_alias_by_name():
|
||||
client = TestClient(app)
|
||||
response = client.get("/header", headers={"param": "value"})
|
||||
assert response.status_code == 422, response.text
|
||||
details = response.json()
|
||||
if PYDANTIC_V2:
|
||||
assert details["detail"][0]["input"] == IsPartialDict({"param": "value"})
|
||||
|
||||
|
||||
def test_cookie_model_with_alias_by_name():
|
||||
client = TestClient(app)
|
||||
client.cookies.set("param", "value")
|
||||
response = client.get("/cookie")
|
||||
assert response.status_code == 422, response.text
|
||||
details = response.json()
|
||||
if PYDANTIC_V2:
|
||||
assert details["detail"][0]["input"] == {"param": "value"}
|
||||
Reference in New Issue
Block a user