Browse Source

Merge branch 'master' into fix-duplicate-special-dependency-handling

pull/12406/head
Peter Volf 7 months ago
committed by GitHub
parent
commit
9e94cb6d7a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 24
      docs/en/docs/how-to/custom-docs-ui-assets.md
  2. 24
      docs/en/docs/how-to/custom-request-and-route.md
  3. 10
      docs/en/docs/how-to/extending-openapi.md
  4. 2
      docs/en/docs/how-to/graphql.md
  5. 17
      docs/en/docs/release-notes.md
  6. 16
      docs/en/docs/tutorial/encoder.md
  7. 33
      docs/ko/docs/advanced/response-change-status-code.md
  8. 2
      fastapi/__init__.py
  9. 30
      fastapi/dependencies/utils.py
  10. 107
      tests/test_multipart_installation.py

24
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: To disable them, set their URLs to `None` when creating your `FastAPI` app:
```Python hl_lines="8" {* ../../docs_src/custom_docs_ui/tutorial001.py hl[8] *}
{!../../docs_src/custom_docs_ui/tutorial001.py!}
```
### Include the custom docs ### 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... And similarly for ReDoc...
```Python hl_lines="2-6 11-19 22-24 27-33" {* ../../docs_src/custom_docs_ui/tutorial001.py hl[2:6,11:19,22:24,27:33] *}
{!../../docs_src/custom_docs_ui/tutorial001.py!}
```
/// tip /// 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*: 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 hl[36:38] *}
{!../../docs_src/custom_docs_ui/tutorial001.py!}
```
### Test it ### 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: To disable them, set their URLs to `None` when creating your `FastAPI` app:
```Python hl_lines="9" {* ../../docs_src/custom_docs_ui/tutorial002.py hl[9] *}
{!../../docs_src/custom_docs_ui/tutorial002.py!}
```
### Include the custom docs for static files ### 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... And similarly for ReDoc...
```Python hl_lines="2-6 14-22 25-27 30-36" {* ../../docs_src/custom_docs_ui/tutorial002.py hl[2:6,14:22,25:27,30:36] *}
{!../../docs_src/custom_docs_ui/tutorial002.py!}
```
/// tip /// 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*: 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 hl[39:41] *}
{!../../docs_src/custom_docs_ui/tutorial002.py!}
```
### Test Static Files UI ### Test Static Files UI

24
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. 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 hl[8:15] *}
{!../../docs_src/custom_request_and_route/tutorial001.py!}
```
### Create a custom `GzipRoute` class ### 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. 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 hl[18:26] *}
{!../../docs_src/custom_request_and_route/tutorial001.py!}
```
/// note | "Technical Details" /// 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: 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 hl[13,15] *}
{!../../docs_src/custom_request_and_route/tutorial002.py!}
```
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: 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 hl[16:18] *}
{!../../docs_src/custom_request_and_route/tutorial002.py!}
```
## Custom `APIRoute` class in a router ## Custom `APIRoute` class in a router
You can also set the `route_class` parameter of an `APIRouter`: You can also set the `route_class` parameter of an `APIRouter`:
```Python hl_lines="26" {* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
{!../../docs_src/custom_request_and_route/tutorial003.py!}
```
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: 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 hl[13:20] *}
{!../../docs_src/custom_request_and_route/tutorial003.py!}
```

10
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] *} {* ../../docs_src/extending_openapi/tutorial001.py hl[1,4,7:9] *}
### Generate the OpenAPI schema ### Generate the OpenAPI schema
Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: 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] *} {* ../../docs_src/extending_openapi/tutorial001.py hl[2,15:21] *}
### Modify the OpenAPI schema ### Modify the OpenAPI schema
Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in 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] *} {* ../../docs_src/extending_openapi/tutorial001.py hl[22:24] *}
### Cache the OpenAPI schema ### Cache the OpenAPI schema
You can use the property `.openapi_schema` as a "cache", to store your generated 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. 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] *} {* ../../docs_src/extending_openapi/tutorial001.py hl[13:14,25:26] *}
### Override the method ### Override the method
Now you can replace the `.openapi()` method with your new function. Now you can replace the `.openapi()` method with your new function.
{* ../../docs_src/extending_openapi/tutorial001.py hl[29] *} {* ../../docs_src/extending_openapi/tutorial001.py hl[29] *}
### Check it ### Check it
Once you go to <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo): Once you go to <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo):

2
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: Here's a small preview of how you could integrate Strawberry with FastAPI:
{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} {* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *}
You can learn more about Strawberry in the <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry documentation</a>. You can learn more about Strawberry in the <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry documentation</a>.
And also the docs about <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">Strawberry with FastAPI</a>. And also the docs about <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">Strawberry with FastAPI</a>.

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

@ -9,6 +9,23 @@ hide:
### Docs ### 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/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/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). * 📝 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).

16
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: It receives an object, like a Pydantic model, and returns a JSON compatible version:
//// tab | Python 3.10+ {* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *}
```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!}
```
////
In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`.

33
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` 파라미터를 선언하고 그 안에서 상태 코드를 설정할 수 있습니다. 단, 마지막으로 설정된 상태 코드가 우선 적용된다는 점을 유의하세요.

2
fastapi/__init__.py

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production""" """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 from starlette import status as status

30
fastapi/dependencies/utils.py

@ -90,21 +90,29 @@ multipart_incorrect_install_error = (
def ensure_multipart_is_installed() -> None: def ensure_multipart_is_installed() -> None:
try: try:
# __version__ is available in both multiparts, and can be mocked from python_multipart import __version__
from multipart import __version__
assert __version__ # Import an attribute that can be mocked/deleted in testing
assert __version__ > "0.0.12"
except (ImportError, AssertionError):
try: try:
# parse_options_header is only available in the right multipart # __version__ is available in both multiparts, and can be mocked
from multipart.multipart import parse_options_header 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: except ImportError:
logger.error(multipart_incorrect_install_error) logger.error(multipart_not_installed_error)
raise RuntimeError(multipart_incorrect_install_error) from None raise RuntimeError(multipart_not_installed_error) from None
except ImportError:
logger.error(multipart_not_installed_error)
raise RuntimeError(multipart_not_installed_error) from None
def get_param_sub_dependant( def get_param_sub_dependant(

107
tests/test_multipart_installation.py

@ -1,3 +1,5 @@
import warnings
import pytest import pytest
from fastapi import FastAPI, File, Form, UploadFile from fastapi import FastAPI, File, Form, UploadFile
from fastapi.dependencies.utils import ( from fastapi.dependencies.utils import (
@ -7,7 +9,10 @@ from fastapi.dependencies.utils import (
def test_incorrect_multipart_installed_form(monkeypatch): 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): with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI() app = FastAPI()
@ -17,7 +22,10 @@ def test_incorrect_multipart_installed_form(monkeypatch):
def test_incorrect_multipart_installed_file_upload(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): with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI() app = FastAPI()
@ -27,7 +35,10 @@ def test_incorrect_multipart_installed_file_upload(monkeypatch):
def test_incorrect_multipart_installed_file_bytes(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): with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI() app = FastAPI()
@ -37,7 +48,10 @@ def test_incorrect_multipart_installed_file_bytes(monkeypatch):
def test_incorrect_multipart_installed_multi_form(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): with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI() app = FastAPI()
@ -47,7 +61,10 @@ def test_incorrect_multipart_installed_multi_form(monkeypatch):
def test_incorrect_multipart_installed_form_file(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): with pytest.raises(RuntimeError, match=multipart_incorrect_install_error):
app = FastAPI() app = FastAPI()
@ -57,50 +74,76 @@ def test_incorrect_multipart_installed_form_file(monkeypatch):
def test_no_multipart_installed(monkeypatch): def test_no_multipart_installed(monkeypatch):
monkeypatch.delattr("multipart.__version__", raising=False) monkeypatch.setattr("python_multipart.__version__", "0.0.12")
with pytest.raises(RuntimeError, match=multipart_not_installed_error): with warnings.catch_warnings(record=True):
app = FastAPI() warnings.simplefilter("always")
monkeypatch.delattr("multipart.__version__", raising=False)
with pytest.raises(RuntimeError, match=multipart_not_installed_error):
app = FastAPI()
@app.post("/") @app.post("/")
async def root(username: str = Form()): async def root(username: str = Form()):
return username # pragma: nocover return username # pragma: nocover
def test_no_multipart_installed_file(monkeypatch): def test_no_multipart_installed_file(monkeypatch):
monkeypatch.delattr("multipart.__version__", raising=False) monkeypatch.setattr("python_multipart.__version__", "0.0.12")
with pytest.raises(RuntimeError, match=multipart_not_installed_error): with warnings.catch_warnings(record=True):
app = FastAPI() warnings.simplefilter("always")
monkeypatch.delattr("multipart.__version__", raising=False)
with pytest.raises(RuntimeError, match=multipart_not_installed_error):
app = FastAPI()
@app.post("/") @app.post("/")
async def root(f: UploadFile = File()): async def root(f: UploadFile = File()):
return f # pragma: nocover return f # pragma: nocover
def test_no_multipart_installed_file_bytes(monkeypatch): def test_no_multipart_installed_file_bytes(monkeypatch):
monkeypatch.delattr("multipart.__version__", raising=False) monkeypatch.setattr("python_multipart.__version__", "0.0.12")
with pytest.raises(RuntimeError, match=multipart_not_installed_error): with warnings.catch_warnings(record=True):
app = FastAPI() warnings.simplefilter("always")
monkeypatch.delattr("multipart.__version__", raising=False)
with pytest.raises(RuntimeError, match=multipart_not_installed_error):
app = FastAPI()
@app.post("/") @app.post("/")
async def root(f: bytes = File()): async def root(f: bytes = File()):
return f # pragma: nocover return f # pragma: nocover
def test_no_multipart_installed_multi_form(monkeypatch): def test_no_multipart_installed_multi_form(monkeypatch):
monkeypatch.delattr("multipart.__version__", raising=False) monkeypatch.setattr("python_multipart.__version__", "0.0.12")
with pytest.raises(RuntimeError, match=multipart_not_installed_error): with warnings.catch_warnings(record=True):
app = FastAPI() warnings.simplefilter("always")
monkeypatch.delattr("multipart.__version__", raising=False)
with pytest.raises(RuntimeError, match=multipart_not_installed_error):
app = FastAPI()
@app.post("/") @app.post("/")
async def root(username: str = Form(), password: str = Form()): async def root(username: str = Form(), password: str = Form()):
return username # pragma: nocover return username # pragma: nocover
def test_no_multipart_installed_form_file(monkeypatch): def test_no_multipart_installed_form_file(monkeypatch):
monkeypatch.delattr("multipart.__version__", raising=False) monkeypatch.setattr("python_multipart.__version__", "0.0.12")
with pytest.raises(RuntimeError, match=multipart_not_installed_error): 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 = FastAPI()
@app.post("/") @app.post("/")
async def root(username: str = Form(), f: UploadFile = File()): async def root(username: str = Form()):
return username # pragma: nocover return username # pragma: nocover

Loading…
Cancel
Save