Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extensions["lifespan.state"] #354

Merged
merged 3 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion asgiref/typing.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the NotRequired should be included only here. This was previously mentioned on the HTTP trailers PR, and on the PR that introduced this module (I'm on my phone, difficult to provide links).

IMO either we change what we consider to be optional on those type hints, or we don't add the NotRequired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that making it Optional implies that the server must set it to None, so every single server is immediately out of spec. Using NotRequired means that no changes are necessary to servers.


__all__ = (
"ASGIVersions",
"HTTPScope",
Expand Down Expand Up @@ -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]]]


Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
35 changes: 35 additions & 0 deletions specs/lifespan.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``,
Expand All @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is unclear... In the next section it's specified to store in lifespan["state"], but here it mentions that applications can use extension["lifespan.state"]... 🤔

Was this supposed to be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think so

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
'''''''''''''''''''''''''''
Expand Down
4 changes: 4 additions & 0 deletions specs/www.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment on lines +124 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing in the WebSocket section below.

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;
Expand Down