Skip to content

Commit

Permalink
Deprecate on_startup and on_shutdown events (#2070)
Browse files Browse the repository at this point in the history
* Revert "Support lifespan state (#2060)"

This reverts commit da6461b.

* new implementation

* Deprecate `on_startup` and `on_shutdown` events

* Rename `events.md` by `lifespan.md`

---------

Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
  • Loading branch information
Kludex and adriangb authored Mar 9, 2023
1 parent 92ab71e commit cc20c86
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 174 deletions.
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def client():
Make a 'client' fixture available to test cases.
"""
# Our fixture is created within a context manager. This ensures that
# application startup and shutdown run for every test case.
# application lifespan runs for every test case.
with TestClient(app) as test_client:
yield test_client
```
Expand Down
10 changes: 8 additions & 2 deletions docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ DATABASE_URL=sqlite:///test.db
**app.py**

```python
import contextlib

import databases
import sqlalchemy
from starlette.applications import Starlette
Expand All @@ -44,6 +46,11 @@ notes = sqlalchemy.Table(

database = databases.Database(DATABASE_URL)

@contextlib.asynccontextmanager
async def lifespan(app):
await database.connect()
yield
await database.disconnect()

# Main application code.
async def list_notes(request):
Expand Down Expand Up @@ -77,8 +84,7 @@ routes = [

app = Starlette(
routes=routes,
on_startup=[database.connect],
on_shutdown=[database.disconnect]
lifespan=lifespan,
)
```

Expand Down
147 changes: 0 additions & 147 deletions docs/events.md

This file was deleted.

91 changes: 91 additions & 0 deletions docs/lifespan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@

Starlette applications can register a lifespan handler for dealing with
code that needs to run before the application starts up, or when the application
is shutting down.

```python
import contextlib

from starlette.applications import Starlette


@contextlib.asynccontextmanager
async def lifespan(app):
async with some_async_resource():
print("Run at startup!")
yield
print("Run on shutdown!")


routes = [
...
]

app = Starlette(routes=routes, lifespan=lifespan)
```

Starlette will not start serving any incoming requests until the lifespan has been run.

The lifespan teardown will run once all connections have been closed, and
any in-process background tasks have completed.

Consider using [`anyio.create_task_group()`](https://anyio.readthedocs.io/en/stable/tasks.html)
for managing asynchronous tasks.

## Lifespan State

The lifespan has the concept of `state`, which is a dictionary that
can be used to share the objects between the lifespan, and the requests.

```python
import contextlib
from typing import TypedDict

import httpx
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route


class State(TypedDict):
http_client: httpx.AsyncClient


@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> State:
async with httpx.AsyncClient() as client:
yield {"http_client": client}


async def homepage(request):
client = request.state.http_client
response = await client.get("https://www.example.com")
return PlainTextResponse(response.text)


app = Starlette(
lifespan=lifespan,
routes=[Route("/", homepage)]
)
```

The `state` received on the requests is a **shallow** copy of the state received on the
lifespan handler.

## Running lifespan in tests

You should use `TestClient` as a context manager, to ensure that the lifespan is called.

```python
from example import app
from starlette.testclient import TestClient


def test_homepage():
with TestClient(app) as client:
# Application's lifespan is called on entering the block.
response = client.get("/")
assert response.status_code == 200

# And the lifespan's teardown is run when exiting the block.
```
8 changes: 4 additions & 4 deletions docs/testclient.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ case you should use `client = TestClient(app, raise_server_exceptions=False)`.

!!! note

If you want the `TestClient` to run `lifespan` events (`on_startup`, `on_shutdown`, or `lifespan`),
you will need to use the `TestClient` as a context manager. Otherwise, the events
will not be triggered when the `TestClient` is instantiated. You can learn more about it
[here](/events/#running-event-handlers-in-tests).
If you want the `TestClient` to run the `lifespan` handler,
you will need to use the `TestClient` as a context manager. It will
not be triggered when the `TestClient` is instantiated. You can learn more about it
[here](/lifespan/#running-lifespan-in-tests).

### Selecting the Async backend

Expand Down
3 changes: 3 additions & 0 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class Starlette:
* **on_shutdown** - A list of callables to run on application shutdown.
Shutdown handler callables do not take any arguments, and may be be either
standard functions, or async functions.
* **lifespan** - A lifespan context function, which can be used to perform
startup and shutdown tasks. This is a newer style that replaces the
`on_startup` and `on_shutdown` handlers. Use one or the other, not both.
"""

def __init__(
Expand Down
10 changes: 9 additions & 1 deletion starlette/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,14 @@ def __init__(
self.on_startup = [] if on_startup is None else list(on_startup)
self.on_shutdown = [] if on_shutdown is None else list(on_shutdown)

if on_startup or on_shutdown:
warnings.warn(
"The on_startup and on_shutdown parameters are deprecated, and they "
"will be removed on version 1.0. Use the lifespan parameter instead. "
"See more about it on https://www.starlette.io/lifespan/.",
DeprecationWarning,
)

if lifespan is None:
self.lifespan_context: Lifespan = _DefaultLifespan(self)

Expand Down Expand Up @@ -841,7 +849,7 @@ def add_event_handler(
def on_event(self, event_type: str) -> typing.Callable:
warnings.warn(
"The `on_event` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501
"Refer to https://www.starlette.io/events/#registering-events for recommended approach.", # noqa: E501
"Refer to https://www.starlette.io/lifespan/ for recommended approach.",
DeprecationWarning,
)

Expand Down
11 changes: 7 additions & 4 deletions tests/test_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,13 @@ def run_cleanup():
nonlocal cleanup_complete
cleanup_complete = True

app = Starlette(
on_startup=[run_startup],
on_shutdown=[run_cleanup],
)
with pytest.deprecated_call(
match="The on_startup and on_shutdown parameters are deprecated"
):
app = Starlette(
on_startup=[run_startup],
on_shutdown=[run_cleanup],
)

assert not startup_complete
assert not cleanup_complete
Expand Down
Loading

0 comments on commit cc20c86

Please sign in to comment.