Compare commits

...

15 Commits

Author SHA1 Message Date
Sebastián Ramírez
3c54a8f07b 🔖 Release version 0.123.2 2025-12-02 06:31:27 +01:00
github-actions[bot]
e1a2933739 📝 Update release notes
[skip ci]
2025-12-02 05:09:48 +00:00
zadevhub
00b97526e7 📝 Add tip on how to install pip in case of No module named pip error in virtual-environments.md (#14211) 2025-12-02 06:09:25 +01:00
github-actions[bot]
2ca7709c24 📝 Update release notes
[skip ci]
2025-12-02 05:07:29 +00:00
Flavius
bb05007f55 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion (#14120) 2025-12-02 06:06:56 +01:00
github-actions[bot]
0f7ce0b78a 📝 Update release notes
[skip ci]
2025-12-02 05:04:09 +00:00
SaisakthiM
cdafd64c15 📝 Clarify estimation note in documentation (#14070) 2025-12-02 06:03:46 +01:00
github-actions[bot]
c6c7b72096 📝 Update release notes
[skip ci]
2025-12-02 05:01:37 +00:00
Alex Colby
cb3792d39e 🐛 Fix unformatted {type_} in FastAPIError (#14416)
Co-authored-by: Alex Colby <alex.colby@intellisense.io>
2025-12-02 06:01:11 +01:00
github-actions[bot]
10eed3806a 📝 Update release notes
[skip ci]
2025-12-02 04:57:45 +00:00
Motov Yurii
de5bec637c 🐛 Fix parsing extra non-body parameter list (#14356)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2025-12-02 05:57:19 +01:00
github-actions[bot]
2330e2de75 📝 Update release notes
[skip ci]
2025-12-02 04:49:52 +00:00
Motov Yurii
6cf40df24d 🐛 Fix parsing extra Form parameter list (#14303)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2025-12-02 05:49:32 +01:00
github-actions[bot]
740ec2787b 📝 Update release notes
[skip ci]
2025-12-02 04:40:16 +00:00
ad hoc
d68c066246 🐛 Fix support for form values with empty strings interpreted as missing (None if that's the default), for compatibility with HTML forms (#13537)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2025-12-02 05:39:55 +01:00
12 changed files with 259 additions and 13 deletions

View File

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

View File

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

View File

@@ -7,6 +7,21 @@ hide:
## Latest Changes
## 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

View File

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

View File

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

View File

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

View File

@@ -791,9 +791,16 @@ def request_params_to_args(
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:
params_to_process[key] = value
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 +909,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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"],
},
}
)