This commit is contained in:
Alexander 2025-11-08 18:50:30 +01:00 committed by GitHub
commit f3140c5448
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 43 additions and 10 deletions

View File

@ -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),

View File

@ -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 "",

View File

@ -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):
"""

View File

@ -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 = [

View File

@ -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)

View File

@ -1,6 +1,6 @@
[tox]
envlist =
py{39,310,311,312,313}
py{310,311,312,313}
[testenv]
extras = tests