mirror of
https://github.com/django/daphne.git
synced 2024-11-27 10:13:44 +03:00
Added runserver to Daphne. (#429)
* Made daphne installable as a Django app. * Added system check to ensure daphne is installed before django.contrib.staticfiles. * Moved runserver command from Channels. * Added changelog entry for runserver command.
This commit is contained in:
parent
438b7ad06d
commit
2b13b74ce2
|
@ -26,6 +26,16 @@ Unreleased
|
||||||
Set e.g. ``ASGI_THREADS=4 daphne ...`` when running to limit the number of
|
Set e.g. ``ASGI_THREADS=4 daphne ...`` when running to limit the number of
|
||||||
workers.
|
workers.
|
||||||
|
|
||||||
|
* Added a ``runserver`` command to run an ASGI Django development server.
|
||||||
|
|
||||||
|
Added ``"daphne"`` to the ``INSTALLED_APPS`` setting, before
|
||||||
|
``"django.contrib.staticfiles"`` to enable:
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"daphne",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
3.0.2 (2021-04-07)
|
3.0.2 (2021-04-07)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
16
daphne/apps.py
Normal file
16
daphne/apps.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Import the server here to ensure the reactor is installed very early on in case other
|
||||||
|
# packages import twisted.internet.reactor (e.g. raven does this).
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.core import checks
|
||||||
|
|
||||||
|
import daphne.server # noqa: F401
|
||||||
|
|
||||||
|
from .checks import check_daphne_installed
|
||||||
|
|
||||||
|
|
||||||
|
class DaphneConfig(AppConfig):
|
||||||
|
name = "daphne"
|
||||||
|
verbose_name = "Daphne"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
checks.register(check_daphne_installed, checks.Tags.staticfiles)
|
21
daphne/checks.py
Normal file
21
daphne/checks.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Django system check to ensure daphne app is listed in INSTALLED_APPS before django.contrib.staticfiles.
|
||||||
|
from django.core.checks import Error, register
|
||||||
|
|
||||||
|
|
||||||
|
@register()
|
||||||
|
def check_daphne_installed(app_configs, **kwargs):
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.staticfiles.apps import StaticFilesConfig
|
||||||
|
|
||||||
|
from daphne.apps import DaphneConfig
|
||||||
|
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
if isinstance(app, DaphneConfig):
|
||||||
|
return []
|
||||||
|
if isinstance(app, StaticFilesConfig):
|
||||||
|
return [
|
||||||
|
Error(
|
||||||
|
"Daphne must be listed before django.contrib.staticfiles in INSTALLED_APPS.",
|
||||||
|
id="daphne.E001",
|
||||||
|
)
|
||||||
|
]
|
0
daphne/management/__init__.py
Normal file
0
daphne/management/__init__.py
Normal file
0
daphne/management/commands/__init__.py
Normal file
0
daphne/management/commands/__init__.py
Normal file
191
daphne/management/commands/runserver.py
Normal file
191
daphne/management/commands/runserver.py
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import datetime
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.core.management import CommandError
|
||||||
|
from django.core.management.commands.runserver import Command as RunserverCommand
|
||||||
|
|
||||||
|
from daphne import __version__
|
||||||
|
from daphne.endpoints import build_endpoint_description_strings
|
||||||
|
from daphne.server import Server
|
||||||
|
|
||||||
|
logger = logging.getLogger("django.channels.server")
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_application():
|
||||||
|
"""
|
||||||
|
Gets the default application, set in the ASGI_APPLICATION setting.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path, name = settings.ASGI_APPLICATION.rsplit(".", 1)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
raise ImproperlyConfigured("Cannot find ASGI_APPLICATION setting.")
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(path)
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured("Cannot import ASGI_APPLICATION module %r" % path)
|
||||||
|
try:
|
||||||
|
value = getattr(module, name)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"Cannot find {name!r} in ASGI_APPLICATION module {path}"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Command(RunserverCommand):
|
||||||
|
protocol = "http"
|
||||||
|
server_cls = Server
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
super().add_arguments(parser)
|
||||||
|
parser.add_argument(
|
||||||
|
"--noasgi",
|
||||||
|
action="store_false",
|
||||||
|
dest="use_asgi",
|
||||||
|
default=True,
|
||||||
|
help="Run the old WSGI-based runserver rather than the ASGI-based one",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--http_timeout",
|
||||||
|
action="store",
|
||||||
|
dest="http_timeout",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Specify the daphne http_timeout interval in seconds "
|
||||||
|
"(default: no timeout)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--websocket_handshake_timeout",
|
||||||
|
action="store",
|
||||||
|
dest="websocket_handshake_timeout",
|
||||||
|
type=int,
|
||||||
|
default=5,
|
||||||
|
help=(
|
||||||
|
"Specify the daphne websocket_handshake_timeout interval in "
|
||||||
|
"seconds (default: 5)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.http_timeout = options.get("http_timeout", None)
|
||||||
|
self.websocket_handshake_timeout = options.get("websocket_handshake_timeout", 5)
|
||||||
|
# Check Channels is installed right
|
||||||
|
if options["use_asgi"] and not hasattr(settings, "ASGI_APPLICATION"):
|
||||||
|
raise CommandError(
|
||||||
|
"You have not set ASGI_APPLICATION, which is needed to run the server."
|
||||||
|
)
|
||||||
|
# Dispatch upward
|
||||||
|
super().handle(*args, **options)
|
||||||
|
|
||||||
|
def inner_run(self, *args, **options):
|
||||||
|
# Maybe they want the wsgi one?
|
||||||
|
if not options.get("use_asgi", True):
|
||||||
|
if hasattr(RunserverCommand, "server_cls"):
|
||||||
|
self.server_cls = RunserverCommand.server_cls
|
||||||
|
return RunserverCommand.inner_run(self, *args, **options)
|
||||||
|
# Run checks
|
||||||
|
self.stdout.write("Performing system checks...\n\n")
|
||||||
|
self.check(display_num_errors=True)
|
||||||
|
self.check_migrations()
|
||||||
|
# Print helpful text
|
||||||
|
quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-C"
|
||||||
|
now = datetime.datetime.now().strftime("%B %d, %Y - %X")
|
||||||
|
self.stdout.write(now)
|
||||||
|
self.stdout.write(
|
||||||
|
(
|
||||||
|
"Django version %(version)s, using settings %(settings)r\n"
|
||||||
|
"Starting ASGI/Daphne version %(daphne_version)s development server"
|
||||||
|
" at %(protocol)s://%(addr)s:%(port)s/\n"
|
||||||
|
"Quit the server with %(quit_command)s.\n"
|
||||||
|
)
|
||||||
|
% {
|
||||||
|
"version": self.get_version(),
|
||||||
|
"daphne_version": __version__,
|
||||||
|
"settings": settings.SETTINGS_MODULE,
|
||||||
|
"protocol": self.protocol,
|
||||||
|
"addr": "[%s]" % self.addr if self._raw_ipv6 else self.addr,
|
||||||
|
"port": self.port,
|
||||||
|
"quit_command": quit_command,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Launch server in 'main' thread. Signals are disabled as it's still
|
||||||
|
# actually a subthread under the autoreloader.
|
||||||
|
logger.debug("Daphne running, listening on %s:%s", self.addr, self.port)
|
||||||
|
|
||||||
|
# build the endpoint description string from host/port options
|
||||||
|
endpoints = build_endpoint_description_strings(host=self.addr, port=self.port)
|
||||||
|
try:
|
||||||
|
self.server_cls(
|
||||||
|
application=self.get_application(options),
|
||||||
|
endpoints=endpoints,
|
||||||
|
signal_handlers=not options["use_reloader"],
|
||||||
|
action_logger=self.log_action,
|
||||||
|
http_timeout=self.http_timeout,
|
||||||
|
root_path=getattr(settings, "FORCE_SCRIPT_NAME", "") or "",
|
||||||
|
websocket_handshake_timeout=self.websocket_handshake_timeout,
|
||||||
|
).run()
|
||||||
|
logger.debug("Daphne exited")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
shutdown_message = options.get("shutdown_message", "")
|
||||||
|
if shutdown_message:
|
||||||
|
self.stdout.write(shutdown_message)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_application(self, options):
|
||||||
|
"""
|
||||||
|
Returns the static files serving application wrapping the default application,
|
||||||
|
if static files should be served. Otherwise just returns the default
|
||||||
|
handler.
|
||||||
|
"""
|
||||||
|
staticfiles_installed = apps.is_installed("django.contrib.staticfiles")
|
||||||
|
use_static_handler = options.get("use_static_handler", staticfiles_installed)
|
||||||
|
insecure_serving = options.get("insecure_serving", False)
|
||||||
|
if use_static_handler and (settings.DEBUG or insecure_serving):
|
||||||
|
return ASGIStaticFilesHandler(get_default_application())
|
||||||
|
else:
|
||||||
|
return get_default_application()
|
||||||
|
|
||||||
|
def log_action(self, protocol, action, details):
|
||||||
|
"""
|
||||||
|
Logs various different kinds of requests to the console.
|
||||||
|
"""
|
||||||
|
# HTTP requests
|
||||||
|
if protocol == "http" and action == "complete":
|
||||||
|
msg = "HTTP %(method)s %(path)s %(status)s [%(time_taken).2f, %(client)s]"
|
||||||
|
|
||||||
|
# Utilize terminal colors, if available
|
||||||
|
if 200 <= details["status"] < 300:
|
||||||
|
# Put 2XX first, since it should be the common case
|
||||||
|
logger.info(self.style.HTTP_SUCCESS(msg), details)
|
||||||
|
elif 100 <= details["status"] < 200:
|
||||||
|
logger.info(self.style.HTTP_INFO(msg), details)
|
||||||
|
elif details["status"] == 304:
|
||||||
|
logger.info(self.style.HTTP_NOT_MODIFIED(msg), details)
|
||||||
|
elif 300 <= details["status"] < 400:
|
||||||
|
logger.info(self.style.HTTP_REDIRECT(msg), details)
|
||||||
|
elif details["status"] == 404:
|
||||||
|
logger.warning(self.style.HTTP_NOT_FOUND(msg), details)
|
||||||
|
elif 400 <= details["status"] < 500:
|
||||||
|
logger.warning(self.style.HTTP_BAD_REQUEST(msg), details)
|
||||||
|
else:
|
||||||
|
# Any 5XX, or any other response
|
||||||
|
logger.error(self.style.HTTP_SERVER_ERROR(msg), details)
|
||||||
|
|
||||||
|
# Websocket requests
|
||||||
|
elif protocol == "websocket" and action == "connected":
|
||||||
|
logger.info("WebSocket CONNECT %(path)s [%(client)s]", details)
|
||||||
|
elif protocol == "websocket" and action == "disconnected":
|
||||||
|
logger.info("WebSocket DISCONNECT %(path)s [%(client)s]", details)
|
||||||
|
elif protocol == "websocket" and action == "connecting":
|
||||||
|
logger.info("WebSocket HANDSHAKING %(path)s [%(client)s]", details)
|
||||||
|
elif protocol == "websocket" and action == "rejected":
|
||||||
|
logger.info("WebSocket REJECT %(path)s [%(client)s]", details)
|
2
setup.py
2
setup.py
|
@ -25,7 +25,7 @@ setup(
|
||||||
install_requires=["twisted[tls]>=22.4", "autobahn>=22.4.2", "asgiref>=3.5.2,<4"],
|
install_requires=["twisted[tls]>=22.4", "autobahn>=22.4.2", "asgiref>=3.5.2,<4"],
|
||||||
python_requires=">=3.7",
|
python_requires=">=3.7",
|
||||||
setup_requires=["pytest-runner"],
|
setup_requires=["pytest-runner"],
|
||||||
extras_require={"tests": ["hypothesis", "pytest", "pytest-asyncio"]},
|
extras_require={"tests": ["hypothesis", "pytest", "pytest-asyncio", "django"]},
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": ["daphne = daphne.cli:CommandLineInterface.entrypoint"]
|
"console_scripts": ["daphne = daphne.cli:CommandLineInterface.entrypoint"]
|
||||||
},
|
},
|
||||||
|
|
21
tests/test_checks.py
Normal file
21
tests/test_checks.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
from daphne.checks import check_daphne_installed
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_daphne_installed():
|
||||||
|
"""
|
||||||
|
Test check error is raised if daphne is not listed before staticfiles, and vice versa.
|
||||||
|
"""
|
||||||
|
settings.configure(
|
||||||
|
INSTALLED_APPS=["daphne.apps.DaphneConfig", "django.contrib.staticfiles"]
|
||||||
|
)
|
||||||
|
django.setup()
|
||||||
|
errors = check_daphne_installed(None)
|
||||||
|
assert len(errors) == 0
|
||||||
|
with override_settings(INSTALLED_APPS=["django.contrib.staticfiles", "daphne"]):
|
||||||
|
errors = check_daphne_installed(None)
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert errors[0].id == "daphne.E001"
|
Loading…
Reference in New Issue
Block a user