Browse Source

Merge remote-tracking branch 'upstream/master' into sync-german-docs-2025-11-19

pull/14367/head
Yurii Motov 8 months ago
parent
commit
c987747e6a
  1. 2
      README.md
  2. 8
      docs/en/docs/advanced/behind-a-proxy.md
  3. 2
      docs/en/docs/index.md
  4. 38
      docs/en/docs/release-notes.md
  5. 2
      docs/en/docs/tutorial/sql-databases.md
  6. 20
      docs/en/docs/virtual-environments.md
  7. 2
      fastapi/__init__.py
  8. 23
      fastapi/_compat/v2.py
  9. 7
      fastapi/applications.py
  10. 23
      fastapi/dependencies/utils.py
  11. 12
      fastapi/encoders.py
  12. 12
      fastapi/utils.py
  13. 24
      tests/test_compat.py
  14. 7
      tests/test_computed_fields.py
  15. 2
      tests/test_fastapi_cli.py
  16. 35
      tests/test_form_default.py
  17. 47
      tests/test_forms_single_model.py
  18. 15
      tests/test_jsonable_encoder.py
  19. 30
      tests/test_optional_file_list.py
  20. 111
      tests/test_query_cookie_header_model_extra_params.py
  21. 76
      tests/test_request_param_model_by_alias.py
  22. 92
      tests/test_schema_compat_pydantic_v2.py
  23. 2
      tests/test_tutorial/test_header_param_models/test_tutorial003.py

2
README.md

@ -40,7 +40,7 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
<small>* estimation based on tests on an internal development team, building production applications.</small>
<small>* estimation based on tests conducted by an internal development team, building production applications.</small>
## Sponsors

8
docs/en/docs/advanced/behind-a-proxy.md

@ -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`:

2
docs/en/docs/index.md

@ -46,7 +46,7 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
<small>* estimation based on tests on an internal development team, building production applications.</small>
<small>* estimation based on tests conducted by an internal development team, building production applications.</small>
## Sponsors { #sponsors }

38
docs/en/docs/release-notes.md

@ -7,8 +7,46 @@ hide:
## Latest Changes
### 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
* 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful).
* 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma).
### Docs
* 📝 Add tip on how to install `pip` in case of `No module named pip` error in `virtual-environments.md`. PR [#14211](https://github.com/fastapi/fastapi/pull/14211) by [@zadevhub](https://github.com/zadevhub).
* 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu).
* 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM).
## 0.123.1
### Fixes
* 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome).
### Internal
* 🐛 Fix Windows UnicodeEncodeError in CLI test. PR [#14295](https://github.com/fastapi/fastapi/pull/14295) by [@hemanth-thirthahalli](https://github.com/hemanth-thirthahalli).
* 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo).
* 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov).

2
docs/en/docs/tutorial/sql-databases.md

@ -65,7 +65,7 @@ There are a few differences:
* `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs).
By having the type as `int | None`, SQLModel will know that this column should be an `INTEGER` in the SQL database and that it should be `NULLABLE`.
**Note:** We use `int | None` for the primary key field so that in Python code we can *create an object without an `id`* (`id=None`), assuming the database will *generate it when saving*. SQLModel understands that the database will provide the `id` and *defines the column as a non-null `INTEGER`* in the database schema. See <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">SQLModel docs on primary keys</a> for details.
* `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column.

20
docs/en/docs/virtual-environments.md

@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
</div>
/// tip
Sometimes, you might get a **`No module named pip`** error when trying to upgrade pip.
If this happens, install and upgrade pip using the command below:
<div class="termy">
```console
$ python -m ensurepip --upgrade
---> 100%
```
</div>
This command will install pip if it is not already installed and also ensures that the installed version of pip is at least as recent as the one available in `ensurepip`.
///
## Add `.gitignore` { #add-gitignore }
If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git.

2
fastapi/__init__.py

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.123.0"
__version__ = "0.123.3"
from starlette import status as status

23
fastapi/_compat/v2.py

@ -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"]
@ -304,7 +314,7 @@ def _remap_definitions_and_field_mappings(
old_name_to_new_name_map = {}
for field_key, schema in field_mapping.items():
model = field_key[0].type_
if model not in model_name_map:
if model not in model_name_map or "$ref" not in schema:
continue
new_name = model_name_map[model]
old_name = schema["$ref"].split("/")[-1]
@ -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]

7
fastapi/applications.py

@ -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:

23
fastapi/dependencies/utils.py

@ -787,13 +787,19 @@ 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, value in received_params.items():
for key in received_params.keys():
if key not in processed_keys:
if hasattr(received_params, "getlist"):
value = received_params.getlist(key)
if isinstance(value, list) and (len(value) == 1):
params_to_process[key] = value[0]
else:
params_to_process[key] = value
else:
params_to_process[key] = received_params.get(key)
if single_not_embedded_field:
field_info = first_field.field_info
@ -902,9 +908,14 @@ async def _extract_form_body(
value = serialize_sequence_value(field=field, value=results)
if value is not None:
values[field.alias] = value
for key, value in received_body.items():
if key not in values:
values[key] = value
field_aliases = {field.alias for field in body_fields}
for key in received_body.keys():
if key not in field_aliases:
param_values = received_body.getlist(key)
if len(param_values) == 1:
values[key] = param_values[0]
else:
values[key] = param_values
return values

12
fastapi/encoders.py

@ -34,14 +34,14 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()
# Taken from Pydantic v1 as is
# Adapted from Pydantic v1
# TODO: pv2 should this return strings instead?
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
Encodes a Decimal as int of there's no exponent, otherwise float
Encodes a Decimal as int if there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float
where an integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse.
Our Id type is a prime example of this.
@ -50,8 +50,12 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
>>> decimal_encoder(Decimal("1"))
1
>>> decimal_encoder(Decimal("NaN"))
nan
"""
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
exponent = dec_value.as_tuple().exponent
if isinstance(exponent, int) and exponent >= 0:
return int(dec_value)
else:
return float(dec_value)

12
fastapi/utils.py

@ -110,7 +110,9 @@ def create_model_field(
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
elif PYDANTIC_V2:
from ._compat import v2
@ -121,7 +123,9 @@ def create_model_field(
try:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
@ -129,7 +133,9 @@ def create_model_field(
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
def create_cloned_field(

24
tests/test_compat.py

@ -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

7
tests/test_computed_fields.py

@ -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")

2
tests/test_fastapi_cli.py

@ -1,3 +1,4 @@
import os
import subprocess
import sys
from unittest.mock import patch
@ -20,6 +21,7 @@ def test_fastapi_cli():
],
capture_output=True,
encoding="utf-8",
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
)
assert result.returncode == 1, result.stdout
assert "Path does not exist non_existent_file.py" in result.stdout

35
tests/test_form_default.py

@ -0,0 +1,35 @@
from typing import Optional
from fastapi import FastAPI, File, Form
from starlette.testclient import TestClient
from typing_extensions import Annotated
app = FastAPI()
@app.post("/urlencoded")
async def post_url_encoded(age: Annotated[Optional[int], Form()] = None):
return age
@app.post("/multipart")
async def post_multi_part(
age: Annotated[Optional[int], Form()] = None,
file: Annotated[Optional[bytes], File()] = None,
):
return {"file": file, "age": age}
client = TestClient(app)
def test_form_default_url_encoded():
response = client.post("/urlencoded", data={"age": ""})
assert response.status_code == 200
assert response.text == "null"
def test_form_default_multi_part():
response = client.post("/multipart", data={"age": ""})
assert response.status_code == 200
assert response.json() == {"file": None, "age": None}

47
tests/test_forms_single_model.py

@ -2,6 +2,7 @@ from typing import List, Optional
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
@ -17,11 +18,27 @@ class FormModel(BaseModel):
alias_with: str = Field(alias="with", default="nothing")
class FormModelExtraAllow(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.post("/form/")
def post_form(user: Annotated[FormModel, Form()]):
return user
@app.post("/form-extra-allow/")
def post_form_extra_allow(params: Annotated[FormModelExtraAllow, Form()]):
return params
client = TestClient(app)
@ -131,3 +148,33 @@ def test_no_data():
]
}
)
def test_extra_param_single():
response = client.post(
"/form-extra-allow/",
data={
"param": "123",
"extra_param": "456",
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"param": "123",
"extra_param": "456",
}
def test_extra_param_list():
response = client.post(
"/form-extra-allow/",
data={
"param": "123",
"extra_params": ["456", "789"],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"param": "123",
"extra_params": ["456", "789"],
}

15
tests/test_jsonable_encoder.py

@ -3,6 +3,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from math import isinf, isnan
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
@ -306,6 +307,20 @@ def test_decimal_encoder_int():
assert jsonable_encoder(data) == {"value": 2}
@needs_pydanticv2
def test_decimal_encoder_nan():
data = {"value": Decimal("NaN")}
assert isnan(jsonable_encoder(data)["value"])
@needs_pydanticv2
def test_decimal_encoder_infinity():
data = {"value": Decimal("Infinity")}
assert isinf(jsonable_encoder(data)["value"])
data = {"value": Decimal("-Infinity")}
assert isinf(jsonable_encoder(data)["value"])
def test_encode_deque_encodes_child_models():
class Model(BaseModel):
test: str

30
tests/test_optional_file_list.py

@ -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}

111
tests/test_query_cookie_header_model_extra_params.py

@ -0,0 +1,111 @@
from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Model(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.get("/query")
async def query_model_with_extra(data: Model = Query()):
return data
@app.get("/header")
async def header_model_with_extra(data: Model = Header()):
return data
@app.get("/cookie")
async def cookies_model_with_extra(data: Model = Cookie()):
return data
def test_query_pass_extra_list():
client = TestClient(app)
resp = client.get(
"/query",
params={
"param": "123",
"param2": ["456", "789"], # Pass a list of values as extra parameter
},
)
assert resp.status_code == 200
assert resp.json() == {
"param": "123",
"param2": ["456", "789"],
}
def test_query_pass_extra_single():
client = TestClient(app)
resp = client.get(
"/query",
params={
"param": "123",
"param2": "456",
},
)
assert resp.status_code == 200
assert resp.json() == {
"param": "123",
"param2": "456",
}
def test_header_pass_extra_list():
client = TestClient(app)
resp = client.get(
"/header",
headers=[
("param", "123"),
("param2", "456"), # Pass a list of values as extra parameter
("param2", "789"),
],
)
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == ["456", "789"]
def test_header_pass_extra_single():
client = TestClient(app)
resp = client.get(
"/header",
headers=[
("param", "123"),
("param2", "456"),
],
)
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == "456"
def test_cookie_pass_extra_list():
client = TestClient(app)
client.cookies = [
("param", "123"),
("param2", "456"), # Pass a list of values as extra parameter
("param2", "789"),
]
resp = client.get("/cookie")
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == "789" # Cookies only keep the last value

76
tests/test_request_param_model_by_alias.py

@ -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"}

92
tests/test_schema_compat_pydantic_v2.py

@ -0,0 +1,92 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from pydantic import BaseModel
from tests.utils import needs_py310, needs_pydanticv2
@pytest.fixture(name="client")
def get_client():
from enum import Enum
app = FastAPI()
class PlatformRole(str, Enum):
admin = "admin"
user = "user"
class OtherRole(str, Enum): ...
class User(BaseModel):
username: str
role: PlatformRole | OtherRole
@app.get("/users")
async def get_user() -> User:
return {"username": "alice", "role": "admin"}
client = TestClient(app)
return client
@needs_py310
@needs_pydanticv2
def test_get(client: TestClient):
response = client.get("/users")
assert response.json() == {"username": "alice", "role": "admin"}
@needs_py310
@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("openapi.json")
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/users": {
"get": {
"summary": "Get User",
"operationId": "get_user_users_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/User"}
}
},
}
},
}
}
},
"components": {
"schemas": {
"PlatformRole": {
"type": "string",
"enum": ["admin", "user"],
"title": "PlatformRole",
},
"User": {
"properties": {
"username": {"type": "string", "title": "Username"},
"role": {
"anyOf": [
{"$ref": "#/components/schemas/PlatformRole"},
{"enum": [], "title": "OtherRole"},
],
"title": "Role",
},
},
"type": "object",
"required": ["username", "role"],
"title": "User",
},
}
},
}
)

2
tests/test_tutorial/test_header_param_models/test_tutorial003.py

@ -77,7 +77,7 @@ def test_header_param_model_no_underscore(client: TestClient):
"user-agent": "testclient",
"save-data": "true",
"if-modified-since": "yesterday",
"x-tag": "two",
"x-tag": ["one", "two"],
},
}
)

Loading…
Cancel
Save