From cf04a42f3b0f76f61fbcc4274b97e1b2618cccd9 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 19 Nov 2023 11:32:25 +0100 Subject: [PATCH] Improve the routing and testing documentation --- docs/remotes.md | 2 +- docs/routing.md | 189 ++++++++++++++++++++++++++----- docs/testing.md | 16 +-- docs/versions/migrating-to-v2.md | 2 + 4 files changed, 170 insertions(+), 39 deletions(-) diff --git a/docs/remotes.md b/docs/remotes.md index 3552d8d..874015c 100644 --- a/docs/remotes.md +++ b/docs/remotes.md @@ -16,7 +16,7 @@ For example: original client IP address must also be forwarded in a header. * the *path* of web requests can be changed while being proxied (e.g. NGINX configured to proxy requests to `/example` to the root `/` of a web - application) + application). This information may be important in request processing, for example in redirects, authentication, link generation when absolute URLs are needed, and diff --git a/docs/routing.md b/docs/routing.md index b7fbd47..1397f94 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -14,6 +14,7 @@ This page describes: - [X] How to define a catch-all route. - [X] How to define a fallback route. - [X] How to use sub-routers and filters. +- [X] How to use the default router and other routers. ## Defining request handlers @@ -51,32 +52,12 @@ The following example shows how to define a request handler for the root path of a web application "/": ```python -from blacksheep import Application, get - -app = Application(show_error_details=True) - - -@get("/") -def hello_world(): - return "Hello World" -``` - -It is possible to assign router methods to variables, to reduce code verbosity: - -```python -from blacksheep import Application, get, post - -app = Application(show_error_details=True) +from blacksheep import get @get("/") def hello_world(): return "Hello World" - - -@post("/message") -def create_message(text: str): - return "TODO" ``` Alternatively, the application router offers a `route` method: @@ -177,8 +158,7 @@ def get_cat(cat_id): ... ``` -It is also possible to specify the expected type, using standard `typing` -annotations: +It is also possible to specify the expected type, using `typing` annotations: ```python @@ -247,11 +227,11 @@ The following value patterns are built-in: | Value pattern | Description | | ------------- | --------------------------------------------------------------------------------- | -| str | Any value that doesn't contain a slash "/". | -| int | Any value that contains only numeric characters. | -| float | Any value that contains only numeric characters and eventually a dot with digits. | -| path | Any value to the end of the path. | -| uuid | Any value that matches the UUID value pattern. | +| `str` | Any value that doesn't contain a slash "/". | +| `int` | Any value that contains only numeric characters. | +| `float` | Any value that contains only numeric characters and eventually a dot with digits. | +| `path` | Any value to the end of the path. | +| `uuid` | Any value that matches the UUID value pattern. | To define custom value patterns, extend the `Route.value_patterns` dictionary. The key of the dictionary is the name used by the parameter, while the value is @@ -371,3 +351,156 @@ class CustomFilter(RouteFilter): example_router = Router(filters=[CustomFilter()]) ``` + +## Using the default router and other routers + +The examples in the documentation show how to register routes using methods +imported from the BlackSheep package: + +```python +from blacksheep import get + +@get("") +async def home(): + ... +``` + +Or, for controllers: + +```python +from blacksheep.server.controllers import Controller, get + + +class Home(Controller): + + @get("/") + async def index(self): + ... +``` + +In this case routes are registered using default singleton routers, used if an +application is instantiated without specifying a router: + +```python +from blacksheep import Application + + +# This application uses the default sigleton routers exposed by BlackSheep: +app = Application() +``` + +This works in most scenarios, when a single `Application` instance per process +is used. For more complex scenarios, it is possible to instantiate a router +and use it as desired: + +```python +# app/router.py + +from blacksheep import Router + + +router = Router() +``` + +And use it when registering routes: + +```python +from app.router import router + + +@router.get("/") +async def home(): + ... +``` + +It is also possible to expose the router methods to reduce code verbosity, like +the BlackSheep package does: + +```python +# app/router.py + +from blacksheep import Router + + +router = Router() + + +get = router.get +post = router.post + +# ... +``` + + +```python +from app.router import get + + +@get("/") +async def home(): + ... +``` + +Then specify the router when instantiating the application: + +```python +from blacksheep import Application + +from app.router import router + + +# This application uses the router instantiated in app.router: +app = Application(router=router) +``` + +### Controllers dedicated router + +Controllers need a different kind of router, an instance of +`blacksheep.server.routing.RoutesRegistry`. If using dedicated router for +controllers is desired, do instead: + +```python +# app/controllers.py + +from blacksheep import RoutesRegistry + + +controllers_router = RoutesRegistry() + + +get = controllers_router.get +post = controllers_router.post + +# ... +``` + +Then when defining your controllers: + +```python +from blacksheep.server.controllers import Controller + +from app.controllers import get, post + + +class Home(Controller): + + @get("/") + async def index(self): + ... +``` + +```python +from blacksheep import Application + +from app.controllers import controllers_router + + +# This application uses the controllers' router instantiated in app.controllers: +app = Application() +app.controllers_router = controllers_router +``` + +!!! info "About Router and RoutesRegistry" + Controllers routes use a "RoutesRegistry" to support dynamic generation of + paths by controller class name. Controllers routes are evaluated and merged + into `Application.router` when the application starts. diff --git a/docs/testing.md b/docs/testing.md index 0c0bf6b..8ebe5a7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -160,16 +160,15 @@ definition of the TODOs API. Start with the following contents: from blacksheep import get, post, delete from domain import ToDo, CreateToDoInput -from typing import List, Optional @get("/api/todos") -async def get_todos() -> List[ToDo]: +async def get_todos() -> list[ToDo]: ... @get("/api/todos/{todo_id}") -async def get_todo(todo_id) -> Optional[ToDo]: +async def get_todo(todo_id) -> ToDo | None: ... @@ -255,13 +254,11 @@ API to work with data stored in memory: ```python # ./app/routes/todos.py -from typing import Dict, List, Optional - from blacksheep import get, delete, not_found, post from domain import CreateToDoInput, ToDo -_MOCKED: Dict[int, ToDo] = { +_MOCKED: dict[int, ToDo] = { 1: ToDo( id=1, title="BlackSheep Documentation", @@ -281,16 +278,16 @@ _MOCKED: Dict[int, ToDo] = { @get("/api/todos") -async def get_todos() -> List[ToDo]: +async def get_todos() -> list[ToDo]: return list(_MOCKED.values()) @get("/api/todos/{todo_id}") -async def get_todo(todo_id: int) -> Optional[ToDo]: +async def get_todo(todo_id: int) -> ToDo | None: try: return _MOCKED[todo_id] except KeyError: - return not_found() + return not_found() # type: ignore @post("/api/todos") @@ -306,7 +303,6 @@ async def delete_todo(todo_id: int) -> None: del _MOCKED[todo_id] except KeyError: pass - ``` Now that the API is mocked, let's see how to add tests for it. diff --git a/docs/versions/migrating-to-v2.md b/docs/versions/migrating-to-v2.md index 54ae2b7..e006aa2 100644 --- a/docs/versions/migrating-to-v2.md +++ b/docs/versions/migrating-to-v2.md @@ -51,6 +51,8 @@ async def add_example(self, example: str): ... ``` +For more information on the above, read [_Using the default router and other routers_](/blacksheep/routing/#using-the-default-router-and-other-routers). + All modules inside `routes` and `controllers` packages are imported automatically in v2. Automatic import works relatively to where a BlackSheep application is instantiated. In the structure described below, the modules in