From cfdcecdbfaca438c2ad42b7403e4318664f9f992 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 27 Dec 2022 10:03:17 -0800 Subject: [PATCH] Introduce lifespan state --- asgiref/typing.py | 10 +++++++++- setup.cfg | 2 +- specs/lifespan.rst | 35 +++++++++++++++++++++++++++++++++++ specs/www.rst | 4 ++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/asgiref/typing.py b/asgiref/typing.py index d126b006..f3e31f28 100644 --- a/asgiref/typing.py +++ b/asgiref/typing.py @@ -1,11 +1,16 @@ import sys -from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union +from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union if sys.version_info >= (3, 8): from typing import Literal, Protocol, TypedDict else: from typing_extensions import Literal, Protocol, TypedDict +if sys.version_info >= (3, 11): + from typing import NotRequired +else: + from typing_extensions import NotRequired + __all__ = ( "ASGIVersions", "HTTPScope", @@ -62,6 +67,7 @@ class HTTPScope(TypedDict): headers: Iterable[Tuple[bytes, bytes]] client: Optional[Tuple[str, int]] server: Optional[Tuple[str, Optional[int]]] + state: NotRequired[Dict[str, Any]] extensions: Optional[Dict[str, Dict[object, object]]] @@ -78,12 +84,14 @@ class WebSocketScope(TypedDict): client: Optional[Tuple[str, int]] server: Optional[Tuple[str, Optional[int]]] subprotocols: Iterable[str] + state: NotRequired[Dict[str, Any]] extensions: Optional[Dict[str, Dict[object, object]]] class LifespanScope(TypedDict): type: Literal["lifespan"] asgi: ASGIVersions + state: Optional[Dict[str, Any]] WWWScope = Union[HTTPScope, WebSocketScope] diff --git a/setup.cfg b/setup.cfg index 25ae034a..a9e681f8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ python_requires = >=3.7 packages = find: include_package_data = true install_requires = - typing_extensions; python_version < "3.8" + typing_extensions; python_version < "3.11" zip_safe = false [options.extras_require] diff --git a/specs/lifespan.rst b/specs/lifespan.rst index 6385b825..26ed46d3 100644 --- a/specs/lifespan.rst +++ b/specs/lifespan.rst @@ -46,6 +46,9 @@ The scope information passed in ``scope`` contains basic metadata: * ``asgi["version"]`` (*Unicode string*) -- The version of the ASGI spec. * ``asgi["spec_version"]`` (*Unicode string*) -- The version of this spec being used. Optional; if missing defaults to ``"1.0"``. +* ``state`` Optional(*dict[Unicode string, Any]*) -- An empty namespace where + the application can persist state to be used when handling subsequent requests. + Optional; if missing the server does not support this feature. If an exception is raised when calling the application callable with a ``lifespan.startup`` message or a ``scope`` with type ``lifespan``, @@ -56,6 +59,38 @@ lifespan protocol. If you want to log an error that occurs during lifespan startup and prevent the server from starting, then send back ``lifespan.startup.failed`` instead. +The ``extensions["lifespan.state"]`` dict is an empty namespace. +Applications can store arbitrary data in this namespace. +A *shallow copy* of this dictionary will get passed into each request handler. +This key will only be set if the server supports this extension. + + +Lifespan State +-------------- + +Applications often want to persist data from the lifespan cycle to request/response handling. +For example, a database connection can be established in the lifespan cycle and persisted to +the request/response cycle. +The ```lifespan["state"]`` namespace provides a place to store these sorts of things. +The server will ensure that a *shallow copy* of the namespace is passed into each subsequent +request/response call into the application. +Since the server manages the application lifespan and often the event loop as well this +ensures that the application is always accessing the database connection (or other stored object) +that corresponds to the right event loop and lifecycle, without using context variables, +global mutable state or having to worry about references to stale/closed connections. + +ASGI servers that implement this feature will provide +``state`` as part of the ``lifespan`` scope:: + + "scope": { + ... + "state": {}, + } + +The namespace is controlled completely by the ASGI application, the server will not +interact with it other than to copy it. +Nonetheless applications should be cooperative by properly naming their keys such that they +will not collide with other frameworks or middleware. Startup - ``receive`` event ''''''''''''''''''''''''''' diff --git a/specs/www.rst b/specs/www.rst index f8f7f923..69104a73 100644 --- a/specs/www.rst +++ b/specs/www.rst @@ -121,6 +121,10 @@ The *connection scope* information passed in ``scope`` contains: listening port, or ``[path, None]`` where ``path`` is that of the unix socket. Optional; if missing defaults to ``None``. +* ``state`` Optional(*dict[Unicode string, Any]*) -- A copy of the + namespace passed into the lifespan corresponding to this request. (See :doc:`lifespan`). + Optional; if missing the server does not support this feature. + Servers are responsible for handling inbound and outbound chunked transfer encodings. A request with a ``chunked`` encoded body should be automatically de-chunked by the server and presented to the application as plain body bytes;