]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
📝 Add variants for code examples in "Advanced User Guide" (#14413)
authorMotov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Wed, 10 Dec 2025 08:55:32 +0000 (09:55 +0100)
committerGitHub <noreply@github.com>
Wed, 10 Dec 2025 08:55:32 +0000 (09:55 +0100)
62 files changed:
docs/en/docs/advanced/additional-responses.md
docs/en/docs/advanced/dataclasses.md
docs/en/docs/advanced/openapi-callbacks.md
docs/en/docs/advanced/path-operation-advanced-configuration.md
docs/en/docs/advanced/response-directly.md
docs/en/docs/advanced/settings.md
docs/en/docs/how-to/configure-swagger-ui.md
docs/en/docs/how-to/custom-request-and-route.md
docs/en/docs/tutorial/bigger-applications.md
docs/en/docs/tutorial/cookie-param-models.md
docs/en/docs/tutorial/testing.md
docs_src/additional_responses/tutorial002_py310.py [new file with mode: 0644]
docs_src/additional_responses/tutorial004_py310.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial001_an.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial001_an_py310.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial001_an_py39.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial001_py310.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial001_py39.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial002_an.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial002_an_py310.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial002_an_py39.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial002_py310.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial002_py39.py [new file with mode: 0644]
docs_src/custom_request_and_route/tutorial003_py310.py [new file with mode: 0644]
docs_src/dataclasses/tutorial001_py310.py [new file with mode: 0644]
docs_src/dataclasses/tutorial002_py310.py [new file with mode: 0644]
docs_src/dataclasses/tutorial002_py39.py [new file with mode: 0644]
docs_src/dataclasses/tutorial003_py310.py [new file with mode: 0644]
docs_src/dataclasses/tutorial003_py39.py [new file with mode: 0644]
docs_src/openapi_callbacks/tutorial001_py310.py [new file with mode: 0644]
docs_src/path_operation_advanced_configuration/tutorial004_py310.py [new file with mode: 0644]
docs_src/path_operation_advanced_configuration/tutorial004_py39.py [new file with mode: 0644]
docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py [new file with mode: 0644]
docs_src/path_operation_advanced_configuration/tutorial007_py39.py [new file with mode: 0644]
docs_src/response_directly/tutorial001_py310.py [new file with mode: 0644]
docs_src/settings/app03/config.py
docs_src/settings/app03/config_pv1.py [new file with mode: 0644]
docs_src/settings/app03_an/main.py
docs_src/settings/app03_an_py39/config.py
docs_src/settings/app03_an_py39/config_pv1.py [new file with mode: 0644]
docs_src/settings/app03_an_py39/main.py
pyproject.toml
tests/test_tutorial/test_additional_responses/test_tutorial002.py
tests/test_tutorial/test_additional_responses/test_tutorial004.py
tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py
tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py
tests/test_tutorial/test_dataclasses/test_tutorial001.py
tests/test_tutorial/test_dataclasses/test_tutorial002.py
tests/test_tutorial/test_dataclasses/test_tutorial003.py
tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py
tests/test_tutorial/test_response_directly/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_response_directly/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_settings/test_app02.py
tests/test_tutorial/test_settings/test_app03.py [new file with mode: 0644]
tests/test_tutorial/test_using_request_directly/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_using_request_directly/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_websockets/test_tutorial003.py
tests/test_tutorial/test_websockets/test_tutorial003_py39.py [deleted file]

index 799532c5b2291b5bfac3c43c324ef6778b2deb70..cb3a40d13eaf8ed95eeb4dfbd3f135a236a6648e 100644 (file)
@@ -175,7 +175,7 @@ You can use this same `responses` parameter to add different media types for the
 
 For example, you can add an additional media type of `image/png`, declaring that your *path operation* can return a JSON object (with media type `application/json`) or a PNG image:
 
-{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *}
+{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
 
 /// note
 
@@ -237,7 +237,7 @@ You can use that technique to reuse some predefined responses in your *path oper
 
 For example:
 
-{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *}
+{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
 
 ## More information about OpenAPI responses { #more-information-about-openapi-responses }
 
index b7b9b65c52fcef16cdb15a65b325b9eaf38a4aba..574beb65f46e5a95ffa85d06aa1df833c085a269 100644 (file)
@@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use
 
 But FastAPI also supports using <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> the same way:
 
-{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *}
+{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 This is still supported thanks to **Pydantic**, as it has <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">internal support for `dataclasses`</a>.
 
@@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us
 
 You can also use `dataclasses` in the `response_model` parameter:
 
-{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *}
+{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
 
 The dataclass will be automatically converted to a Pydantic dataclass.
 
@@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`.
 
 In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement:
 
-{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *}
+{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. We still import `field` from standard `dataclasses`.
 
index 059d893c26cf5cd147d52af28b8fa352a6032eaf..5bd7c2cfd44225fb7ad6f8513503f72c07f7ee89 100644 (file)
@@ -31,7 +31,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query
 
 This part is pretty normal, most of the code is probably already familiar to you:
 
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
 
 /// tip
 
@@ -90,7 +90,7 @@ Temporarily adopting this point of view (of the *external developer*) can help y
 
 First create a new `APIRouter` that will contain one or more callbacks.
 
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
 
 ### Create the callback *path operation* { #create-the-callback-path-operation }
 
@@ -101,7 +101,7 @@ It should look just like a normal FastAPI *path operation*:
 * It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`.
 * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`.
 
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
 
 There are 2 main differences from a normal *path operation*:
 
@@ -169,7 +169,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that
 
 Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router:
 
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
 
 /// tip
 
index b9961f9f384f52932e4b220cf6fcdd153cea8b37..5879bc5c71cd58208cb56e54c3682abf66d4f7d0 100644 (file)
@@ -50,7 +50,7 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate
 
 It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
 
-{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
 
 ## Additional Responses { #additional-responses }
 
@@ -155,13 +155,13 @@ For example, in this application we don't use FastAPI's integrated functionality
 
 //// tab | Pydantic v2
 
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
 
 ////
 
 //// tab | Pydantic v1
 
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
 
 ////
 
@@ -179,13 +179,13 @@ And then in our code, we parse that YAML content directly, and then we are again
 
 //// tab | Pydantic v2
 
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
 
 ////
 
 //// tab | Pydantic v1
 
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
 
 ////
 
index 3197e1bd4680b46b3d1791cea1715c4c15d308b5..156b4dac7e819600d3dd804a068060dc81776dd7 100644 (file)
@@ -34,7 +34,7 @@ For example, you cannot put a Pydantic model in a `JSONResponse` without first c
 
 For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response:
 
-{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *}
+{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
 
 /// note | Technical Details
 
index a218c3d0167006682d83f3f975c0299921314674..0220c52ce140888c7f484b28afc2bc9b8e26e58a 100644 (file)
@@ -148,7 +148,7 @@ This could be especially useful during testing, as it's very easy to override a
 
 Coming from the previous example, your `config.py` file could look like:
 
-{* ../../docs_src/settings/app02/config.py hl[10] *}
+{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
 
 Notice that now we don't create a default instance `settings = Settings()`.
 
@@ -174,7 +174,7 @@ And then we can require it from the *path operation function* as a dependency an
 
 Then it would be very easy to provide a different settings object during testing by creating a dependency override for `get_settings`:
 
-{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
+{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
 
 In the dependency override we set a new value for the `admin_email` when creating the new `Settings` object, and then we return that new object.
 
@@ -217,7 +217,7 @@ And then update your `config.py` with:
 
 //// tab | Pydantic v2
 
-{* ../../docs_src/settings/app03_an/config.py hl[9] *}
+{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
 
 /// tip
 
@@ -229,7 +229,7 @@ The `model_config` attribute is used just for Pydantic configuration. You can re
 
 //// tab | Pydantic v1
 
-{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *}
+{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
 
 /// tip
 
index 2d7b99f8faf83b9a8a3208eded83e26bd0e01794..3dbfcffecac76c9bd8472cba7eec46b1f0618ae4 100644 (file)
@@ -40,7 +40,7 @@ FastAPI includes some default configuration parameters appropriate for most of t
 
 It includes these default configurations:
 
-{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *}
+{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
 
 You can override any of them by setting a different value in the argument `swagger_ui_parameters`.
 
index 884c8ed04f336628aabe4c96ed4f2972c445b0ac..bfc60729f88e2f2e42bd2f3bcf641b028191ac5b 100644 (file)
@@ -42,7 +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.
 
-{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *}
+{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
 
 ### Create a custom `GzipRoute` class { #create-a-custom-gziproute-class }
 
@@ -54,7 +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.
 
-{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *}
+{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
 
 /// note | Technical Details
 
@@ -92,18 +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:
 
-{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *}
+{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
 
 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:
 
-{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *}
+{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
 
 ## Custom `APIRoute` class in a router { #custom-apiroute-class-in-a-router }
 
 You can also set the `route_class` parameter of an `APIRouter`:
 
-{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
+{* ../../docs_src/custom_request_and_route/tutorial003_py310.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:
 
-{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *}
+{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
index 74daa54835f055634b11a660bfae5a0a018267cf..3cc9d7ecfbb52fe734b6f307ec04772856f0a2d8 100644 (file)
@@ -85,9 +85,7 @@ You can create the *path operations* for that module using `APIRouter`.
 
 You import it and create an "instance" the same way you would with the class `FastAPI`:
 
-```Python hl_lines="1  3" title="app/routers/users.py"
-{!../../docs_src/bigger_applications/app/routers/users.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
 
 ### *Path operations* with `APIRouter` { #path-operations-with-apirouter }
 
@@ -95,9 +93,7 @@ And then you use it to declare your *path operations*.
 
 Use it the same way you would use the `FastAPI` class:
 
-```Python hl_lines="6  11  16" title="app/routers/users.py"
-{!../../docs_src/bigger_applications/app/routers/users.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
 
 You can think of `APIRouter` as a "mini `FastAPI`" class.
 
@@ -121,35 +117,7 @@ So we put them in their own `dependencies` module (`app/dependencies.py`).
 
 We will now use a simple dependency to read a custom `X-Token` header:
 
-//// tab | Python 3.9+
-
-```Python hl_lines="3  6-8" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!}
-```
-
-////
-
-//// tab | Python 3.8+
-
-```Python hl_lines="1  5-7" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app_an/dependencies.py!}
-```
-
-////
-
-//// tab | Python 3.8+ non-Annotated
-
-/// tip
-
-Prefer to use the `Annotated` version if possible.
-
-///
-
-```Python hl_lines="1  4-6" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app/dependencies.py!}
-```
-
-////
+{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
 
 /// tip
 
@@ -181,9 +149,7 @@ We know all the *path operations* in this module have the same:
 
 So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`.
 
-```Python hl_lines="5-10  16  21" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
 
 As the path of each *path operation* has to start with `/`, like in:
 
@@ -242,9 +208,7 @@ And we need to get the dependency function from the module `app.dependencies`, t
 
 So we use a relative import with `..` for the dependencies:
 
-```Python hl_lines="3" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
 
 #### How relative imports work { #how-relative-imports-work }
 
@@ -315,9 +279,7 @@ We are not adding the prefix `/items` nor the `tags=["items"]` to each *path ope
 
 But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*:
 
-```Python hl_lines="30-31" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
 
 /// tip
 
@@ -343,17 +305,13 @@ You import and create a `FastAPI` class as normally.
 
 And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`:
 
-```Python hl_lines="1  3  7" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
 
 ### Import the `APIRouter` { #import-the-apirouter }
 
 Now we import the other submodules that have `APIRouter`s:
 
-```Python hl_lines="4-5" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
 
 As the files `app/routers/users.py` and `app/routers/items.py` are submodules that are part of the same Python package `app`, we can use a single dot `.` to import them using "relative imports".
 
@@ -416,17 +374,13 @@ the `router` from `users` would overwrite the one from `items` and we wouldn't b
 
 So, to be able to use both of them in the same file, we import the submodules directly:
 
-```Python hl_lines="5" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
 
 ### Include the `APIRouter`s for `users` and `items` { #include-the-apirouters-for-users-and-items }
 
 Now, let's include the `router`s from the submodules `users` and `items`:
 
-```Python hl_lines="10-11" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
 
 /// info
 
@@ -466,17 +420,13 @@ It contains an `APIRouter` with some admin *path operations* that your organizat
 
 For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`:
 
-```Python hl_lines="3" title="app/internal/admin.py"
-{!../../docs_src/bigger_applications/app/internal/admin.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
 
 But we still want to set a custom `prefix` when including the `APIRouter` so that all its *path operations* start with `/admin`, we want to secure it with the `dependencies` we already have for this project, and we want to include `tags` and `responses`.
 
 We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`:
 
-```Python hl_lines="14-17" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
 
 That way, the original `APIRouter` will stay unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization.
 
@@ -497,9 +447,7 @@ We can also add *path operations* directly to the `FastAPI` app.
 
 Here we do it... just to show that we can 🤷:
 
-```Python hl_lines="21-23" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
 
 and it will work correctly, together with all the other *path operations* added with `app.include_router()`.
 
index 96dc5cf3d877d24229be086c618f92baebfba456..016a65d7f0ad926e64abcc886c3e9b69be5bce26 100644 (file)
@@ -50,7 +50,7 @@ Your API now has the power to control its own <abbr title="This is a joke, just
 
 You can use Pydantic's model configuration to `forbid` any `extra` fields:
 
-{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *}
+{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *}
 
 If a client tries to send some **extra cookies**, they will receive an **error** response.
 
index 3dcf5dc4ac9a4780f4e3c5b9ba67561bda71e588..c6a0e5b7d5bb0d971a83d523c21f7908dd3eb28e 100644 (file)
@@ -121,63 +121,13 @@ It has a `POST` operation that could return several errors.
 
 Both *path operations* require an `X-Token` header.
 
-//// tab | Python 3.10+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an_py310/main.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an_py39/main.py!}
-```
-
-////
-
-//// tab | Python 3.8+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an/main.py!}
-```
-
-////
-
-//// tab | Python 3.10+ non-Annotated
-
-/// tip
-
-Prefer to use the `Annotated` version if possible.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b_py310/main.py!}
-```
-
-////
-
-//// tab | Python 3.8+ non-Annotated
-
-/// tip
-
-Prefer to use the `Annotated` version if possible.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b/main.py!}
-```
-
-////
+{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
 
 ### Extended testing file { #extended-testing-file }
 
 You could then update `test_main.py` with the extended tests:
 
-{* ../../docs_src/app_testing/app_b/test_main.py *}
+{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
 
 
 Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `httpx`, or even how to do it with `requests`, as HTTPX's design is based on Requests' design.
diff --git a/docs_src/additional_responses/tutorial002_py310.py b/docs_src/additional_responses/tutorial002_py310.py
new file mode 100644 (file)
index 0000000..a94b740
--- /dev/null
@@ -0,0 +1,28 @@
+from fastapi import FastAPI
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+    id: str
+    value: str
+
+
+app = FastAPI()
+
+
+@app.get(
+    "/items/{item_id}",
+    response_model=Item,
+    responses={
+        200: {
+            "content": {"image/png": {}},
+            "description": "Return the JSON item or an image.",
+        }
+    },
+)
+async def read_item(item_id: str, img: bool | None = None):
+    if img:
+        return FileResponse("image.png", media_type="image/png")
+    else:
+        return {"id": "foo", "value": "there goes my hero"}
diff --git a/docs_src/additional_responses/tutorial004_py310.py b/docs_src/additional_responses/tutorial004_py310.py
new file mode 100644 (file)
index 0000000..65cbef6
--- /dev/null
@@ -0,0 +1,30 @@
+from fastapi import FastAPI
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+    id: str
+    value: str
+
+
+responses = {
+    404: {"description": "Item not found"},
+    302: {"description": "The item was moved"},
+    403: {"description": "Not enough privileges"},
+}
+
+
+app = FastAPI()
+
+
+@app.get(
+    "/items/{item_id}",
+    response_model=Item,
+    responses={**responses, 200: {"content": {"image/png": {}}}},
+)
+async def read_item(item_id: str, img: bool | None = None):
+    if img:
+        return FileResponse("image.png", media_type="image/png")
+    else:
+        return {"id": "foo", "value": "there goes my hero"}
diff --git a/docs_src/custom_request_and_route/tutorial001_an.py b/docs_src/custom_request_and_route/tutorial001_an.py
new file mode 100644 (file)
index 0000000..6224ba8
--- /dev/null
@@ -0,0 +1,36 @@
+import gzip
+from typing import Callable, List
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+from typing_extensions import Annotated
+
+
+class GzipRequest(Request):
+    async def body(self) -> bytes:
+        if not hasattr(self, "_body"):
+            body = await super().body()
+            if "gzip" in self.headers.getlist("Content-Encoding"):
+                body = gzip.decompress(body)
+            self._body = body
+        return self._body
+
+
+class GzipRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = GzipRequest(request.scope, request.receive)
+            return await original_route_handler(request)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[List[int], Body()]):
+    return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_an_py310.py b/docs_src/custom_request_and_route/tutorial001_an_py310.py
new file mode 100644 (file)
index 0000000..381bab6
--- /dev/null
@@ -0,0 +1,36 @@
+import gzip
+from collections.abc import Callable
+from typing import Annotated
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+    async def body(self) -> bytes:
+        if not hasattr(self, "_body"):
+            body = await super().body()
+            if "gzip" in self.headers.getlist("Content-Encoding"):
+                body = gzip.decompress(body)
+            self._body = body
+        return self._body
+
+
+class GzipRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = GzipRequest(request.scope, request.receive)
+            return await original_route_handler(request)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+    return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_an_py39.py b/docs_src/custom_request_and_route/tutorial001_an_py39.py
new file mode 100644 (file)
index 0000000..076727e
--- /dev/null
@@ -0,0 +1,35 @@
+import gzip
+from typing import Annotated, Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+    async def body(self) -> bytes:
+        if not hasattr(self, "_body"):
+            body = await super().body()
+            if "gzip" in self.headers.getlist("Content-Encoding"):
+                body = gzip.decompress(body)
+            self._body = body
+        return self._body
+
+
+class GzipRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = GzipRequest(request.scope, request.receive)
+            return await original_route_handler(request)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+    return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_py310.py b/docs_src/custom_request_and_route/tutorial001_py310.py
new file mode 100644 (file)
index 0000000..c678088
--- /dev/null
@@ -0,0 +1,35 @@
+import gzip
+from collections.abc import Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+    async def body(self) -> bytes:
+        if not hasattr(self, "_body"):
+            body = await super().body()
+            if "gzip" in self.headers.getlist("Content-Encoding"):
+                body = gzip.decompress(body)
+            self._body = body
+        return self._body
+
+
+class GzipRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = GzipRequest(request.scope, request.receive)
+            return await original_route_handler(request)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: list[int] = Body()):
+    return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_py39.py b/docs_src/custom_request_and_route/tutorial001_py39.py
new file mode 100644 (file)
index 0000000..54b20b9
--- /dev/null
@@ -0,0 +1,35 @@
+import gzip
+from typing import Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+    async def body(self) -> bytes:
+        if not hasattr(self, "_body"):
+            body = await super().body()
+            if "gzip" in self.headers.getlist("Content-Encoding"):
+                body = gzip.decompress(body)
+            self._body = body
+        return self._body
+
+
+class GzipRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = GzipRequest(request.scope, request.receive)
+            return await original_route_handler(request)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: list[int] = Body()):
+    return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial002_an.py b/docs_src/custom_request_and_route/tutorial002_an.py
new file mode 100644 (file)
index 0000000..127f7a9
--- /dev/null
@@ -0,0 +1,30 @@
+from typing import Callable, List
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+from typing_extensions import Annotated
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                body = await request.body()
+                detail = {"errors": exc.errors(), "body": body.decode()}
+                raise HTTPException(status_code=422, detail=detail)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[List[int], Body()]):
+    return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_an_py310.py b/docs_src/custom_request_and_route/tutorial002_an_py310.py
new file mode 100644 (file)
index 0000000..69b7de4
--- /dev/null
@@ -0,0 +1,30 @@
+from collections.abc import Callable
+from typing import Annotated
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                body = await request.body()
+                detail = {"errors": exc.errors(), "body": body.decode()}
+                raise HTTPException(status_code=422, detail=detail)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+    return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_an_py39.py b/docs_src/custom_request_and_route/tutorial002_an_py39.py
new file mode 100644 (file)
index 0000000..e7de09d
--- /dev/null
@@ -0,0 +1,29 @@
+from typing import Annotated, Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                body = await request.body()
+                detail = {"errors": exc.errors(), "body": body.decode()}
+                raise HTTPException(status_code=422, detail=detail)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+    return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_py310.py b/docs_src/custom_request_and_route/tutorial002_py310.py
new file mode 100644 (file)
index 0000000..13a5ca5
--- /dev/null
@@ -0,0 +1,29 @@
+from collections.abc import Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                body = await request.body()
+                detail = {"errors": exc.errors(), "body": body.decode()}
+                raise HTTPException(status_code=422, detail=detail)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: list[int] = Body()):
+    return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_py39.py b/docs_src/custom_request_and_route/tutorial002_py39.py
new file mode 100644 (file)
index 0000000..c4e4748
--- /dev/null
@@ -0,0 +1,29 @@
+from typing import Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                body = await request.body()
+                detail = {"errors": exc.errors(), "body": body.decode()}
+                raise HTTPException(status_code=422, detail=detail)
+
+        return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: list[int] = Body()):
+    return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial003_py310.py b/docs_src/custom_request_and_route/tutorial003_py310.py
new file mode 100644 (file)
index 0000000..f4e60be
--- /dev/null
@@ -0,0 +1,39 @@
+import time
+from collections.abc import Callable
+
+from fastapi import APIRouter, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class TimedRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            before = time.time()
+            response: Response = await original_route_handler(request)
+            duration = time.time() - before
+            response.headers["X-Response-Time"] = str(duration)
+            print(f"route duration: {duration}")
+            print(f"route response: {response}")
+            print(f"route response headers: {response.headers}")
+            return response
+
+        return custom_route_handler
+
+
+app = FastAPI()
+router = APIRouter(route_class=TimedRoute)
+
+
+@app.get("/")
+async def not_timed():
+    return {"message": "Not timed"}
+
+
+@router.get("/timed")
+async def timed():
+    return {"message": "It's the time of my life"}
+
+
+app.include_router(router)
diff --git a/docs_src/dataclasses/tutorial001_py310.py b/docs_src/dataclasses/tutorial001_py310.py
new file mode 100644 (file)
index 0000000..ab709a7
--- /dev/null
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+    name: str
+    price: float
+    description: str | None = None
+    tax: float | None = None
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Item):
+    return item
diff --git a/docs_src/dataclasses/tutorial002_py310.py b/docs_src/dataclasses/tutorial002_py310.py
new file mode 100644 (file)
index 0000000..e16249f
--- /dev/null
@@ -0,0 +1,25 @@
+from dataclasses import dataclass, field
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+    name: str
+    price: float
+    tags: list[str] = field(default_factory=list)
+    description: str | None = None
+    tax: float | None = None
+
+
+app = FastAPI()
+
+
+@app.get("/items/next", response_model=Item)
+async def read_next_item():
+    return {
+        "name": "Island In The Moon",
+        "price": 12.99,
+        "description": "A place to be playin' and havin' fun",
+        "tags": ["breater"],
+    }
diff --git a/docs_src/dataclasses/tutorial002_py39.py b/docs_src/dataclasses/tutorial002_py39.py
new file mode 100644 (file)
index 0000000..0c23765
--- /dev/null
@@ -0,0 +1,26 @@
+from dataclasses import dataclass, field
+from typing import Union
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+    name: str
+    price: float
+    tags: list[str] = field(default_factory=list)
+    description: Union[str, None] = None
+    tax: Union[float, None] = None
+
+
+app = FastAPI()
+
+
+@app.get("/items/next", response_model=Item)
+async def read_next_item():
+    return {
+        "name": "Island In The Moon",
+        "price": 12.99,
+        "description": "A place to be playin' and havin' fun",
+        "tags": ["breater"],
+    }
diff --git a/docs_src/dataclasses/tutorial003_py310.py b/docs_src/dataclasses/tutorial003_py310.py
new file mode 100644 (file)
index 0000000..9b9a3fd
--- /dev/null
@@ -0,0 +1,54 @@
+from dataclasses import field  # (1)
+
+from fastapi import FastAPI
+from pydantic.dataclasses import dataclass  # (2)
+
+
+@dataclass
+class Item:
+    name: str
+    description: str | None = None
+
+
+@dataclass
+class Author:
+    name: str
+    items: list[Item] = field(default_factory=list)  # (3)
+
+
+app = FastAPI()
+
+
+@app.post("/authors/{author_id}/items/", response_model=Author)  # (4)
+async def create_author_items(author_id: str, items: list[Item]):  # (5)
+    return {"name": author_id, "items": items}  # (6)
+
+
+@app.get("/authors/", response_model=list[Author])  # (7)
+def get_authors():  # (8)
+    return [  # (9)
+        {
+            "name": "Breaters",
+            "items": [
+                {
+                    "name": "Island In The Moon",
+                    "description": "A place to be playin' and havin' fun",
+                },
+                {"name": "Holy Buddies"},
+            ],
+        },
+        {
+            "name": "System of an Up",
+            "items": [
+                {
+                    "name": "Salt",
+                    "description": "The kombucha mushroom people's favorite",
+                },
+                {"name": "Pad Thai"},
+                {
+                    "name": "Lonely Night",
+                    "description": "The mostests lonliest nightiest of allest",
+                },
+            ],
+        },
+    ]
diff --git a/docs_src/dataclasses/tutorial003_py39.py b/docs_src/dataclasses/tutorial003_py39.py
new file mode 100644 (file)
index 0000000..991708c
--- /dev/null
@@ -0,0 +1,55 @@
+from dataclasses import field  # (1)
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic.dataclasses import dataclass  # (2)
+
+
+@dataclass
+class Item:
+    name: str
+    description: Union[str, None] = None
+
+
+@dataclass
+class Author:
+    name: str
+    items: list[Item] = field(default_factory=list)  # (3)
+
+
+app = FastAPI()
+
+
+@app.post("/authors/{author_id}/items/", response_model=Author)  # (4)
+async def create_author_items(author_id: str, items: list[Item]):  # (5)
+    return {"name": author_id, "items": items}  # (6)
+
+
+@app.get("/authors/", response_model=list[Author])  # (7)
+def get_authors():  # (8)
+    return [  # (9)
+        {
+            "name": "Breaters",
+            "items": [
+                {
+                    "name": "Island In The Moon",
+                    "description": "A place to be playin' and havin' fun",
+                },
+                {"name": "Holy Buddies"},
+            ],
+        },
+        {
+            "name": "System of an Up",
+            "items": [
+                {
+                    "name": "Salt",
+                    "description": "The kombucha mushroom people's favorite",
+                },
+                {"name": "Pad Thai"},
+                {
+                    "name": "Lonely Night",
+                    "description": "The mostests lonliest nightiest of allest",
+                },
+            ],
+        },
+    ]
diff --git a/docs_src/openapi_callbacks/tutorial001_py310.py b/docs_src/openapi_callbacks/tutorial001_py310.py
new file mode 100644 (file)
index 0000000..3efe0ee
--- /dev/null
@@ -0,0 +1,51 @@
+from fastapi import APIRouter, FastAPI
+from pydantic import BaseModel, HttpUrl
+
+app = FastAPI()
+
+
+class Invoice(BaseModel):
+    id: str
+    title: str | None = None
+    customer: str
+    total: float
+
+
+class InvoiceEvent(BaseModel):
+    description: str
+    paid: bool
+
+
+class InvoiceEventReceived(BaseModel):
+    ok: bool
+
+
+invoices_callback_router = APIRouter()
+
+
+@invoices_callback_router.post(
+    "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
+)
+def invoice_notification(body: InvoiceEvent):
+    pass
+
+
+@app.post("/invoices/", callbacks=invoices_callback_router.routes)
+def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None):
+    """
+    Create an invoice.
+
+    This will (let's imagine) let the API user (some external developer) create an
+    invoice.
+
+    And this path operation will:
+
+    * Send the invoice to the client.
+    * Collect the money from the client.
+    * Send a notification back to the API user (the external developer), as a callback.
+        * At this point is that the API will somehow send a POST request to the
+            external API with the notification of the invoice event
+            (e.g. "payment successful").
+    """
+    # Send the invoice, collect the money, send the notification (the callback)
+    return {"msg": "Invoice received"}
diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py310.py b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py
new file mode 100644 (file)
index 0000000..a815a56
--- /dev/null
@@ -0,0 +1,28 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    price: float
+    tax: float | None = None
+    tags: set[str] = set()
+
+
+@app.post("/items/", response_model=Item, summary="Create an item")
+async def create_item(item: Item):
+    """
+    Create an item with all the information:
+
+    - **name**: each item must have a name
+    - **description**: a long description
+    - **price**: required
+    - **tax**: if the item doesn't have tax, you can omit this
+    - **tags**: a set of unique tag strings for this item
+    \f
+    :param item: User input.
+    """
+    return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py39.py b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py
new file mode 100644 (file)
index 0000000..d5fe670
--- /dev/null
@@ -0,0 +1,30 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    price: float
+    tax: Union[float, None] = None
+    tags: set[str] = set()
+
+
+@app.post("/items/", response_model=Item, summary="Create an item")
+async def create_item(item: Item):
+    """
+    Create an item with all the information:
+
+    - **name**: each item must have a name
+    - **description**: a long description
+    - **price**: required
+    - **tax**: if the item doesn't have tax, you can omit this
+    - **tags**: a set of unique tag strings for this item
+    \f
+    :param item: User input.
+    """
+    return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py
new file mode 100644 (file)
index 0000000..8319665
--- /dev/null
@@ -0,0 +1,32 @@
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    tags: list[str]
+
+
+@app.post(
+    "/items/",
+    openapi_extra={
+        "requestBody": {
+            "content": {"application/x-yaml": {"schema": Item.schema()}},
+            "required": True,
+        },
+    },
+)
+async def create_item(request: Request):
+    raw_body = await request.body()
+    try:
+        data = yaml.safe_load(raw_body)
+    except yaml.YAMLError:
+        raise HTTPException(status_code=422, detail="Invalid YAML")
+    try:
+        item = Item.parse_obj(data)
+    except ValidationError as e:
+        raise HTTPException(status_code=422, detail=e.errors())
+    return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py
new file mode 100644 (file)
index 0000000..ff64ef7
--- /dev/null
@@ -0,0 +1,32 @@
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    tags: list[str]
+
+
+@app.post(
+    "/items/",
+    openapi_extra={
+        "requestBody": {
+            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
+            "required": True,
+        },
+    },
+)
+async def create_item(request: Request):
+    raw_body = await request.body()
+    try:
+        data = yaml.safe_load(raw_body)
+    except yaml.YAMLError:
+        raise HTTPException(status_code=422, detail="Invalid YAML")
+    try:
+        item = Item.model_validate(data)
+    except ValidationError as e:
+        raise HTTPException(status_code=422, detail=e.errors(include_url=False))
+    return item
diff --git a/docs_src/response_directly/tutorial001_py310.py b/docs_src/response_directly/tutorial001_py310.py
new file mode 100644 (file)
index 0000000..81e094d
--- /dev/null
@@ -0,0 +1,21 @@
+from datetime import datetime
+
+from fastapi import FastAPI
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+    title: str
+    timestamp: datetime
+    description: str | None = None
+
+
+app = FastAPI()
+
+
+@app.put("/items/{id}")
+def update_item(id: str, item: Item):
+    json_compatible_item_data = jsonable_encoder(item)
+    return JSONResponse(content=json_compatible_item_data)
index 942aea3e58b70090cd8c9bdc7e8071b92fb491dc..08f8f88c280ac1e61428b797322b290c2eefd3fb 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic_settings import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
 class Settings(BaseSettings):
@@ -6,5 +6,4 @@ class Settings(BaseSettings):
     admin_email: str
     items_per_user: int = 50
 
-    class Config:
-        env_file = ".env"
+    model_config = SettingsConfigDict(env_file=".env")
diff --git a/docs_src/settings/app03/config_pv1.py b/docs_src/settings/app03/config_pv1.py
new file mode 100644 (file)
index 0000000..e1c3ee3
--- /dev/null
@@ -0,0 +1,10 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    app_name: str = "Awesome API"
+    admin_email: str
+    items_per_user: int = 50
+
+    class Config:
+        env_file = ".env"
index 2f64b9cd175d8b0bbb3c8c4396cc506815ae12cd..62f3476396ff02b301c7c04f00679210577cafe5 100644 (file)
@@ -1,7 +1,7 @@
 from functools import lru_cache
-from typing import Annotated
 
 from fastapi import Depends, FastAPI
+from typing_extensions import Annotated
 
 from . import config
 
index 942aea3e58b70090cd8c9bdc7e8071b92fb491dc..08f8f88c280ac1e61428b797322b290c2eefd3fb 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic_settings import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
 class Settings(BaseSettings):
@@ -6,5 +6,4 @@ class Settings(BaseSettings):
     admin_email: str
     items_per_user: int = 50
 
-    class Config:
-        env_file = ".env"
+    model_config = SettingsConfigDict(env_file=".env")
diff --git a/docs_src/settings/app03_an_py39/config_pv1.py b/docs_src/settings/app03_an_py39/config_pv1.py
new file mode 100644 (file)
index 0000000..e1c3ee3
--- /dev/null
@@ -0,0 +1,10 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    app_name: str = "Awesome API"
+    admin_email: str
+    items_per_user: int = 50
+
+    class Config:
+        env_file = ".env"
index 62f3476396ff02b301c7c04f00679210577cafe5..2f64b9cd175d8b0bbb3c8c4396cc506815ae12cd 100644 (file)
@@ -1,7 +1,7 @@
 from functools import lru_cache
+from typing import Annotated
 
 from fastapi import Depends, FastAPI
-from typing_extensions import Annotated
 
 from . import config
 
index cafcf65c636f24d36bc00503d5bf932deb31c646..f8d5fa7c7ac9effb50c3222d429acbe7f6cc555f 100644 (file)
@@ -236,8 +236,15 @@ ignore = [
 "docs_src/custom_response/tutorial007.py" = ["B007"]
 "docs_src/dataclasses/tutorial003.py" = ["I001"]
 "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_py39.py" = ["B904"]
 "docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py" = ["B904"]
 "docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_py39.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_py310.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an_py39.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an_py310.py" = ["B904"]
 "docs_src/dependencies/tutorial008_an.py" = ["F821"]
 "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]
 "docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"]
index 588a3160a93e9adda7608e9dfe6981d6ebc88351..91d6ff101f760e6ebb5e7cd054d895a1544b46db 100644 (file)
@@ -1,21 +1,36 @@
+import importlib
 import os
 import shutil
 
+import pytest
 from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from docs_src.additional_responses.tutorial002 import app
+from tests.utils import needs_py310
 
-client = TestClient(app)
 
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.additional_responses.{request.param}")
 
-def test_path_operation():
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
+
+
+def test_path_operation(client: TestClient):
     response = client.get("/items/foo")
     assert response.status_code == 200, response.text
     assert response.json() == {"id": "foo", "value": "there goes my hero"}
 
 
-def test_path_operation_img():
+def test_path_operation_img(client: TestClient):
     shutil.copy("./docs/en/docs/img/favicon.png", "./image.png")
     response = client.get("/items/foo?img=1")
     assert response.status_code == 200, response.text
@@ -24,7 +39,7 @@ def test_path_operation_img():
     os.remove("./image.png")
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
index 55b556d8e19b87cfe6bdb777657813b11cef5abe..2d9491467e321e662594d3114176dad7d8ded7a3 100644 (file)
@@ -1,21 +1,36 @@
+import importlib
 import os
 import shutil
 
+import pytest
 from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from docs_src.additional_responses.tutorial004 import app
+from tests.utils import needs_py310
 
-client = TestClient(app)
 
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.additional_responses.{request.param}")
 
-def test_path_operation():
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
+
+
+def test_path_operation(client: TestClient):
     response = client.get("/items/foo")
     assert response.status_code == 200, response.text
     assert response.json() == {"id": "foo", "value": "there goes my hero"}
 
 
-def test_path_operation_img():
+def test_path_operation_img(client: TestClient):
     shutil.copy("./docs/en/docs/img/favicon.png", "./image.png")
     response = client.get("/items/foo?img=1")
     assert response.status_code == 200, response.text
@@ -24,7 +39,7 @@ def test_path_operation_img():
     os.remove("./image.png")
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
index e6da630e8813a210bdddf4570d9ae211c11c52e7..f9fd0d1af88e962616bac52dc4d8152cc530e3ff 100644 (file)
@@ -1,23 +1,38 @@
 import gzip
+import importlib
 import json
 
 import pytest
 from fastapi import Request
 from fastapi.testclient import TestClient
 
-from docs_src.custom_request_and_route.tutorial001 import app
+from tests.utils import needs_py39, needs_py310
 
 
-@app.get("/check-class")
-async def check_gzip_request(request: Request):
-    return {"request_class": type(request).__name__}
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001"),
+        pytest.param("tutorial001_py39", marks=needs_py39),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+        pytest.param("tutorial001_an"),
+        pytest.param("tutorial001_an_py39", marks=needs_py39),
+        pytest.param("tutorial001_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
 
+    @mod.app.get("/check-class")
+    async def check_gzip_request(request: Request):
+        return {"request_class": type(request).__name__}
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    return client
 
 
 @pytest.mark.parametrize("compress", [True, False])
-def test_gzip_request(compress):
+def test_gzip_request(client: TestClient, compress):
     n = 1000
     headers = {}
     body = [1] * n
@@ -30,6 +45,6 @@ def test_gzip_request(compress):
     assert response.json() == {"sum": n}
 
 
-def test_request_class():
+def test_request_class(client: TestClient):
     response = client.get("/check-class")
     assert response.json() == {"request_class": "GzipRequest"}
index 647f1c5ddf126bbf04ef4e1afd2efa621783d1d7..c35752ed135b63738379901af60ac3f435495ea0 100644 (file)
@@ -1,17 +1,36 @@
+import importlib
+
+import pytest
 from dirty_equals import IsDict, IsOneOf
 from fastapi.testclient import TestClient
 
-from docs_src.custom_request_and_route.tutorial002 import app
+from tests.utils import needs_py39, needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002"),
+        pytest.param("tutorial002_py39", marks=needs_py39),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+        pytest.param("tutorial002_an"),
+        pytest.param("tutorial002_an_py39", marks=needs_py39),
+        pytest.param("tutorial002_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    return client
 
 
-def test_endpoint_works():
+def test_endpoint_works(client: TestClient):
     response = client.post("/", json=[1, 2, 3])
     assert response.json() == 6
 
 
-def test_exception_handler_body_access():
+def test_exception_handler_body_access(client: TestClient):
     response = client.post("/", json={"numbers": [1, 2, 3]})
     assert response.json() == IsDict(
         {
index db5dad7cf666d114507a88929b95b3a6fe716832..9e895b2dafc2d46fffb35e4f6547750c47249964 100644 (file)
@@ -1,17 +1,32 @@
+import importlib
+
+import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.custom_request_and_route.tutorial003 import app
+from tests.utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    return client
 
 
-def test_get():
+def test_get(client: TestClient):
     response = client.get("/")
     assert response.json() == {"message": "Not timed"}
     assert "X-Response-Time" not in response.headers
 
 
-def test_get_timed():
+def test_get_timed(client: TestClient):
     response = client.get("/timed")
     assert response.json() == {"message": "It's the time of my life"}
     assert "X-Response-Time" in response.headers
index 762654d29d77d818d27918292813d9e2e2ae8d6e..b36dee76842ecde9a7e50a2c326ac978461a7b4c 100644 (file)
@@ -1,12 +1,28 @@
+import importlib
+
+import pytest
 from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from docs_src.dataclasses.tutorial001 import app
+from tests.utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
 
 
-def test_post_item():
+def test_post_item(client: TestClient):
     response = client.post("/items/", json={"name": "Foo", "price": 3})
     assert response.status_code == 200
     assert response.json() == {
@@ -17,7 +33,7 @@ def test_post_item():
     }
 
 
-def test_post_invalid_item():
+def test_post_invalid_item(client: TestClient):
     response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
     assert response.status_code == 422
     assert response.json() == IsDict(
@@ -45,7 +61,7 @@ def test_post_invalid_item():
     )
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
index e6d303cfc1e5ba1219b3cbb179c0e3872633995c..baaea45d8ffd7ed5451648cf8239ce5b4f5c93f7 100644 (file)
@@ -1,12 +1,29 @@
+import importlib
+
+import pytest
 from dirty_equals import IsDict, IsOneOf
 from fastapi.testclient import TestClient
 
-from docs_src.dataclasses.tutorial002 import app
+from tests.utils import needs_py39, needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002"),
+        pytest.param("tutorial002_py39", marks=needs_py39),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
 
 
-def test_get_item():
+def test_get_item(client: TestClient):
     response = client.get("/items/next")
     assert response.status_code == 200
     assert response.json() == {
@@ -18,7 +35,7 @@ def test_get_item():
     }
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
index e1fa45201f2a49eb2b786a7e3455fb4172db0b78..5728d2b6b358409c3d0bb425c940c883379ca9d1 100644 (file)
@@ -1,13 +1,28 @@
+import importlib
+
+import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.dataclasses.tutorial003 import app
+from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
+
 
-from ...utils import needs_pydanticv1, needs_pydanticv2
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003"),
+        pytest.param("tutorial003_py39", marks=needs_py39),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
 
 
-def test_post_authors_item():
+def test_post_authors_item(client: TestClient):
     response = client.post(
         "/authors/foo/items/",
         json=[{"name": "Bar"}, {"name": "Baz", "description": "Drop the Baz"}],
@@ -22,7 +37,7 @@ def test_post_authors_item():
     }
 
 
-def test_get_authors():
+def test_get_authors(client: TestClient):
     response = client.get("/authors/")
     assert response.status_code == 200
     assert response.json() == [
@@ -54,7 +69,7 @@ def test_get_authors():
 
 
 @needs_pydanticv2
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
@@ -191,7 +206,7 @@ def test_openapi_schema():
 
 # TODO: remove when deprecating Pydantic v1
 @needs_pydanticv1
-def test_openapi_schema_pv1():
+def test_openapi_schema_pv1(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
index 73af420ae1eff7c2e16a58ba3a776864f901d5cf..2df2b98893c9ca37d899646aa28740919aa979c4 100644 (file)
@@ -1,12 +1,33 @@
+import importlib
+from types import ModuleType
+
+import pytest
 from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification
+from tests.utils import needs_py310
+
+
+@pytest.fixture(
+    name="mod",
+    params=[
+        pytest.param("tutorial001"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_mod(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.openapi_callbacks.{request.param}")
+    return mod
+
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client(mod: ModuleType):
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
 
 
-def test_get():
+def test_get(client: TestClient):
     response = client.post(
         "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
     )
@@ -14,12 +35,12 @@ def test_get():
     assert response.json() == {"msg": "Invoice received"}
 
 
-def test_dummy_callback():
+def test_dummy_callback(mod: ModuleType):
     # Just for coverage
-    invoice_notification({})
+    mod.invoice_notification({})
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
index 4f69e4646c980bc94a8976bc20e78321f3148575..da5782d189e1d35cdad763027c00a880c7702fbf 100644 (file)
@@ -1,13 +1,30 @@
+import importlib
+
+import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.path_operation_advanced_configuration.tutorial004 import app
+from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
+
 
-from ...utils import needs_pydanticv1, needs_pydanticv2
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004"),
+        pytest.param("tutorial004_py39", marks=needs_py39),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.path_operation_advanced_configuration.{request.param}"
+    )
 
-client = TestClient(app)
+    client = TestClient(mod.app)
+    client.headers.clear()
+    return client
 
 
-def test_query_params_str_validations():
+def test_query_params_str_validations(client: TestClient):
     response = client.post("/items/", json={"name": "Foo", "price": 42})
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -20,7 +37,7 @@ def test_query_params_str_validations():
 
 
 @needs_pydanticv2
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -123,7 +140,7 @@ def test_openapi_schema():
 
 # TODO: remove when deprecating Pydantic v1
 @needs_pydanticv1
-def test_openapi_schema_pv1():
+def test_openapi_schema_pv1(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
index 8240b60a62da9737dbca0a92dc99bb3cd34713cc..a90337a63dd70d5711f3f76651ee25055f35fe33 100644 (file)
@@ -1,14 +1,24 @@
+import importlib
+
 import pytest
 from fastapi.testclient import TestClient
 
-from ...utils import needs_pydanticv2
+from ...utils import needs_py39, needs_pydanticv2
 
 
-@pytest.fixture(name="client")
-def get_client():
-    from docs_src.path_operation_advanced_configuration.tutorial007 import app
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial007"),
+        pytest.param("tutorial007_py39", marks=needs_py39),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.path_operation_advanced_configuration.{request.param}"
+    )
 
-    client = TestClient(app)
+    client = TestClient(mod.app)
     return client
 
 
index ef012f8a6cc8c7f931c4a849851d744bb1d4fe57..b38e4947cca5024df111754886f758a10c2c7bd0 100644 (file)
@@ -1,14 +1,24 @@
+import importlib
+
 import pytest
 from fastapi.testclient import TestClient
 
-from ...utils import needs_pydanticv1
+from ...utils import needs_py39, needs_pydanticv1
 
 
-@pytest.fixture(name="client")
-def get_client():
-    from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial007_pv1"),
+        pytest.param("tutorial007_pv1_py39", marks=needs_py39),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.path_operation_advanced_configuration.{request.param}"
+    )
 
-    client = TestClient(app)
+    client = TestClient(mod.app)
     return client
 
 
diff --git a/tests/test_tutorial/test_response_directly/__init__.py b/tests/test_tutorial/test_response_directly/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py
new file mode 100644 (file)
index 0000000..2cc4f3b
--- /dev/null
@@ -0,0 +1,288 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.response_directly.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_path_operation(client: TestClient):
+    response = client.put(
+        "/items/1",
+        json={
+            "title": "Foo",
+            "timestamp": "2023-01-01T12:00:00",
+            "description": "A test item",
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "description": "A test item",
+        "timestamp": "2023-01-01T12:00:00",
+        "title": "Foo",
+    }
+
+
+@needs_pydanticv2
+def test_openapi_schema_pv2(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/{id}": {
+                "put": {
+                    "operationId": "update_item_items__id__put",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "id",
+                            "required": True,
+                            "schema": {"title": "Id", "type": "string"},
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {"schema": {}},
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Update Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "anyOf": [
+                                {"type": "string"},
+                                {"type": "null"},
+                            ],
+                            "title": "Description",
+                        },
+                        "timestamp": {
+                            "format": "date-time",
+                            "title": "Timestamp",
+                            "type": "string",
+                        },
+                        "title": {"title": "Title", "type": "string"},
+                    },
+                    "required": [
+                        "title",
+                        "timestamp",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {"type": "string"},
+                                    {"type": "integer"},
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
+
+
+@needs_pydanticv1
+def test_openapi_schema_pv1(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/{id}": {
+                "put": {
+                    "operationId": "update_item_items__id__put",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "id",
+                            "required": True,
+                            "schema": {
+                                "title": "Id",
+                                "type": "string",
+                            },
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Update Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "title": "Description",
+                            "type": "string",
+                        },
+                        "timestamp": {
+                            "format": "date-time",
+                            "title": "Timestamp",
+                            "type": "string",
+                        },
+                        "title": {
+                            "title": "Title",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "title",
+                        "timestamp",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
index eced88c044ba4032b34dc0b7f2aaa1e479c837bf..5e1232ea01553dd7604e6465ef82b40dd260d90c 100644 (file)
@@ -1,20 +1,45 @@
+import importlib
+from types import ModuleType
+
+import pytest
 from pytest import MonkeyPatch
 
-from ...utils import needs_pydanticv2
+from ...utils import needs_py39, needs_pydanticv2
 
 
-@needs_pydanticv2
-def test_settings(monkeypatch: MonkeyPatch):
-    from docs_src.settings.app02 import main
+@pytest.fixture(
+    name="mod_path",
+    params=[
+        pytest.param("app02"),
+        pytest.param("app02_an"),
+        pytest.param("app02_an_py39", marks=needs_py39),
+    ],
+)
+def get_mod_path(request: pytest.FixtureRequest):
+    mod_path = f"docs_src.settings.{request.param}"
+    return mod_path
+
+
+@pytest.fixture(name="main_mod")
+def get_main_mod(mod_path: str) -> ModuleType:
+    main_mod = importlib.import_module(f"{mod_path}.main")
+    return main_mod
+
 
+@pytest.fixture(name="test_main_mod")
+def get_test_main_mod(mod_path: str) -> ModuleType:
+    test_main_mod = importlib.import_module(f"{mod_path}.test_main")
+    return test_main_mod
+
+
+@needs_pydanticv2
+def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch):
     monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
-    settings = main.get_settings()
+    settings = main_mod.get_settings()
     assert settings.app_name == "Awesome API"
     assert settings.items_per_user == 50
 
 
 @needs_pydanticv2
-def test_override_settings():
-    from docs_src.settings.app02 import test_main
-
-    test_main.test_app()
+def test_override_settings(test_main_mod: ModuleType):
+    test_main_mod.test_app()
diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py
new file mode 100644 (file)
index 0000000..d9872c1
--- /dev/null
@@ -0,0 +1,59 @@
+import importlib
+from types import ModuleType
+
+import pytest
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2
+
+
+@pytest.fixture(
+    name="mod_path",
+    params=[
+        pytest.param("app03"),
+        pytest.param("app03_an"),
+        pytest.param("app03_an_py39", marks=needs_py39),
+    ],
+)
+def get_mod_path(request: pytest.FixtureRequest):
+    mod_path = f"docs_src.settings.{request.param}"
+    return mod_path
+
+
+@pytest.fixture(name="main_mod")
+def get_main_mod(mod_path: str) -> ModuleType:
+    main_mod = importlib.import_module(f"{mod_path}.main")
+    return main_mod
+
+
+@needs_pydanticv2
+def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch):
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    settings = main_mod.get_settings()
+    assert settings.app_name == "Awesome API"
+    assert settings.admin_email == "admin@example.com"
+    assert settings.items_per_user == 50
+
+
+@needs_pydanticv1
+def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch):
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    config_mod = importlib.import_module(f"{mod_path}.config_pv1")
+    settings = config_mod.Settings()
+    assert settings.app_name == "Awesome API"
+    assert settings.admin_email == "admin@example.com"
+    assert settings.items_per_user == 50
+
+
+@needs_pydanticv2
+def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch):
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    client = TestClient(main_mod.app)
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.json() == {
+        "app_name": "Awesome API",
+        "admin_email": "admin@example.com",
+        "items_per_user": 50,
+    }
diff --git a/tests/test_tutorial/test_using_request_directly/__init__.py b/tests/test_tutorial/test_using_request_directly/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py
new file mode 100644 (file)
index 0000000..54c53ae
--- /dev/null
@@ -0,0 +1,112 @@
+from fastapi.testclient import TestClient
+
+from docs_src.using_request_directly.tutorial001 import app
+
+client = TestClient(app)
+
+
+def test_path_operation():
+    response = client.get("/items/foo")
+    assert response.status_code == 200
+    assert response.json() == {"client_host": "testclient", "item_id": "foo"}
+
+
+def test_openapi():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "operationId": "read_root_items__item_id__get",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "string",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Read Root",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
index dbcad3b02c94ce1a040df0b0beb729f16cf0c6fe..85efc185901492e8037f57a991eb1910fd42e23d 100644 (file)
@@ -1,16 +1,45 @@
+import importlib
+from types import ModuleType
+
+import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.websockets.tutorial003 import app, html
+from ...utils import needs_py39
+
+
+@pytest.fixture(
+    name="mod",
+    params=[
+        pytest.param("tutorial003"),
+        pytest.param("tutorial003_py39", marks=needs_py39),
+    ],
+)
+def get_mod(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.websockets.{request.param}")
+
+    return mod
+
+
+@pytest.fixture(name="html")
+def get_html(mod: ModuleType):
+    return mod.html
+
+
+@pytest.fixture(name="client")
+def get_client(mod: ModuleType):
+    client = TestClient(mod.app)
 
-client = TestClient(app)
+    return client
 
 
-def test_get():
+@needs_py39
+def test_get(client: TestClient, html: str):
     response = client.get("/")
     assert response.text == html
 
 
-def test_websocket_handle_disconnection():
+@needs_py39
+def test_websocket_handle_disconnection(client: TestClient):
     with client.websocket_connect("/ws/1234") as connection, client.websocket_connect(
         "/ws/5678"
     ) as connection_two:
diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py
deleted file mode 100644 (file)
index 06c4a92..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-import pytest
-from fastapi import FastAPI
-from fastapi.testclient import TestClient
-
-from ...utils import needs_py39
-
-
-@pytest.fixture(name="app")
-def get_app():
-    from docs_src.websockets.tutorial003_py39 import app
-
-    return app
-
-
-@pytest.fixture(name="html")
-def get_html():
-    from docs_src.websockets.tutorial003_py39 import html
-
-    return html
-
-
-@pytest.fixture(name="client")
-def get_client(app: FastAPI):
-    client = TestClient(app)
-
-    return client
-
-
-@needs_py39
-def test_get(client: TestClient, html: str):
-    response = client.get("/")
-    assert response.text == html
-
-
-@needs_py39
-def test_websocket_handle_disconnection(client: TestClient):
-    with client.websocket_connect("/ws/1234") as connection, client.websocket_connect(
-        "/ws/5678"
-    ) as connection_two:
-        connection.send_text("Hello from 1234")
-        data1 = connection.receive_text()
-        assert data1 == "You wrote: Hello from 1234"
-        data2 = connection_two.receive_text()
-        client1_says = "Client #1234 says: Hello from 1234"
-        assert data2 == client1_says
-        data1 = connection.receive_text()
-        assert data1 == client1_says
-        connection_two.close()
-        data1 = connection.receive_text()
-        assert data1 == "Client #5678 left the chat"