diff --git a/docs/en/docs/how-to/custom-docs-ui-assets.md b/docs/en/docs/how-to/custom-docs-ui-assets.md
index 16c873d11..abcccb499 100644
--- a/docs/en/docs/how-to/custom-docs-ui-assets.md
+++ b/docs/en/docs/how-to/custom-docs-ui-assets.md
@@ -18,9 +18,7 @@ The first step is to disable the automatic docs, as by default, those use the de
To disable them, set their URLs to `None` when creating your `FastAPI` app:
-```Python hl_lines="8"
-{!../../docs_src/custom_docs_ui/tutorial001.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *}
### Include the custom docs
@@ -36,9 +34,7 @@ You can reuse FastAPI's internal functions to create the HTML pages for the docs
And similarly for ReDoc...
-```Python hl_lines="2-6 11-19 22-24 27-33"
-{!../../docs_src/custom_docs_ui/tutorial001.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *}
/// tip
@@ -54,9 +50,7 @@ Swagger UI will handle it behind the scenes for you, but it needs this "redirect
Now, to be able to test that everything works, create a *path operation*:
-```Python hl_lines="36-38"
-{!../../docs_src/custom_docs_ui/tutorial001.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial001.py hl[36:38] *}
### Test it
@@ -158,9 +152,7 @@ The same as when using a custom CDN, the first step is to disable the automatic
To disable them, set their URLs to `None` when creating your `FastAPI` app:
-```Python hl_lines="9"
-{!../../docs_src/custom_docs_ui/tutorial002.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *}
### Include the custom docs for static files
@@ -176,9 +168,7 @@ Again, you can reuse FastAPI's internal functions to create the HTML pages for t
And similarly for ReDoc...
-```Python hl_lines="2-6 14-22 25-27 30-36"
-{!../../docs_src/custom_docs_ui/tutorial002.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *}
/// tip
@@ -194,9 +184,7 @@ Swagger UI will handle it behind the scenes for you, but it needs this "redirect
Now, to be able to test that everything works, create a *path operation*:
-```Python hl_lines="39-41"
-{!../../docs_src/custom_docs_ui/tutorial002.py!}
-```
+{* ../../docs_src/custom_docs_ui/tutorial002.py hl[39:41] *}
### Test Static Files UI
diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md
index a62ebf1d5..25ec0a335 100644
--- a/docs/en/docs/how-to/custom-request-and-route.md
+++ b/docs/en/docs/how-to/custom-request-and-route.md
@@ -42,9 +42,7 @@ If there's no `gzip` in the header, it will not try to decompress the body.
That way, the same route class can handle gzip compressed or uncompressed requests.
-```Python hl_lines="8-15"
-{!../../docs_src/custom_request_and_route/tutorial001.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *}
### Create a custom `GzipRoute` class
@@ -56,9 +54,7 @@ This method returns a function. And that function is what will receive a request
Here we use it to create a `GzipRequest` from the original request.
-```Python hl_lines="18-26"
-{!../../docs_src/custom_request_and_route/tutorial001.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *}
/// note | "Technical Details"
@@ -96,26 +92,18 @@ We can also use this same approach to access the request body in an exception ha
All we need to do is handle the request inside a `try`/`except` block:
-```Python hl_lines="13 15"
-{!../../docs_src/custom_request_and_route/tutorial002.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *}
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
-```Python hl_lines="16-18"
-{!../../docs_src/custom_request_and_route/tutorial002.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *}
## Custom `APIRoute` class in a router
You can also set the `route_class` parameter of an `APIRouter`:
-```Python hl_lines="26"
-{!../../docs_src/custom_request_and_route/tutorial003.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
-```Python hl_lines="13-20"
-{!../../docs_src/custom_request_and_route/tutorial003.py!}
-```
+{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *}
diff --git a/docs/en/docs/how-to/extending-openapi.md b/docs/en/docs/how-to/extending-openapi.md
index 8c7790725..26c742c20 100644
--- a/docs/en/docs/how-to/extending-openapi.md
+++ b/docs/en/docs/how-to/extending-openapi.md
@@ -45,23 +45,18 @@ First, write all your **FastAPI** application as normally:
{* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *}
-
### Generate the OpenAPI schema
Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function:
-
-
{* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *}
-
### Modify the OpenAPI schema
Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema:
{* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *}
-
### Cache the OpenAPI schema
You can use the property `.openapi_schema` as a "cache", to store your generated schema.
@@ -70,19 +65,14 @@ That way, your application won't have to generate the schema every time a user o
It will be generated only once, and then the same cached schema will be used for the next requests.
-
{* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *}
-
### Override the method
Now you can replace the `.openapi()` method with your new function.
-
-
{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *}
-
### Check it
Once you go to http://127.0.0.1:8000/redoc you will see that you are using your custom logo (in this example, **FastAPI**'s logo):
diff --git a/docs/en/docs/how-to/graphql.md b/docs/en/docs/how-to/graphql.md
index 5d8f879d1..a6219e481 100644
--- a/docs/en/docs/how-to/graphql.md
+++ b/docs/en/docs/how-to/graphql.md
@@ -35,10 +35,8 @@ Depending on your use case, you might prefer to use a different library, but if
Here's a small preview of how you could integrate Strawberry with FastAPI:
-
{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *}
-
You can learn more about Strawberry in the Strawberry documentation.
And also the docs about Strawberry with FastAPI.
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index bedd3e5f2..c687c7e0f 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -9,6 +9,23 @@ hide:
### Docs
+* π Update includes in `docs/en/docs/tutorial/encoder.md`. PR [#12597](https://github.com/fastapi/fastapi/pull/12597) by [@tonyjly](https://github.com/tonyjly).
+* π Update includes in `docs/en/docs/how-to/custom-docs-ui-assets.md`. PR [#12557](https://github.com/fastapi/fastapi/pull/12557) by [@philipokiokio](https://github.com/philipokiokio).
+* π¨ Adjust spacing. PR [#12635](https://github.com/fastapi/fastapi/pull/12635) by [@alejsdev](https://github.com/alejsdev).
+* π Update includes in `docs/en/docs/how-to/custom-request-and-route.md`. PR [#12560](https://github.com/fastapi/fastapi/pull/12560) by [@philipokiokio](https://github.com/philipokiokio).
+
+### Translations
+
+* π Add Korean Translation for `docs/ko/docs/advanced/response-change-status-code.md`. PR [#12547](https://github.com/fastapi/fastapi/pull/12547) by [@9zimin9](https://github.com/9zimin9).
+
+## 0.115.4
+
+### Refactors
+
+* β»οΈ Update logic to import and check `python-multipart` for compatibility with newer version. PR [#12627](https://github.com/fastapi/fastapi/pull/12627) by [@tiangolo](https://github.com/tiangolo).
+
+### Docs
+
* π Update includes in `docs/fr/docs/tutorial/body.md`. PR [#12596](https://github.com/fastapi/fastapi/pull/12596) by [@kantandane](https://github.com/kantandane).
* π Update includes in `docs/fr/docs/tutorial/debugging.md`. PR [#12595](https://github.com/fastapi/fastapi/pull/12595) by [@kantandane](https://github.com/kantandane).
* π Update includes in `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#12591](https://github.com/fastapi/fastapi/pull/12591) by [@kantandane](https://github.com/kantandane).
diff --git a/docs/en/docs/tutorial/encoder.md b/docs/en/docs/tutorial/encoder.md
index 039ac6714..e2eceafcc 100644
--- a/docs/en/docs/tutorial/encoder.md
+++ b/docs/en/docs/tutorial/encoder.md
@@ -20,21 +20,7 @@ You can use `jsonable_encoder` for that.
It receives an object, like a Pydantic model, and returns a JSON compatible version:
-//// tab | Python 3.10+
-
-```Python hl_lines="4 21"
-{!> ../../docs_src/encoder/tutorial001_py310.py!}
-```
-
-////
-
-//// tab | Python 3.8+
-
-```Python hl_lines="5 22"
-{!> ../../docs_src/encoder/tutorial001.py!}
-```
-
-////
+{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *}
In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`.
diff --git a/docs/ko/docs/advanced/response-change-status-code.md b/docs/ko/docs/advanced/response-change-status-code.md
new file mode 100644
index 000000000..f3cdd2ba5
--- /dev/null
+++ b/docs/ko/docs/advanced/response-change-status-code.md
@@ -0,0 +1,33 @@
+# μλ΅ - μν μ½λ λ³κ²½
+
+κΈ°λ³Έ [μλ΅ μν μ½λ μ€μ ](../tutorial/response-status-code.md){.internal-link target=_blank}μ΄ κ°λ₯νλ€λ κ±Έ μ΄λ―Έ μκ³ κ³μ€ κ²λλ€.
+
+νμ§λ§ κ²½μ°μ λ°λΌ κΈ°λ³Έ μ€μ κ³Ό λ€λ₯Έ μν μ½λλ₯Ό λ°νν΄μΌ ν λκ° μμ΅λλ€.
+
+## μ¬μ© μ
+
+μλ₯Ό λ€μ΄ κΈ°λ³Έμ μΌλ‘ HTTP μν μ½λ "OK" `200`μ λ°ννκ³ μΆλ€κ³ κ°μ ν΄ λ΄
μλ€.
+
+νμ§λ§ λ°μ΄ν°κ° μ‘΄μ¬νμ§ μμΌλ©΄ μ΄λ₯Ό μλ‘ μμ±νκ³ , HTTP μν μ½λ "CREATED" `201`μ λ°ννκ³ μ ν λκ° μμ μ μμ΅λλ€.
+
+μ΄λλ μ¬μ ν `response_model`μ μ¬μ©νμ¬ λ°ννλ λ°μ΄ν°λ₯Ό νν°λ§νκ³ λ³ννκ³ μΆμ μ μμ΅λλ€.
+
+μ΄λ° κ²½μ°μλ `Response` νλΌλ―Έν°λ₯Ό μ¬μ©ν μ μμ΅λλ€.
+
+## `Response` νλΌλ―Έν° μ¬μ©νκΈ°
+
+*κ²½λ‘ μλ ν¨μ*μ `Response` νμ
μ νλΌλ―Έν°λ₯Ό μ μΈν μ μμ΅λλ€. (μΏ ν€μ ν€λμ λν΄ μ μΈνλ κ²κ³Ό μ μ¬νκ²)
+
+κ·Έλ¦¬κ³ μ΄ *μμ* μλ΅ κ°μ²΄μμ `status_code`λ₯Ό μ€μ ν μ μμ΅λλ€.
+
+```Python hl_lines="1 9 12"
+{!../../docs_src/response_change_status_code/tutorial001.py!}
+```
+
+κ·Έλ¦¬κ³ νμμ²λΌ μνλ κ°μ²΄(`dict`, λ°μ΄ν°λ² μ΄μ€ λͺ¨λΈ λ±)λ₯Ό λ°νν μ μμ΅λλ€.
+
+`response_model`μ μ μΈνλ€λ©΄ λ°νλ κ°μ²΄λ μ¬μ ν νν°λ§λκ³ λ³νλ©λλ€.
+
+**FastAPI**λ μ΄ *μμ* μλ΅ κ°μ²΄μμ μν μ½λ(μΏ ν€μ ν€λ ν¬ν¨)λ₯Ό μΆμΆνμ¬, `response_model`λ‘ νν°λ§λ λ°ν κ°μ μ΅μ’
μλ΅μ λ£μ΅λλ€.
+
+λν, μμ‘΄μ±μμλ `Response` νλΌλ―Έν°λ₯Ό μ μΈνκ³ κ·Έ μμμ μν μ½λλ₯Ό μ€μ ν μ μμ΅λλ€. λ¨, λ§μ§λ§μΌλ‘ μ€μ λ μν μ½λκ° μ°μ μ μ©λλ€λ μ μ μ μνμΈμ.
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 64d5dd39b..51e3ca510 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.115.3"
+__version__ = "0.115.4"
from starlette import status as status
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 94b215e6f..70d7d1e13 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -90,21 +90,29 @@ multipart_incorrect_install_error = (
def ensure_multipart_is_installed() -> None:
try:
- # __version__ is available in both multiparts, and can be mocked
- from multipart import __version__
+ from python_multipart import __version__
- assert __version__
+ # Import an attribute that can be mocked/deleted in testing
+ assert __version__ > "0.0.12"
+ except (ImportError, AssertionError):
try:
- # parse_options_header is only available in the right multipart
- from multipart.multipart import parse_options_header
+ # __version__ is available in both multiparts, and can be mocked
+ from multipart import __version__ # type: ignore[no-redef,import-untyped]
- assert parse_options_header # type: ignore[truthy-function]
+ assert __version__
+ try:
+ # parse_options_header is only available in the right multipart
+ from multipart.multipart import ( # type: ignore[import-untyped]
+ parse_options_header,
+ )
+
+ assert parse_options_header
+ except ImportError:
+ logger.error(multipart_incorrect_install_error)
+ raise RuntimeError(multipart_incorrect_install_error) from None
except ImportError:
- logger.error(multipart_incorrect_install_error)
- raise RuntimeError(multipart_incorrect_install_error) from None
- except ImportError:
- logger.error(multipart_not_installed_error)
- raise RuntimeError(multipart_not_installed_error) from None
+ logger.error(multipart_not_installed_error)
+ raise RuntimeError(multipart_not_installed_error) from None
def get_param_sub_dependant(
diff --git a/tests/test_multipart_installation.py b/tests/test_multipart_installation.py
index 788d9ef5a..9c3e47c49 100644
--- a/tests/test_multipart_installation.py
+++ b/tests/test_multipart_installation.py
@@ -1,3 +1,5 @@
+import warnings
+
import pytest
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.dependencies.utils import (
@@ -7,7 +9,10 @@ from fastapi.dependencies.utils import (
def test_incorrect_multipart_installed_form(monkeypatch):
- monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI()
@@ -17,7 +22,10 @@ def test_incorrect_multipart_installed_form(monkeypatch):
def test_incorrect_multipart_installed_file_upload(monkeypatch):
- monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI()
@@ -27,7 +35,10 @@ def test_incorrect_multipart_installed_file_upload(monkeypatch):
def test_incorrect_multipart_installed_file_bytes(monkeypatch):
- monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI()
@@ -37,7 +48,10 @@ def test_incorrect_multipart_installed_file_bytes(monkeypatch):
def test_incorrect_multipart_installed_multi_form(monkeypatch):
- monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI()
@@ -47,7 +61,10 @@ def test_incorrect_multipart_installed_multi_form(monkeypatch):
def test_incorrect_multipart_installed_form_file(monkeypatch):
- monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.multipart.parse_options_header", raising=False)
with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI()
@@ -57,50 +74,76 @@ def test_incorrect_multipart_installed_form_file(monkeypatch):
def test_no_multipart_installed(monkeypatch):
- monkeypatch.delattr("multipart.__version__", raising=False)
- with pytest.raises(RuntimeError, match=multipart_not_installed_error):
- app = FastAPI()
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.__version__", raising=False)
+ with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ app = FastAPI()
- @app.post("/")
- async def root(username: str = Form()):
- return username # pragma: nocover
+ @app.post("/")
+ async def root(username: str = Form()):
+ return username # pragma: nocover
def test_no_multipart_installed_file(monkeypatch):
- monkeypatch.delattr("multipart.__version__", raising=False)
- with pytest.raises(RuntimeError, match=multipart_not_installed_error):
- app = FastAPI()
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.__version__", raising=False)
+ with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ app = FastAPI()
- @app.post("/")
- async def root(f: UploadFile = File()):
- return f # pragma: nocover
+ @app.post("/")
+ async def root(f: UploadFile = File()):
+ return f # pragma: nocover
def test_no_multipart_installed_file_bytes(monkeypatch):
- monkeypatch.delattr("multipart.__version__", raising=False)
- with pytest.raises(RuntimeError, match=multipart_not_installed_error):
- app = FastAPI()
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.__version__", raising=False)
+ with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ app = FastAPI()
- @app.post("/")
- async def root(f: bytes = File()):
- return f # pragma: nocover
+ @app.post("/")
+ async def root(f: bytes = File()):
+ return f # pragma: nocover
def test_no_multipart_installed_multi_form(monkeypatch):
- monkeypatch.delattr("multipart.__version__", raising=False)
- with pytest.raises(RuntimeError, match=multipart_not_installed_error):
- app = FastAPI()
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.__version__", raising=False)
+ with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ app = FastAPI()
- @app.post("/")
- async def root(username: str = Form(), password: str = Form()):
- return username # pragma: nocover
+ @app.post("/")
+ async def root(username: str = Form(), password: str = Form()):
+ return username # pragma: nocover
def test_no_multipart_installed_form_file(monkeypatch):
- monkeypatch.delattr("multipart.__version__", raising=False)
- with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+ monkeypatch.delattr("multipart.__version__", raising=False)
+ with pytest.raises(RuntimeError, match=multipart_not_installed_error):
+ app = FastAPI()
+
+ @app.post("/")
+ async def root(username: str = Form(), f: UploadFile = File()):
+ return username # pragma: nocover
+
+
+def test_old_multipart_installed(monkeypatch):
+ monkeypatch.setattr("python_multipart.__version__", "0.0.12")
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
app = FastAPI()
@app.post("/")
- async def root(username: str = Form(), f: UploadFile = File()):
+ async def root(username: str = Form()):
return username # pragma: nocover