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