diff --git a/daphne/cli.py b/daphne/cli.py index a5623ad..9dac979 100755 --- a/daphne/cli.py +++ b/daphne/cli.py @@ -160,6 +160,12 @@ class CommandLineInterface: self.parser.add_argument( "--no-server-name", dest="server_name", action="store_const", const="" ) + self.parser.add_argument( + "--enable-lifespan", + dest="enable_lifespan", + action="store_true", + help="Enables lifespan support.", + ) self.server = None @@ -279,6 +285,7 @@ class CommandLineInterface: action_logger=( AccessLogGenerator(access_log_stream) if access_log_stream else None ), + enable_lifespan=args.enable_lifespan, root_path=args.root_path, verbosity=args.verbosity, proxy_forwarded_address_header=self._get_forwarded_host(args=args), diff --git a/daphne/management/commands/runserver.py b/daphne/management/commands/runserver.py index d505f33..1b6b186 100644 --- a/daphne/management/commands/runserver.py +++ b/daphne/management/commands/runserver.py @@ -51,6 +51,13 @@ class Command(RunserverCommand): default=True, help="Run the old WSGI-based runserver rather than the ASGI-based one", ) + parser.add_argument( + "--enable-lifespan", + action="store_true", + dest="enable_lifespan", + default=False, + help="Enable lifespan support.", + ) parser.add_argument( "--http_timeout", action="store", @@ -141,6 +148,7 @@ class Command(RunserverCommand): application=self.get_application(options), endpoints=endpoints, signal_handlers=not options["use_reloader"], + enable_lifespan=options["enable_lifespan"], action_logger=self.log_action, http_timeout=self.http_timeout, root_path=getattr(settings, "FORCE_SCRIPT_NAME", "") or "", diff --git a/daphne/server.py b/daphne/server.py index a6d3819..da77314 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -5,6 +5,7 @@ import sys # isort:skip import warnings # isort:skip from concurrent.futures import ThreadPoolExecutor # isort:skip from twisted.internet import asyncioreactor # isort:skip +from monkay.asgi import Lifespan # isort:skip twisted_loop = asyncio.new_event_loop() @@ -66,6 +67,7 @@ class Server: application_close_timeout=10, ready_callable=None, server_name="daphne", + enable_lifespan=False, ): self.application = application self.endpoints = endpoints or [] @@ -93,6 +95,9 @@ class Server: if not self.endpoints: logger.error("No endpoints. This server will not listen on anything.") sys.exit(1) + self.lifespan_context = None + if enable_lifespan: + self.lifespan_context = Lifespan(self.application) def run(self): # A dict of protocol: {"application_instance":, "connected":, "disconnected":} dicts @@ -120,6 +125,10 @@ class Server: logger.info( "HTTP/2 support not enabled (install the http2 and tls Twisted extras)" ) + # Set the asyncio reactor's event loop as global + # TODO: Should we instead pass the global one into the reactor? + evloop = reactor._asyncioEventloop + asyncio.set_event_loop(evloop) # Kick off the timeout loop reactor.callLater(1, self.application_checker) @@ -133,21 +142,25 @@ class Server: listener.addErrback(self.listen_error) self.listeners.append(listener) - # Set the asyncio reactor's event loop as global - # TODO: Should we instead pass the global one into the reactor? - asyncio.set_event_loop(reactor._asyncioEventloop) - # Verbosity 3 turns on asyncio debug to find those blocking yields if self.verbosity >= 3: - asyncio.get_event_loop().set_debug(True) + evloop.set_debug(True) reactor.addSystemEventTrigger("before", "shutdown", self.kill_all_applications) if not self.abort_start: # Trigger the ready flag if we had one if self.ready_callable: self.ready_callable() + # Run the lifespan setup + if self.lifespan_context is not None: + evloop.run_until_complete(self.lifespan_context.__aenter__()) # Run the reactor - reactor.run(installSignalHandlers=self.signal_handlers) + try: + reactor.run(installSignalHandlers=self.signal_handlers) + finally: + # Execute lifespan cleanup + if self.lifespan_context is not None: + evloop.run_until_complete(self.lifespan_context.__aexit__()) def listen_success(self, port): """ diff --git a/pyproject.toml b/pyproject.toml index 9b410aa..11abc5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "daphne" dynamic = ["version"] description = "Django ASGI (HTTP/WebSocket) server" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "Django Software Foundation", email = "foundation@djangoproject.com" }, ] @@ -16,7 +16,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -24,7 +23,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] -dependencies = ["asgiref>=3.5.2,<4", "autobahn>=22.4.2", "twisted[tls]>=22.4"] +dependencies = ["asgiref>=3.5.2,<4", "autobahn>=22.4.2", "twisted[tls]>=22.4", "monkay>=0.5.0"] [project.optional-dependencies] tests = [ diff --git a/tests/test_cli.py b/tests/test_cli.py index 59f44e7..770ae1f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -253,6 +253,12 @@ class TestCLIInterface(TestCase): self.assertCLI(["--server-name", ""], {"server_name": ""}) self.assertCLI(["--server-name", "python"], {"server_name": "python"}) + def test_enable_lifespan(self): + """ + Passing `--enable-lifespan` will set enable_lifespan. + """ + self.assertCLI(["--enable-lifespan"], {"enable_lifespan": True}) + def test_no_servername(self): """ Passing `--no-server-name` will set server name to '' (empty string) diff --git a/tox.ini b/tox.ini index e3bae91..990781f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{39,310,311,312,313} + py{310,311,312,313} [testenv] extras = tests