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

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.
```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] *}

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] *}
### 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 <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:
{* ../../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>.
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
* 📝 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).

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:
//// 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`.

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"""
__version__ = "0.115.3"
__version__ = "0.115.4"
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:
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(

107
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

Loading…
Cancel
Save