Skip to content

Commit fa0f0f0

Browse files
committed
Add Starlette lifespan handler implementation
1 parent 2c998b8 commit fa0f0f0

File tree

5 files changed

+195
-0
lines changed

5 files changed

+195
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Integration With Starlette-based Frameworks
2+
===========================================
3+
4+
This is a `Starlette <https://www.starlette.io/>`_ +
5+
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application
6+
utilizing `lifespan API <https://www.starlette.io/lifespan/>`_.
7+
8+
.. note::
9+
10+
Pretty much `any framework built on top of Starlette <https://www.starlette.io/third-party-packages/#frameworks>`_
11+
supports this feature (`FastAPI <https://fastapi.tiangolo.com/advanced/events/#lifespan>`_,
12+
`Xpresso <https://xpresso-api.dev/latest/tutorial/lifespan/>`_, etc...).
13+
14+
Run
15+
---
16+
17+
Create virtual environment:
18+
19+
.. code-block:: bash
20+
21+
python -m venv env
22+
. env/bin/activate
23+
24+
Install requirements:
25+
26+
.. code-block:: bash
27+
28+
pip install -r requirements.txt
29+
30+
To run the application do:
31+
32+
.. code-block:: bash
33+
34+
python example.py
35+
# or (logging won't be configured):
36+
uvicorn --factory example:container.app
37+
38+
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``,
39+
etc).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python
2+
3+
from logging import basicConfig, getLogger
4+
5+
from dependency_injector.containers import DeclarativeContainer
6+
from dependency_injector.ext.starlette import Lifespan
7+
from dependency_injector.providers import Factory, Resource, Self, Singleton
8+
from starlette.applications import Starlette
9+
from starlette.requests import Request
10+
from starlette.responses import JSONResponse
11+
from starlette.routing import Route
12+
13+
count = 0
14+
15+
16+
def init():
17+
log = getLogger(__name__)
18+
log.info("Inittializing resources")
19+
yield
20+
log.info("Cleaning up resources")
21+
22+
23+
async def homepage(request: Request) -> JSONResponse:
24+
global count
25+
response = JSONResponse({"hello": "world", "count": count})
26+
count += 1
27+
return response
28+
29+
30+
class Container(DeclarativeContainer):
31+
__self__ = Self()
32+
lifespan = Singleton(Lifespan, __self__)
33+
logging = Resource(
34+
basicConfig,
35+
level="DEBUG",
36+
datefmt="%Y-%m-%d %H:%M",
37+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
38+
)
39+
init = Resource(init)
40+
app = Factory(
41+
Starlette,
42+
debug=True,
43+
lifespan=lifespan,
44+
routes=[Route("/", homepage)],
45+
)
46+
47+
48+
container = Container()
49+
50+
if __name__ == "__main__":
51+
import uvicorn
52+
53+
uvicorn.run(
54+
container.app,
55+
factory=True,
56+
# NOTE: `None` prevents uvicorn from configuring logging, which is
57+
# impossible via CLI
58+
log_config=None,
59+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dependency-injector
2+
starlette
3+
uvicorn
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import sys
2+
from abc import ABCMeta, abstractmethod
3+
from typing import Any, Callable, Coroutine, Optional
4+
5+
if sys.version_info >= (3, 11): # pragma: no cover
6+
from typing import Self
7+
else: # pragma: no cover
8+
from typing_extensions import Self
9+
10+
from dependency_injector.containers import Container
11+
12+
13+
class Lifespan:
14+
"""A starlette lifespan handler performing container resource initialization and shutdown.
15+
16+
See https://www.starlette.io/lifespan/ for details.
17+
18+
Usage:
19+
20+
.. code-block:: python
21+
22+
from dependency_injector.containers import DeclarativeContainer
23+
from dependency_injector.ext.starlette import Lifespan
24+
from dependency_injector.providers import Factory, Self, Singleton
25+
from starlette.applications import Starlette
26+
27+
class Container(DeclarativeContainer):
28+
__self__ = Self()
29+
lifespan = Singleton(Lifespan, __self__)
30+
app = Factory(Starlette, lifespan=lifespan)
31+
32+
:param container: container instance
33+
"""
34+
35+
container: Container
36+
37+
def __init__(self, container: Container) -> None:
38+
self.container = container
39+
40+
def __call__(self, app: Any) -> Self:
41+
return self
42+
43+
async def __aenter__(self) -> None:
44+
result = self.container.init_resources()
45+
46+
if result is not None:
47+
await result
48+
49+
async def __aexit__(self, *exc_info: Any) -> None:
50+
result = self.container.shutdown_resources()
51+
52+
if result is not None:
53+
await result

tests/unit/ext/test_starlette.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import AsyncIterator, Iterator
2+
from unittest.mock import ANY
3+
4+
from pytest import mark
5+
6+
from dependency_injector.containers import DeclarativeContainer
7+
from dependency_injector.ext.starlette import Lifespan
8+
from dependency_injector.providers import Resource
9+
10+
11+
class TestLifespan:
12+
@mark.parametrize("sync", [False, True])
13+
@mark.asyncio
14+
async def test_context_manager(self, sync: bool) -> None:
15+
init, shutdown = False, False
16+
17+
def sync_resource() -> Iterator[None]:
18+
nonlocal init, shutdown
19+
20+
init = True
21+
yield
22+
shutdown = True
23+
24+
async def async_resource() -> AsyncIterator[None]:
25+
nonlocal init, shutdown
26+
27+
init = True
28+
yield
29+
shutdown = True
30+
31+
class Container(DeclarativeContainer):
32+
x = Resource(sync_resource if sync else async_resource)
33+
34+
container = Container()
35+
lifespan = Lifespan(container)
36+
37+
async with lifespan(ANY) as scope:
38+
assert scope is None
39+
assert init
40+
41+
assert shutdown

0 commit comments

Comments
 (0)