mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-29 17:19:00 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c54a8f07b | ||
|
|
e1a2933739 | ||
|
|
00b97526e7 | ||
|
|
2ca7709c24 | ||
|
|
bb05007f55 | ||
|
|
0f7ce0b78a | ||
|
|
cdafd64c15 | ||
|
|
c6c7b72096 | ||
|
|
cb3792d39e | ||
|
|
10eed3806a | ||
|
|
de5bec637c | ||
|
|
2330e2de75 | ||
|
|
6cf40df24d | ||
|
|
740ec2787b | ||
|
|
d68c066246 |
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
35
tests/test_form_default.py
Normal file
35
tests/test_form_default.py
Normal 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}
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
111
tests/test_query_cookie_header_model_extra_params.py
Normal file
111
tests/test_query_cookie_header_model_extra_params.py
Normal 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
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user