mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 08:10:57 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4976568fc7 | ||
|
|
fb30cc2f50 | ||
|
|
f95a174288 | ||
|
|
5126e099bd | ||
|
|
dcf0299195 | ||
|
|
c516c9904b | ||
|
|
b49c05ec22 | ||
|
|
015b4fae9c | ||
|
|
eead41bf4c | ||
|
|
0f613d9051 |
@@ -443,6 +443,14 @@ The docs UI will interact with the server that you select.
|
||||
|
||||
///
|
||||
|
||||
/// note | Technical Details
|
||||
|
||||
The `servers` property in the OpenAPI specification is optional.
|
||||
|
||||
If you don't specify the `servers` parameter and `root_path` is equal to `/`, the `servers` property in the generated OpenAPI schema will be omitted entirely by default, which is the equivalent of a single server with a `url` value of `/`.
|
||||
|
||||
///
|
||||
|
||||
### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path }
|
||||
|
||||
If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:
|
||||
|
||||
@@ -7,6 +7,23 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.123.4
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
## 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.4"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -180,8 +180,13 @@ def get_schema_from_model_field(
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
||||
"computed_fields", []
|
||||
)
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None if separate_input_output_schemas else "validation"
|
||||
None
|
||||
if (separate_input_output_schemas or len(computed_fields) > 0)
|
||||
else "validation"
|
||||
)
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
json_schema = field_mapping[(field, override_mode or field.mode)]
|
||||
@@ -203,9 +208,14 @@ def get_definitions(
|
||||
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
has_computed_fields: bool = any(
|
||||
field._type_adapter.core_schema.get("schema", {}).get("computed_fields", [])
|
||||
for field in fields
|
||||
)
|
||||
|
||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None if separate_input_output_schemas else "validation"
|
||||
None if (separate_input_output_schemas or has_computed_fields) else "validation"
|
||||
)
|
||||
validation_fields = [field for field in fields if field.mode == "validation"]
|
||||
serialization_fields = [field for field in fields if field.mode == "serialization"]
|
||||
@@ -371,6 +381,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]
|
||||
|
||||
|
||||
@@ -301,7 +301,12 @@ class FastAPI(Starlette):
|
||||
browser tabs open). Or if you want to leave fixed the possible URLs.
|
||||
|
||||
If the servers `list` is not provided, or is an empty `list`, the
|
||||
default value would be a `dict` with a `url` value of `/`.
|
||||
`servers` property in the generated OpenAPI will be:
|
||||
|
||||
* a `dict` with a `url` value of the application's mounting point
|
||||
(`root_path`) if it's different from `/`.
|
||||
* otherwise, the `servers` property will be omitted from the OpenAPI
|
||||
schema.
|
||||
|
||||
Each item in the `list` is a `dict` containing:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,8 +6,9 @@ from .utils import needs_pydanticv2
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
app = FastAPI()
|
||||
def get_client(request):
|
||||
separate_input_output_schemas = request.param
|
||||
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
|
||||
|
||||
from pydantic import BaseModel, computed_field
|
||||
|
||||
@@ -32,6 +33,7 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.parametrize("client", [True, False], indirect=True)
|
||||
@pytest.mark.parametrize("path", ["/", "/responses"])
|
||||
@needs_pydanticv2
|
||||
def test_get(client: TestClient, path: str):
|
||||
@@ -40,6 +42,7 @@ def test_get(client: TestClient, path: str):
|
||||
assert response.json() == {"width": 3, "length": 4, "area": 12}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("client", [True, False], indirect=True)
|
||||
@needs_pydanticv2
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
|
||||
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