Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions daphne/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ def __init__(self):
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

Expand Down Expand Up @@ -279,6 +285,7 @@ def run(self, args):
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),
Expand Down
8 changes: 8 additions & 0 deletions daphne/management/commands/runserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ def add_arguments(self, parser):
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",
Expand Down Expand Up @@ -141,6 +148,7 @@ def inner_run(self, *args, **options):
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 "",
Expand Down
25 changes: 19 additions & 6 deletions daphne/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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()
Expand Down Expand Up @@ -66,6 +67,7 @@ def __init__(
application_close_timeout=10,
ready_callable=None,
server_name="daphne",
enable_lifespan=False,
):
self.application = application
self.endpoints = endpoints or []
Expand Down Expand Up @@ -93,6 +95,9 @@ def __init__(
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
Expand Down Expand Up @@ -120,6 +125,10 @@ def run(self):
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)
Expand All @@ -133,21 +142,25 @@ def run(self):
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):
"""
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand All @@ -16,15 +16,14 @@ 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",
"Programming Language :: Python :: 3.13",
"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 = [
Expand Down
6 changes: 6 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ def test_custom_servername(self):
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)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py{39,310,311,312,313}
py{310,311,312,313}

[testenv]
extras = tests
Expand Down