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

Introduce lifespan state #1818

Merged
merged 23 commits into from
Mar 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
2 changes: 2 additions & 0 deletions tests/test_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ async def asgi3app(scope, receive, send):
assert scope == {
"type": "lifespan",
"asgi": {"version": "3.0", "spec_version": "2.0"},
"state": {},
}

async def test():
Expand All @@ -188,6 +189,7 @@ def asgi2app(scope):
assert scope == {
"type": "lifespan",
"asgi": {"version": "2.0", "spec_version": "2.0"},
"state": {},
}

async def asgi(receive, send):
Expand Down
6 changes: 5 additions & 1 deletion uvicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

if TYPE_CHECKING:
from asgiref.typing import ASGIApplication
from uvicorn.lifespan.off import LifespanOff
from uvicorn.lifespan.on import LifespanOn

HTTPProtocolType = Literal["auto", "h11", "httptools"]
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
Expand Down Expand Up @@ -471,7 +473,9 @@ def load(self) -> None:
else:
self.ws_protocol_class = self.ws

self.lifespan_class = import_from_string(LIFESPAN[self.lifespan])
self.lifespan_class: Type[Union[LifespanOn, LifespanOff]] = import_from_string(
LIFESPAN[self.lifespan]
)
adriangb marked this conversation as resolved.
Show resolved Hide resolved

try:
self.loaded_app = import_from_string(self.app)
Expand Down
5 changes: 5 additions & 0 deletions uvicorn/lifespan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Union
from uvicorn.lifespan.off import LifespanOff
from uvicorn.lifespan.on import LifespanOn

Lifespan = Union[LifespanOff, LifespanOn]
2 changes: 2 additions & 0 deletions uvicorn/lifespan/off.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any, Dict
from uvicorn import Config


class LifespanOff:
def __init__(self, config: Config) -> None:
self.should_exit = False
self.state: Dict[str, Any] = {}

async def startup(self) -> None:
pass
Expand Down
4 changes: 3 additions & 1 deletion uvicorn/lifespan/on.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import logging
from asyncio import Queue
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Any, Dict, Union

from uvicorn import Config

Expand Down Expand Up @@ -42,6 +42,7 @@ def __init__(self, config: Config) -> None:
self.startup_failed = False
self.shutdown_failed = False
self.should_exit = False
self.state: Dict[str, Any] = {}

async def startup(self) -> None:
self.logger.info("Waiting for application startup.")
Expand Down Expand Up @@ -82,6 +83,7 @@ async def main(self) -> None:
scope: LifespanScope = {
"type": "lifespan",
"asgi": {"version": self.config.asgi_version, "spec_version": "2.0"},
"state": self.state,
}
await app(scope, self.receive, self.send)
except BaseException as exc:
Expand Down
4 changes: 4 additions & 0 deletions uvicorn/protocols/http/h11_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
HTTPResponseStartEvent,
HTTPScope,
)
from uvicorn.lifespan import Lifespan

H11Event = Union[
h11.Request,
Expand Down Expand Up @@ -68,6 +69,7 @@ def __init__(
self,
config: Config,
server_state: ServerState,
lifespan: "Lifespan",
_loop: Optional[asyncio.AbstractEventLoop] = None,
) -> None:
if not config.loaded:
Expand All @@ -83,6 +85,7 @@ def __init__(
self.ws_protocol_class = config.ws_protocol_class
self.root_path = config.root_path
self.limit_concurrency = config.limit_concurrency
self.lifespan = lifespan

# Timeouts
self.timeout_keep_alive_task: Optional[asyncio.TimerHandle] = None
Expand Down Expand Up @@ -223,6 +226,7 @@ def handle_events(self) -> None:
"raw_path": raw_path,
"query_string": query_string,
"headers": self.headers,
"state": self.lifespan.state.copy(),
}

upgrade = self._get_upgrade()
Expand Down
4 changes: 4 additions & 0 deletions uvicorn/protocols/http/httptools_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
HTTPResponseStartEvent,
HTTPScope,
)
from uvicorn.lifespan import Lifespan

HEADER_RE = re.compile(b'[\x00-\x1F\x7F()<>@,;:[]={} \t\\"]')
HEADER_VALUE_RE = re.compile(b"[\x00-\x1F\x7F]")
Expand All @@ -66,6 +67,7 @@ def __init__(
self,
config: Config,
server_state: ServerState,
lifespan: "Lifespan",
_loop: Optional[asyncio.AbstractEventLoop] = None,
) -> None:
if not config.loaded:
Expand All @@ -81,6 +83,7 @@ def __init__(
self.ws_protocol_class = config.ws_protocol_class
self.root_path = config.root_path
self.limit_concurrency = config.limit_concurrency
self.lifespan = lifespan

# Timeouts
self.timeout_keep_alive_task: Optional[TimerHandle] = None
Expand Down Expand Up @@ -237,6 +240,7 @@ def on_message_begin(self) -> None:
"scheme": self.scheme,
"root_path": self.root_path,
"headers": self.headers,
"state": self.lifespan.state.copy(),
}

# Parser callbacks
Expand Down
4 changes: 2 additions & 2 deletions uvicorn/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import time
from email.utils import formatdate
from types import FrameType
from typing import TYPE_CHECKING, List, Optional, Sequence, Set, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, Tuple, Union

import click

Expand Down Expand Up @@ -93,7 +93,7 @@ async def startup(self, sockets: Optional[List[socket.socket]] = None) -> None:
config = self.config

create_protocol = functools.partial(
config.http_protocol_class, config=config, server_state=self.server_state
config.http_protocol_class, config=config, server_state=self.server_state, lifespan=self.lifespan
)
loop = asyncio.get_running_loop()

Expand Down