]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add parameter dependencies to path operation decorators and include_router...
authorSebastián Ramírez <tiangolo@gmail.com>
Thu, 16 May 2019 14:07:00 +0000 (18:07 +0400)
committerGitHub <noreply@github.com>
Thu, 16 May 2019 14:07:00 +0000 (18:07 +0400)
* :sparkles: Implement dependencies in decorator and .include_router

* :memo: Add docs for parameter dependencies

* :white_check_mark: Add tests for dependencies parameter

* :fire: Remove debugging prints in tests

* :memo: Update release notes

16 files changed:
docs/release-notes.md
docs/src/bigger_applications/app/main.py
docs/src/dependencies/tutorial006.py
docs/src/dependencies/tutorial007.py [new file with mode: 0644]
docs/tutorial/bigger-applications.md
docs/tutorial/dependencies/advanced-dependencies.md
docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md [new file with mode: 0644]
docs/tutorial/security/oauth2-scopes.md
fastapi/applications.py
fastapi/dependencies/utils.py
fastapi/routing.py
mkdocs.yml
tests/test_tutorial/test_bigger_applications/test_main.py
tests/test_tutorial/test_dependencies/test_tutorial006.py [new file with mode: 0644]
tests/test_tutorial/test_request_files/test_tutorial002.py
tests/test_tutorial/test_security/test_tutorial005.py

index 180f8c6d589ade99fc10d3557a70345168178566..eb8506069088aa54348cc43901466a1d6843783b 100644 (file)
@@ -1,5 +1,17 @@
 ## Next release
 
+* Add support for `dependencies` parameter:
+    * A parameter in *path operation decorators*, for dependencies that should be executed but the return value is not important or not used in the *path operation function*.
+    * A parameter in the `.include_router()` method of FastAPI applications and routers, to include dependencies that should be executed in each *path operation* in a router.
+        * This is useful, for example, to require authentication or permissions in specific group of *path operations*.
+        * Different `dependencies` can be applied to different routers.
+    * These `dependencies` are run before the normal parameter dependencies. And normal dependencies are run too. They can be combined.
+    * Dependencies declared in a router are executed first, then the ones defined in *path operation decorators*, and then the ones declared in normal parameters. They are all combined and executed.
+    * All this also supports using `Security` with `scopes` in those `dependencies` parameters, for more advanced OAuth 2.0 security scenarios with scopes.
+    * New documentation about [dependencies in *path operation decorators*](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
+    * New documentation about [dependencies in the `include_router()` method](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-prefix-tags-responses-and-dependencies).
+    * PR [#235](https://github.com/tiangolo/fastapi/pull/235).
+
 * Fix OpenAPI documentation of Starlette URL convertors. Specially useful when using `path` convertors, to take a whole path as a parameter, like `/some/url/{p:path}`. PR [#234](https://github.com/tiangolo/fastapi/pull/234) by [@euri10](https://github.com/euri10).
 
 * Make default parameter utilities exported from `fastapi` be functions instead of classes (the new functions return instances of those classes). To be able to override the return types and fix `mypy` errors in FastAPI's users' code. Applies to `Path`, `Query`, `Header`, `Cookie`, `Body`, `Form`, `File`, `Depends`, and `Security`. PR [#226](https://github.com/tiangolo/fastapi/pull/226) and PR [#231](https://github.com/tiangolo/fastapi/pull/231).
index 2cebd4244a1a7acf37823160702e4d15a098e50f..fdff13947b908593da64c12857b3535a6db5e382 100644 (file)
@@ -1,13 +1,20 @@
-from fastapi import FastAPI
+from fastapi import Depends, FastAPI, Header, HTTPException
 
 from .routers import items, users
 
 app = FastAPI()
 
+
+async def get_token_header(x_token: str = Header(...)):
+    if x_token != "fake-super-secret-token":
+        raise HTTPException(status_code=400, detail="X-Token header invalid")
+
+
 app.include_router(users.router)
 app.include_router(
     items.router,
     prefix="/items",
     tags=["items"],
+    dependencies=[Depends(get_token_header)],
     responses={404: {"description": "Not found"}},
 )
index 5d22f6823785c058849176f4c1de29a5382ab166..a71d7cce6ea5d3df6b7debcf58460a5f62fe046b 100644 (file)
@@ -1,21 +1,19 @@
-from fastapi import Depends, FastAPI
+from fastapi import Depends, FastAPI, Header, HTTPException
 
 app = FastAPI()
 
 
-class FixedContentQueryChecker:
-    def __init__(self, fixed_content: str):
-        self.fixed_content = fixed_content
+async def verify_token(x_token: str = Header(...)):
+    if x_token != "fake-super-secret-token":
+        raise HTTPException(status_code=400, detail="X-Token header invalid")
 
-    def __call__(self, q: str = ""):
-        if q:
-            return self.fixed_content in q
-        return False
 
+async def verify_key(x_key: str = Header(...)):
+    if x_key != "fake-super-secret-key":
+        raise HTTPException(status_code=400, detail="X-Key header invalid")
+    return x_key
 
-checker = FixedContentQueryChecker("bar")
 
-
-@app.get("/query-checker/")
-async def read_query_check(fixed_content_included: bool = Depends(checker)):
-    return {"fixed_content_in_query": fixed_content_included}
+@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
+async def read_items():
+    return [{"item": "Foo"}, {"item": "Bar"}]
diff --git a/docs/src/dependencies/tutorial007.py b/docs/src/dependencies/tutorial007.py
new file mode 100644 (file)
index 0000000..5d22f68
--- /dev/null
@@ -0,0 +1,21 @@
+from fastapi import Depends, FastAPI
+
+app = FastAPI()
+
+
+class FixedContentQueryChecker:
+    def __init__(self, fixed_content: str):
+        self.fixed_content = fixed_content
+
+    def __call__(self, q: str = ""):
+        if q:
+            return self.fixed_content in q
+        return False
+
+
+checker = FixedContentQueryChecker("bar")
+
+
+@app.get("/query-checker/")
+async def read_query_check(fixed_content_included: bool = Depends(checker)):
+    return {"fixed_content_in_query": fixed_content_included}
index 7adbab77c7d308d8970591cd56826f6cf87009ad..f9e2f6aa7e4c1df82f52e8e6a8652e4a162ecbb0 100644 (file)
@@ -22,16 +22,15 @@ Let's say you have a file structure like this:
 
 !!! tip
     There are two `__init__.py` files: one in each directory or subdirectory.
-    
+
     This is what allows importing code from one file into another.
 
     For example, in `app/main.py` you could have a line like:
-    
+
     ```
     from app.routers import items
     ```
 
-
 * The `app` directory contains everything.
 * This `app` directory has an empty file `app/__init__.py`.
     * So, the `app` directory is a "Python package" (a collection of "Python modules").
@@ -107,7 +106,7 @@ And we don't want to have to explicitly type `/items/` and `tags=["items"]` in e
 {!./src/bigger_applications/app/routers/items.py!}
 ```
 
-### Add some custom `tags` and `responses`
+### Add some custom `tags`, `responses`, and `dependencies`
 
 We are not adding the prefix `/items/` nor the `tags=["items"]` to add them later.
 
@@ -197,12 +196,11 @@ So, to be able to use both of them in the same file, we import the submodules di
 {!./src/bigger_applications/app/main.py!}
 ```
 
-
 ### Include an `APIRouter`
 
 Now, let's include the `router` from the submodule `users`:
 
-```Python hl_lines="7"
+```Python hl_lines="13"
 {!./src/bigger_applications/app/main.py!}
 ```
 
@@ -221,13 +219,12 @@ It will include all the routes from that router as part of it.
 
 !!! check
     You don't have to worry about performance when including routers.
-    
+
     This will take microseconds and will only happen at startup.
-    
-    So it won't affect performance.
 
+    So it won't affect performance.
 
-### Include an `APIRouter` with a `prefix`, `tags`, and `responses`
+### Include an `APIRouter` with a `prefix`, `tags`, `responses`, and `dependencies`
 
 Now, let's include the router from the `items` submodule.
 
@@ -251,7 +248,9 @@ We can also add a list of `tags` that will be applied to all the *path operation
 
 And we can add predefined `responses` that will be included in all the *path operations* too.
 
-```Python hl_lines="8 9 10 11 12 13"
+And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them.
+
+```Python hl_lines="8 9 10 14 15 16 17 18 19 20"
 {!./src/bigger_applications/app/main.py!}
 ```
 
@@ -262,27 +261,28 @@ The end result is that the item paths are now:
 
 ...as we intended.
 
-They will be marked with a list of tags that contain a single string `"items"`.
+* They will be marked with a list of tags that contain a single string `"items"`.
+* The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
+    * These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
+* All of them will include the predefined `responses`.
+* The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
+* All these *path operations* will have the list of `dependencies` evaluated/executed before them.
+    * If you also declare dependencies in a specific *path operation*, **they will be executed too**.
+    * The router dependencies are executed first, then the <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`dependencies` in the decorator</a>, and then the normal parameter dependencies.
+    * You can also add <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/" target="_blank">`Security` dependencies with `scopes`</a>.
 
-The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
-
-These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
-
-And all of them will include the the predefined `responses`.
-
-The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
+!!! tip
+    Having `dependencies` in a decorator can be used, for example, to require authentication for a whole group of *path operations*. Even if the dependencies are not added individually to each one of them.
 
 !!! check
-    The `prefix`, `tags`, and `responses` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
-
+    The `prefix`, `tags`, `responses` and `dependencies` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
 
 !!! tip
     You could also add path operations directly, for example with: `@app.get(...)`.
-    
+
     Apart from `app.include_router()`, in the same **FastAPI** app.
-    
-    It would still work the same.
 
+    It would still work the same.
 
 !!! info "Very Technical Details"
     **Note**: this is a very technical detail that you probably can **just skip**.
index 8240064412b086799abddf6e26c77bb36a3a4944..903090f238a37793ebfa26c9c537a0b12c613056 100644 (file)
@@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
 To do that, we declare a method `__call__`:
 
 ```Python hl_lines="10"
-{!./src/dependencies/tutorial006.py!}
+{!./src/dependencies/tutorial007.py!}
 ```
 
 In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
 And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
 
 ```Python hl_lines="7"
-{!./src/dependencies/tutorial006.py!}
+{!./src/dependencies/tutorial007.py!}
 ```
 
 In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
 We could create an instance of this class with:
 
 ```Python hl_lines="16"
-{!./src/dependencies/tutorial006.py!}
+{!./src/dependencies/tutorial007.py!}
 ```
 
 And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@@ -60,7 +60,7 @@ checker(q="somequery")
 ...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
 
 ```Python hl_lines="20"
-{!./src/dependencies/tutorial006.py!}
+{!./src/dependencies/tutorial007.py!}
 ```
 
 !!! tip
diff --git a/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md
new file mode 100644 (file)
index 0000000..477bb3d
--- /dev/null
@@ -0,0 +1,60 @@
+In some cases you don't really need the return value of a dependency inside your *path operation function*.
+
+Or the dependency doesn't return a value.
+
+But you still need it to be executed/solved.
+
+For those cases, instead of declaring a *path operation function* parameter with `Depends`, you can add a `list` of `dependencies` to the *path operation decorator*.
+
+## Add `dependencies` to the *path operation decorator*
+
+The *path operation decorator* receives an optional argument `dependencies`.
+
+It should be a `list` of `Depends()`:
+
+```Python hl_lines="17"
+{!./src/dependencies/tutorial006.py!}
+```
+
+These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*.
+
+!!! tip
+    Some editors check for unused function parameters, and show them as errors.
+
+    Using these `dependencies` in the *path operation decorator* you can make sure they are executed while avoiding editor/tooling errors.
+
+    It might also help avoiding confusion for new developers that see an un-used parameter in your code and could think it's unnecessary.
+
+## Dependencies errors and return values
+
+You can use the same dependency *functions* you use normally.
+
+### Dependency requirements
+
+They can declare request requirements (like headers) or other sub-dependencies:
+
+```Python hl_lines="6 11"
+{!./src/dependencies/tutorial006.py!}
+```
+
+### Raise exceptions
+
+These dependencies can `raise` exceptions, the same as normal dependencies:
+
+```Python hl_lines="8 13"
+{!./src/dependencies/tutorial006.py!}
+```
+
+### Return values
+
+And they can return values or not, the values won't be used.
+
+So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed:
+
+```Python hl_lines="9 14"
+{!./src/dependencies/tutorial006.py!}
+```
+
+## Dependencies for a group of *path operations*
+
+Later, when reading about how to <a href="https://fastapi.tiangolo.com/tutorial/bigger-applications/" target="_blank">structure bigger applications</a>, possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*.
index ef4f6798ec469d21a21c5923418faca446cba30e..89c973e8f152617c5dd850fa20f61bed2ea29e7b 100644 (file)
@@ -244,3 +244,7 @@ The most secure is the code flow, but is more complex to implement as it require
     But in the end, they are implementing the same OAuth2 standard.
 
 **FastAPI** includes utilities for all these OAuth2 authentication flows in `fastapi.security.oauth2`.
+
+## `Security` in decorator `dependencies`
+
+The same way you can define a `list` of <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`Depends` in the decorator's `dependencies` parameter</a>, you could also use `Security` with `scopes` there.
index 076ebfcc69e50de4c2fdcc4f1aea8ca699f0db72..e4b9ab967be1b6a47bcb1adeaf5e64135dcabee6 100644 (file)
@@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union
 from fastapi import routing
 from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
 from fastapi.openapi.utils import get_openapi
+from fastapi.params import Depends
 from pydantic import BaseModel
 from starlette.applications import Starlette
 from starlette.exceptions import ExceptionMiddleware, HTTPException
@@ -111,6 +112,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -128,6 +130,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -147,6 +150,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -165,6 +169,7 @@ class FastAPI(Starlette):
                 response_model=response_model,
                 status_code=status_code,
                 tags=tags or [],
+                dependencies=dependencies or [],
                 summary=summary,
                 description=description,
                 response_description=response_description,
@@ -186,10 +191,15 @@ class FastAPI(Starlette):
         *,
         prefix: str = "",
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
     ) -> None:
         self.router.include_router(
-            router, prefix=prefix, tags=tags, responses=responses or {}
+            router,
+            prefix=prefix,
+            tags=tags,
+            dependencies=dependencies,
+            responses=responses or {},
         )
 
     def get(
@@ -199,6 +209,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -214,6 +225,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -232,6 +244,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -247,6 +260,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -265,6 +279,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -280,6 +295,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -298,6 +314,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -313,6 +330,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -331,6 +349,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -346,6 +365,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -364,6 +384,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -379,6 +400,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -397,6 +419,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -412,6 +435,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -430,6 +454,7 @@ class FastAPI(Starlette):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -445,6 +470,7 @@ class FastAPI(Starlette):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
index a87f23c69ccee1d28e9112c5a3b5db242fb0addc..0530fd2093cc5aeb8e079b4cd30c345774a09e14 100644 (file)
@@ -52,7 +52,7 @@ sequence_types = (list, set, tuple)
 sequence_shape_to_type = {Shape.LIST: list, Shape.SET: set, Shape.TUPLE: tuple}
 
 
-def get_sub_dependant(
+def get_param_sub_dependant(
     *, param: inspect.Parameter, path: str, security_scopes: List[str] = None
 ) -> Dependant:
     depends: params.Depends = param.default
@@ -60,6 +60,30 @@ def get_sub_dependant(
         dependency = depends.dependency
     else:
         dependency = param.annotation
+    return get_sub_dependant(
+        depends=depends,
+        dependency=dependency,
+        path=path,
+        name=param.name,
+        security_scopes=security_scopes,
+    )
+
+
+def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
+    assert callable(
+        depends.dependency
+    ), "A parameter-less dependency must have a callable dependency"
+    return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path)
+
+
+def get_sub_dependant(
+    *,
+    depends: params.Depends,
+    dependency: Callable,
+    path: str,
+    name: str = None,
+    security_scopes: List[str] = None,
+) -> Dependant:
     security_requirement = None
     security_scopes = security_scopes or []
     if isinstance(depends, params.Security):
@@ -73,7 +97,7 @@ def get_sub_dependant(
             security_scheme=dependency, scopes=use_scopes
         )
     sub_dependant = get_dependant(
-        path=path, call=dependency, name=param.name, security_scopes=security_scopes
+        path=path, call=dependency, name=name, security_scopes=security_scopes
     )
     if security_requirement:
         sub_dependant.security_requirements.append(security_requirement)
@@ -111,7 +135,7 @@ def get_dependant(
     for param_name in signature_params:
         param = signature_params[param_name]
         if isinstance(param.default, params.Depends):
-            sub_dependant = get_sub_dependant(
+            sub_dependant = get_param_sub_dependant(
                 param=param, path=path, security_scopes=security_scopes
             )
             dependant.dependencies.append(sub_dependant)
@@ -277,8 +301,8 @@ async def solve_dependencies(
             solved = await sub_dependant.call(**sub_values)
         else:
             solved = await run_in_threadpool(sub_dependant.call, **sub_values)
-        assert sub_dependant.name is not None, "Subdependants always have a name"
-        values[sub_dependant.name] = solved
+        if sub_dependant.name is not None:
+            values[sub_dependant.name] = solved
     path_values, path_errors = request_params_to_args(
         dependant.path_params, request.path_params
     )
index ac8192bafc2f5539983a8e8f3dfc74c341371997..ef8d9bed212e3702aff887da02d10949e508f78b 100644 (file)
@@ -5,7 +5,12 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union
 
 from fastapi import params
 from fastapi.dependencies.models import Dependant
-from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
+from fastapi.dependencies.utils import (
+    get_body_field,
+    get_dependant,
+    get_parameterless_sub_dependant,
+    solve_dependencies,
+)
 from fastapi.encoders import jsonable_encoder
 from pydantic import BaseConfig, BaseModel, Schema
 from pydantic.error_wrappers import ErrorWrapper, ValidationError
@@ -101,6 +106,7 @@ class APIRoute(routing.Route):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -135,6 +141,7 @@ class APIRoute(routing.Route):
             self.response_field = None
         self.status_code = status_code
         self.tags = tags or []
+        self.dependencies = dependencies or []
         self.summary = summary
         self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
         self.response_description = response_description
@@ -175,6 +182,10 @@ class APIRoute(routing.Route):
             endpoint
         ), f"An endpoint must be a function or method"
         self.dependant = get_dependant(path=path, call=self.endpoint)
+        for depends in self.dependencies[::-1]:
+            self.dependant.dependencies.insert(
+                0, get_parameterless_sub_dependant(depends=depends, path=path)
+            )
         self.body_field = get_body_field(dependant=self.dependant, name=self.name)
         self.app = request_response(
             get_app(
@@ -196,6 +207,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -213,6 +225,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -233,6 +246,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -251,6 +265,7 @@ class APIRouter(routing.Router):
                 response_model=response_model,
                 status_code=status_code,
                 tags=tags or [],
+                dependencies=dependencies or [],
                 summary=summary,
                 description=description,
                 response_description=response_description,
@@ -272,6 +287,7 @@ class APIRouter(routing.Router):
         *,
         prefix: str = "",
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
     ) -> None:
         if prefix:
@@ -290,6 +306,7 @@ class APIRouter(routing.Router):
                     response_model=route.response_model,
                     status_code=route.status_code,
                     tags=(route.tags or []) + (tags or []),
+                    dependencies=(dependencies or []) + (route.dependencies or []),
                     summary=route.summary,
                     description=route.description,
                     response_description=route.response_description,
@@ -321,6 +338,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -336,6 +354,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -355,6 +374,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -370,6 +390,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -389,6 +410,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -404,6 +426,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -423,6 +446,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -438,6 +462,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -457,6 +482,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -472,6 +498,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -491,6 +518,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -506,6 +534,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -525,6 +554,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -540,6 +570,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
@@ -559,6 +590,7 @@ class APIRouter(routing.Router):
         response_model: Type[BaseModel] = None,
         status_code: int = 200,
         tags: List[str] = None,
+        dependencies: List[params.Depends] = None,
         summary: str = None,
         description: str = None,
         response_description: str = "Successful Response",
@@ -574,6 +606,7 @@ class APIRouter(routing.Router):
             response_model=response_model,
             status_code=status_code,
             tags=tags or [],
+            dependencies=dependencies or [],
             summary=summary,
             description=description,
             response_description=response_description,
index d5b81d5433763b2151729b3484c76bd7bc8d7568..8fa41d01257ed454643122c9a968711f6d06b8c4 100644 (file)
@@ -54,6 +54,7 @@ nav:
             - First Steps: 'tutorial/dependencies/first-steps.md'
             - Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
             - Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
+            - Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md'
             - Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
         - Security: 
             - Security Intro: 'tutorial/security/intro.md'
index db094df7d4c7780b374eb7e378da5782372c7a75..d0eff5b65150468eff5fc6c253d95e58969289c1 100644 (file)
@@ -74,10 +74,28 @@ openapi_schema = {
                         "description": "Successful Response",
                         "content": {"application/json": {"schema": {}}},
                     },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
                 },
                 "tags": ["items"],
                 "summary": "Read Items",
                 "operationId": "read_items_items__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "X-Token", "type": "string"},
+                        "name": "x-token",
+                        "in": "header",
+                    }
+                ],
             }
         },
         "/items/{item_id}": {
@@ -108,7 +126,13 @@ openapi_schema = {
                         "schema": {"title": "Item_Id", "type": "string"},
                         "name": "item_id",
                         "in": "path",
-                    }
+                    },
+                    {
+                        "required": True,
+                        "schema": {"title": "X-Token", "type": "string"},
+                        "name": "x-token",
+                        "in": "header",
+                    },
                 ],
             },
             "put": {
@@ -139,7 +163,13 @@ openapi_schema = {
                         "schema": {"title": "Item_Id", "type": "string"},
                         "name": "item_id",
                         "in": "path",
-                    }
+                    },
+                    {
+                        "required": True,
+                        "schema": {"title": "X-Token", "type": "string"},
+                        "name": "x-token",
+                        "in": "header",
+                    },
                 ],
             },
         },
@@ -177,29 +207,94 @@ openapi_schema = {
 
 
 @pytest.mark.parametrize(
-    "path,expected_status,expected_response",
+    "path,expected_status,expected_response,headers",
     [
-        ("/users", 200, [{"username": "Foo"}, {"username": "Bar"}]),
-        ("/users/foo", 200, {"username": "foo"}),
-        ("/users/me", 200, {"username": "fakecurrentuser"}),
-        ("/items", 200, [{"name": "Item Foo"}, {"name": "item Bar"}]),
-        ("/items/bar", 200, {"name": "Fake Specific Item", "item_id": "bar"}),
-        ("/openapi.json", 200, openapi_schema),
+        ("/users", 200, [{"username": "Foo"}, {"username": "Bar"}], {}),
+        ("/users/foo", 200, {"username": "foo"}, {}),
+        ("/users/me", 200, {"username": "fakecurrentuser"}, {}),
+        (
+            "/items",
+            200,
+            [{"name": "Item Foo"}, {"name": "item Bar"}],
+            {"X-Token": "fake-super-secret-token"},
+        ),
+        (
+            "/items/bar",
+            200,
+            {"name": "Fake Specific Item", "item_id": "bar"},
+            {"X-Token": "fake-super-secret-token"},
+        ),
+        ("/items", 400, {"detail": "X-Token header invalid"}, {"X-Token": "invalid"}),
+        (
+            "/items/bar",
+            400,
+            {"detail": "X-Token header invalid"},
+            {"X-Token": "invalid"},
+        ),
+        (
+            "/items",
+            422,
+            {
+                "detail": [
+                    {
+                        "loc": ["header", "x-token"],
+                        "msg": "field required",
+                        "type": "value_error.missing",
+                    }
+                ]
+            },
+            {},
+        ),
+        (
+            "/items/bar",
+            422,
+            {
+                "detail": [
+                    {
+                        "loc": ["header", "x-token"],
+                        "msg": "field required",
+                        "type": "value_error.missing",
+                    }
+                ]
+            },
+            {},
+        ),
+        ("/openapi.json", 200, openapi_schema, {}),
     ],
 )
-def test_get_path(path, expected_status, expected_response):
-    response = client.get(path)
+def test_get_path(path, expected_status, expected_response, headers):
+    response = client.get(path, headers=headers)
     assert response.status_code == expected_status
     assert response.json() == expected_response
 
 
-def test_put():
+def test_put_no_header():
     response = client.put("/items/foo")
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["header", "x-token"],
+                "msg": "field required",
+                "type": "value_error.missing",
+            }
+        ]
+    }
+
+
+def test_put_invalid_header():
+    response = client.put("/items/foo", headers={"X-Token": "invalid"})
+    assert response.status_code == 400
+    assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_put():
+    response = client.put("/items/foo", headers={"X-Token": "fake-super-secret-token"})
     assert response.status_code == 200
     assert response.json() == {"item_id": "foo", "name": "The Fighters"}
 
 
 def test_put_forbidden():
-    response = client.put("/items/bar")
+    response = client.put("/items/bar", headers={"X-Token": "fake-super-secret-token"})
     assert response.status_code == 403
     assert response.json() == {"detail": "You can only update the item: foo"}
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py
new file mode 100644 (file)
index 0000000..fdc8789
--- /dev/null
@@ -0,0 +1,128 @@
+from starlette.testclient import TestClient
+
+from dependencies.tutorial006 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/items/": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+                "summary": "Read Items",
+                "operationId": "read_items_items__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "X-Token", "type": "string"},
+                        "name": "x-token",
+                        "in": "header",
+                    },
+                    {
+                        "required": True,
+                        "schema": {"title": "X-Key", "type": "string"},
+                        "name": "x-key",
+                        "in": "header",
+                    },
+                ],
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+            "HTTPValidationError": {
+                "title": "HTTPValidationError",
+                "type": "object",
+                "properties": {
+                    "detail": {
+                        "title": "Detail",
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/ValidationError"},
+                    }
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == openapi_schema
+
+
+def test_get_no_headers():
+    response = client.get("/items/")
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["header", "x-token"],
+                "msg": "field required",
+                "type": "value_error.missing",
+            },
+            {
+                "loc": ["header", "x-key"],
+                "msg": "field required",
+                "type": "value_error.missing",
+            },
+        ]
+    }
+
+
+def test_get_invalid_one_header():
+    response = client.get("/items/", headers={"X-Token": "invalid"})
+    assert response.status_code == 400
+    assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_get_invalid_second_header():
+    response = client.get(
+        "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"}
+    )
+    assert response.status_code == 400
+    assert response.json() == {"detail": "X-Key header invalid"}
+
+
+def test_get_valid_headers():
+    response = client.get(
+        "/items/",
+        headers={
+            "X-Token": "fake-super-secret-token",
+            "X-Key": "fake-super-secret-key",
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == [{"item": "Foo"}, {"item": "Bar"}]
index e6b7ba479c247daa4313c0e4f8730f4c5b3baecc..15ea952ba93ead6f87656c3f071159e286db2f25 100644 (file)
@@ -166,8 +166,6 @@ def test_post_form_no_body():
 
 def test_post_body_json():
     response = client.post("/files/", json={"file": "Foo"})
-    print(response)
-    print(response.content)
     assert response.status_code == 422
     assert response.json() == file_required
 
index 96f9fa9c1f3070fe39e433cf00f411dfeecdc677..403130e49143d9ee47f019d08abb3d6cb269937e 100644 (file)
@@ -227,7 +227,6 @@ def test_token():
     response = client.get(
         "/users/me", headers={"Authorization": f"Bearer {access_token}"}
     )
-    print(response.json())
     assert response.status_code == 200
     assert response.json() == {
         "username": "johndoe",
@@ -319,7 +318,6 @@ def test_token_inactive_user():
     response = client.get(
         "/users/me", headers={"Authorization": f"Bearer {access_token}"}
     )
-    print(response.json())
     assert response.status_code == 400
     assert response.json() == {"detail": "Inactive user"}