Repository: encode/starlette

Issue:
Support `shutdown` as a synonym for `cleanup`
* Support either `cleanup` or `shutdown` as the ASGI lifespan message name.
* Update uvicorn to move to shutdown - https://github.com/encode/uvicorn/issues/233
* Finally, after a small period of time, drop `cleanup`

Easy PR for a contributor to jump on would be addressing the first part of this, and supporting either name.


Discussion:

Resolving patch:
diff --git a/starlette/lifespan.py b/starlette/lifespan.py
--- a/starlette/lifespan.py
+++ b/starlette/lifespan.py
@@ -22,7 +22,7 @@ def decorator(func: typing.Callable) -> typing.Callable:
         return decorator
 
     def add_event_handler(self, event_type: str, func: typing.Callable) -> None:
-        assert event_type in ("startup", "cleanup")
+        assert event_type in ("startup", "shutdown", "cleanup")
 
         if event_type == "startup":
             self.startup_handlers.append(func)
@@ -53,19 +53,26 @@ async def run_lifespan(self, receive: Receive, send: Send) -> None:
         await self.run_startup()
         await send({"type": "lifespan.startup.complete"})
         message = await receive()
-        assert message["type"] == "lifespan.cleanup"
+        assert (
+            message["type"] == "lifespan.shutdown"
+            or message["type"] == "lifespan.cleanup"
+        )
         await self.run_cleanup()
-        await send({"type": "lifespan.cleanup.complete"})
+        if message["type"] == "lifespan.shutdown":
+            await send({"type": "lifespan.shutdown.complete"})
+
+        if message["type"] == "lifespan.cleanup":
+            await send({"type": "lifespan.cleanup.complete"})  # pragma: no cover
 
 
 class LifespanContext:
     def __init__(
-        self, app: ASGIApp, startup_timeout: int = 10, cleanup_timeout: int = 10
+        self, app: ASGIApp, startup_timeout: int = 10, shutdown_timeout: int = 10
     ) -> None:
         self.startup_timeout = startup_timeout
-        self.cleanup_timeout = cleanup_timeout
+        self.shutdown_timeout = shutdown_timeout
         self.startup_event = asyncio.Event()
-        self.cleanup_event = asyncio.Event()
+        self.shutdown_event = asyncio.Event()
         self.receive_queue = asyncio.Queue()  # type: asyncio.Queue
         self.asgi = app({"type": "lifespan"})  # type: ASGIInstance
 
@@ -81,25 +88,25 @@ def __exit__(
         tb: TracebackType,
     ) -> None:
         loop = asyncio.get_event_loop()
-        loop.run_until_complete(self.wait_cleanup())
+        loop.run_until_complete(self.wait_shutdown())
 
     async def run_lifespan(self) -> None:
         try:
             await self.asgi(self.receive, self.send)
         finally:
             self.startup_event.set()
-            self.cleanup_event.set()
+            self.shutdown_event.set()
 
     async def send(self, message: Message) -> None:
         if message["type"] == "lifespan.startup.complete":
             assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
-            assert not self.cleanup_event.is_set(), STATE_TRANSITION_ERROR
+            assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
             self.startup_event.set()
         else:
-            assert message["type"] == "lifespan.cleanup.complete"
+            assert message["type"] == "lifespan.shutdown.complete"
             assert self.startup_event.is_set(), STATE_TRANSITION_ERROR
-            assert not self.cleanup_event.is_set(), STATE_TRANSITION_ERROR
-            self.cleanup_event.set()
+            assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
+            self.shutdown_event.set()
 
     async def receive(self) -> Message:
         return await self.receive_queue.get()
@@ -108,6 +115,8 @@ async def wait_startup(self) -> None:
         await self.receive_queue.put({"type": "lifespan.startup"})
         await asyncio.wait_for(self.startup_event.wait(), timeout=self.startup_timeout)
 
-    async def wait_cleanup(self) -> None:
-        await self.receive_queue.put({"type": "lifespan.cleanup"})
-        await asyncio.wait_for(self.cleanup_event.wait(), timeout=self.cleanup_timeout)
+    async def wait_shutdown(self) -> None:
+        await self.receive_queue.put({"type": "lifespan.shutdown"})
+        await asyncio.wait_for(
+            self.shutdown_event.wait(), timeout=self.shutdown_timeout
+        )


Test patch:
diff --git a/tests/test_applications.py b/tests/test_applications.py
--- a/tests/test_applications.py
+++ b/tests/test_applications.py
@@ -181,7 +181,7 @@ def run_cleanup():
         cleanup_complete = True
 
     app.add_event_handler("startup", run_startup)
-    app.add_event_handler("cleanup", run_cleanup)
+    app.add_event_handler("shutdown", run_cleanup)
 
     assert not startup_complete
     assert not cleanup_complete
diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py
--- a/tests/test_lifespan.py
+++ b/tests/test_lifespan.py
@@ -12,7 +12,7 @@ def run_startup():
         nonlocal startup_complete
         startup_complete = True
 
-    @handler.on_event("cleanup")
+    @handler.on_event("shutdown")
     def run_cleanup():
         nonlocal cleanup_complete
         cleanup_complete = True
@@ -36,7 +36,7 @@ async def run_startup():
         nonlocal startup_complete
         startup_complete = True
 
-    @handler.on_event("cleanup")
+    @handler.on_event("shutdown")
     async def run_cleanup():
         nonlocal cleanup_complete
         cleanup_complete = True
@@ -60,7 +60,7 @@ def run_startup():
         nonlocal startup_complete
         startup_complete = True
 
-    @app.on_event("cleanup")
+    @app.on_event("shutdown")
     def run_cleanup():
         nonlocal cleanup_complete
         cleanup_complete = True