mirror of
https://github.com/django/daphne.git
synced 2024-11-24 08:53:43 +03:00
Merge branch 'main' into master
This commit is contained in:
commit
50979f1679
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
24
.github/workflows/pre-commit.yml
vendored
24
.github/workflows/pre-commit.yml
vendored
|
@ -1,24 +0,0 @@
|
||||||
name: pre-commit
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-commit:
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
|
|
||||||
- uses: pre-commit/action@v2.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
24
.github/workflows/tests.yml
vendored
24
.github/workflows/tests.yml
vendored
|
@ -3,33 +3,37 @@ name: Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu
|
||||||
|
- windows
|
||||||
python-version:
|
python-version:
|
||||||
- 3.6
|
- "3.7"
|
||||||
- 3.7
|
- "3.8"
|
||||||
- 3.8
|
- "3.9"
|
||||||
- 3.9
|
- "3.10"
|
||||||
|
- "3.11"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
python -m pip install --upgrade tox tox-py
|
python -m pip install --upgrade tox
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
- name: Run tox targets for ${{ matrix.python-version }}
|
||||||
run: tox --py current
|
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.11.0
|
rev: v3.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py36-plus]
|
args: [--py36-plus]
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 23.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: 5.8.0
|
rev: 5.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.9.0
|
rev: 6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
|
|
@ -1,3 +1,54 @@
|
||||||
|
4.0.0 (2022-10-07)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Major versioning targeting use with Channels 4.0 and beyond. Except where
|
||||||
|
noted should remain usable with Channels v3 projects, but updating Channels to the latest version is recommended.
|
||||||
|
|
||||||
|
* 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",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
This replaces the Channels implementation of ``runserver``, which is removed
|
||||||
|
in Channels 4.0.
|
||||||
|
|
||||||
|
* Made the ``DaphneProcess`` tests helper class compatible with the ``spawn``
|
||||||
|
process start method, which is used on macOS and Windows.
|
||||||
|
|
||||||
|
Note that requires Channels v4 if using with ``ChannelsLiveServerTestCase``.
|
||||||
|
|
||||||
|
* Dropped support for Python 3.6.
|
||||||
|
|
||||||
|
* Updated dependencies to the latest versions.
|
||||||
|
|
||||||
|
Previously a range of Twisted versions have been supported. Recent Twisted
|
||||||
|
releases (22.2, 22.4) have issued security fixes, so those are now the
|
||||||
|
minimum supported version. Given the stability of Twisted, supporting a
|
||||||
|
range of versions does not represent a good use of maintainer time. Going
|
||||||
|
forward the latest Twisted version will be required.
|
||||||
|
|
||||||
|
* Set ``daphne`` as default ``Server`` header.
|
||||||
|
|
||||||
|
This can be configured with the ``--server-name`` CLI argument.
|
||||||
|
|
||||||
|
Added the new ``--no-server-name`` CLI argument to disable the ``Server``
|
||||||
|
header, which is equivalent to ``--server-name=` (an empty name).
|
||||||
|
|
||||||
|
* Added ``--log-fmt`` CLI argument.
|
||||||
|
|
||||||
|
* Added support for ``ASGI_THREADS`` environment variable, setting the maximum
|
||||||
|
number of workers used by a ``SyncToAsync`` thread-pool executor.
|
||||||
|
|
||||||
|
Set e.g. ``ASGI_THREADS=4 daphne ...`` when running to limit the number of
|
||||||
|
workers.
|
||||||
|
|
||||||
|
* Removed deprecated ``--ws_protocols`` CLI option.
|
||||||
|
|
||||||
3.0.2 (2021-04-07)
|
3.0.2 (2021-04-07)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ Daphne supports terminating HTTP/2 connections natively. You'll
|
||||||
need to do a couple of things to get it working, though. First, you need to
|
need to do a couple of things to get it working, though. First, you need to
|
||||||
make sure you install the Twisted ``http2`` and ``tls`` extras::
|
make sure you install the Twisted ``http2`` and ``tls`` extras::
|
||||||
|
|
||||||
pip install -U 'Twisted[tls,http2]'
|
pip install -U "Twisted[tls,http2]"
|
||||||
|
|
||||||
Next, because all current browsers only support HTTP/2 when using TLS, you will
|
Next, because all current browsers only support HTTP/2 when using TLS, you will
|
||||||
need to start Daphne with TLS turned on, which can be done using the Twisted endpoint syntax::
|
need to start Daphne with TLS turned on, which can be done using the Twisted endpoint syntax::
|
||||||
|
@ -122,7 +122,7 @@ The compression implementation is provided by
|
||||||
Python Support
|
Python Support
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Daphne requires Python 3.6 or later.
|
Daphne requires Python 3.7 or later.
|
||||||
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
__version__ = "3.0.2"
|
__version__ = "4.0.0"
|
||||||
|
|
||||||
|
|
||||||
# Windows on Python 3.8+ uses ProactorEventLoop, which is not compatible with
|
# Windows on Python 3.8+ uses ProactorEventLoop, which is not compatible with
|
||||||
|
|
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",
|
||||||
|
)
|
||||||
|
]
|
|
@ -90,6 +90,11 @@ class CommandLineInterface:
|
||||||
help="Where to write the access log (- for stdout, the default for verbosity=1)",
|
help="Where to write the access log (- for stdout, the default for verbosity=1)",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--log-fmt",
|
||||||
|
help="Log format to use",
|
||||||
|
default="%(asctime)-15s %(levelname)-8s %(message)s",
|
||||||
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
"--ping-interval",
|
"--ping-interval",
|
||||||
type=int,
|
type=int,
|
||||||
|
@ -108,13 +113,6 @@ class CommandLineInterface:
|
||||||
help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
|
help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
|
||||||
default=10,
|
default=10,
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
|
||||||
"--ws-protocol",
|
|
||||||
nargs="*",
|
|
||||||
dest="ws_protocols",
|
|
||||||
help="The WebSocket protocols you wish to support",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
"--root-path",
|
"--root-path",
|
||||||
dest="root_path",
|
dest="root_path",
|
||||||
|
@ -157,7 +155,10 @@ class CommandLineInterface:
|
||||||
"--server-name",
|
"--server-name",
|
||||||
dest="server_name",
|
dest="server_name",
|
||||||
help="specify which value should be passed to response header Server attribute",
|
help="specify which value should be passed to response header Server attribute",
|
||||||
default="Daphne",
|
default="daphne",
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--no-server-name", dest="server_name", action="store_const", const=""
|
||||||
)
|
)
|
||||||
|
|
||||||
self.server = None
|
self.server = None
|
||||||
|
@ -215,7 +216,7 @@ class CommandLineInterface:
|
||||||
2: logging.DEBUG,
|
2: logging.DEBUG,
|
||||||
3: logging.DEBUG, # Also turns on asyncio debug
|
3: logging.DEBUG, # Also turns on asyncio debug
|
||||||
}[args.verbosity],
|
}[args.verbosity],
|
||||||
format="%(asctime)-15s %(levelname)-8s %(message)s",
|
format=args.log_fmt,
|
||||||
)
|
)
|
||||||
# If verbosity is 1 or greater, or they told us explicitly, set up access log
|
# If verbosity is 1 or greater, or they told us explicitly, set up access log
|
||||||
access_log_stream = None
|
access_log_stream = None
|
||||||
|
@ -272,7 +273,6 @@ class CommandLineInterface:
|
||||||
action_logger=AccessLogGenerator(access_log_stream)
|
action_logger=AccessLogGenerator(access_log_stream)
|
||||||
if access_log_stream
|
if access_log_stream
|
||||||
else None,
|
else None,
|
||||||
ws_protocols=args.ws_protocols,
|
|
||||||
root_path=args.root_path,
|
root_path=args.root_path,
|
||||||
verbosity=args.verbosity,
|
verbosity=args.verbosity,
|
||||||
proxy_forwarded_address_header=self._get_forwarded_host(args=args),
|
proxy_forwarded_address_header=self._get_forwarded_host(args=args),
|
||||||
|
|
|
@ -50,6 +50,8 @@ class WebRequest(http.Request):
|
||||||
) # Shorten it a bit, bytes wise
|
) # Shorten it a bit, bytes wise
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.client_addr = None
|
||||||
|
self.server_addr = None
|
||||||
try:
|
try:
|
||||||
http.Request.__init__(self, *args, **kwargs)
|
http.Request.__init__(self, *args, **kwargs)
|
||||||
# Easy server link
|
# Easy server link
|
||||||
|
@ -77,9 +79,6 @@ class WebRequest(http.Request):
|
||||||
# requires unicode string.
|
# requires unicode string.
|
||||||
self.client_addr = [str(self.client.host), self.client.port]
|
self.client_addr = [str(self.client.host), self.client.port]
|
||||||
self.server_addr = [str(self.host.host), self.host.port]
|
self.server_addr = [str(self.host.host), self.host.port]
|
||||||
else:
|
|
||||||
self.client_addr = None
|
|
||||||
self.server_addr = None
|
|
||||||
|
|
||||||
self.client_scheme = "https" if self.isSecure() else "http"
|
self.client_scheme = "https" if self.isSecure() else "http"
|
||||||
|
|
||||||
|
@ -250,8 +249,8 @@ class WebRequest(http.Request):
|
||||||
# Write headers
|
# Write headers
|
||||||
for header, value in message.get("headers", {}):
|
for header, value in message.get("headers", {}):
|
||||||
self.responseHeaders.addRawHeader(header, value)
|
self.responseHeaders.addRawHeader(header, value)
|
||||||
if self.server.server_name and self.server.server_name.lower() != "daphne":
|
if self.server.server_name and not self.responseHeaders.hasHeader("server"):
|
||||||
self.setHeader(b"server", self.server.server_name.encode("utf-8"))
|
self.setHeader(b"server", self.server.server_name.encode())
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"HTTP %s response started for %s", message["status"], self.client_addr
|
"HTTP %s response started for %s", message["status"], self.client_addr
|
||||||
)
|
)
|
||||||
|
|
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)
|
|
@ -1,10 +1,18 @@
|
||||||
# This has to be done first as Twisted is import-order-sensitive with reactors
|
# This has to be done first as Twisted is import-order-sensitive with reactors
|
||||||
import asyncio # isort:skip
|
import asyncio # isort:skip
|
||||||
|
import os # isort:skip
|
||||||
import sys # isort:skip
|
import sys # isort:skip
|
||||||
import warnings # isort:skip
|
import warnings # isort:skip
|
||||||
|
from concurrent.futures import ThreadPoolExecutor # isort:skip
|
||||||
from twisted.internet import asyncioreactor # isort:skip
|
from twisted.internet import asyncioreactor # isort:skip
|
||||||
|
|
||||||
|
|
||||||
twisted_loop = asyncio.new_event_loop()
|
twisted_loop = asyncio.new_event_loop()
|
||||||
|
if "ASGI_THREADS" in os.environ:
|
||||||
|
twisted_loop.set_default_executor(
|
||||||
|
ThreadPoolExecutor(max_workers=int(os.environ["ASGI_THREADS"]))
|
||||||
|
)
|
||||||
|
|
||||||
current_reactor = sys.modules.get("twisted.internet.reactor", None)
|
current_reactor = sys.modules.get("twisted.internet.reactor", None)
|
||||||
if current_reactor is not None:
|
if current_reactor is not None:
|
||||||
if not isinstance(current_reactor, asyncioreactor.AsyncioSelectorReactor):
|
if not isinstance(current_reactor, asyncioreactor.AsyncioSelectorReactor):
|
||||||
|
@ -13,6 +21,7 @@ if current_reactor is not None:
|
||||||
+ "you can fix this warning by importing daphne.server early in your codebase or "
|
+ "you can fix this warning by importing daphne.server early in your codebase or "
|
||||||
+ "finding the package that imports Twisted and importing it later on.",
|
+ "finding the package that imports Twisted and importing it later on.",
|
||||||
UserWarning,
|
UserWarning,
|
||||||
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
del sys.modules["twisted.internet.reactor"]
|
del sys.modules["twisted.internet.reactor"]
|
||||||
asyncioreactor.install(twisted_loop)
|
asyncioreactor.install(twisted_loop)
|
||||||
|
@ -62,9 +71,7 @@ class Server:
|
||||||
websocket_handshake_timeout=5,
|
websocket_handshake_timeout=5,
|
||||||
application_close_timeout=10,
|
application_close_timeout=10,
|
||||||
ready_callable=None,
|
ready_callable=None,
|
||||||
server_name="Daphne",
|
server_name="daphne",
|
||||||
# Deprecated and does not work, remove in version 2.2
|
|
||||||
ws_protocols=None,
|
|
||||||
):
|
):
|
||||||
self.application = application
|
self.application = application
|
||||||
self.endpoints = endpoints or []
|
self.endpoints = endpoints or []
|
||||||
|
|
|
@ -26,6 +26,9 @@ class BaseDaphneTestingInstance:
|
||||||
self.request_buffer_size = request_buffer_size
|
self.request_buffer_size = request_buffer_size
|
||||||
self.application = application
|
self.application = application
|
||||||
|
|
||||||
|
def get_application(self):
|
||||||
|
return self.application
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
# Option Daphne features
|
# Option Daphne features
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
@ -41,7 +44,7 @@ class BaseDaphneTestingInstance:
|
||||||
# Start up process
|
# Start up process
|
||||||
self.process = DaphneProcess(
|
self.process = DaphneProcess(
|
||||||
host=self.host,
|
host=self.host,
|
||||||
application=self.application,
|
get_application=self.get_application,
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
setup=self.process_setup,
|
setup=self.process_setup,
|
||||||
teardown=self.process_teardown,
|
teardown=self.process_teardown,
|
||||||
|
@ -123,13 +126,13 @@ class DaphneProcess(multiprocessing.Process):
|
||||||
port it ends up listening on back to the parent process.
|
port it ends up listening on back to the parent process.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host, application, kwargs=None, setup=None, teardown=None):
|
def __init__(self, host, get_application, kwargs=None, setup=None, teardown=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.host = host
|
self.host = host
|
||||||
self.application = application
|
self.get_application = get_application
|
||||||
self.kwargs = kwargs or {}
|
self.kwargs = kwargs or {}
|
||||||
self.setup = setup or (lambda: None)
|
self.setup = setup
|
||||||
self.teardown = teardown or (lambda: None)
|
self.teardown = teardown
|
||||||
self.port = multiprocessing.Value("i")
|
self.port = multiprocessing.Value("i")
|
||||||
self.ready = multiprocessing.Event()
|
self.ready = multiprocessing.Event()
|
||||||
self.errors = multiprocessing.Queue()
|
self.errors = multiprocessing.Queue()
|
||||||
|
@ -146,11 +149,13 @@ class DaphneProcess(multiprocessing.Process):
|
||||||
from .endpoints import build_endpoint_description_strings
|
from .endpoints import build_endpoint_description_strings
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
|
||||||
|
application = self.get_application()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the server class
|
# Create the server class
|
||||||
endpoints = build_endpoint_description_strings(host=self.host, port=0)
|
endpoints = build_endpoint_description_strings(host=self.host, port=0)
|
||||||
self.server = Server(
|
self.server = Server(
|
||||||
application=self.application,
|
application=application,
|
||||||
endpoints=endpoints,
|
endpoints=endpoints,
|
||||||
signal_handlers=False,
|
signal_handlers=False,
|
||||||
**self.kwargs
|
**self.kwargs
|
||||||
|
@ -158,11 +163,13 @@ class DaphneProcess(multiprocessing.Process):
|
||||||
# Set up a poller to look for the port
|
# Set up a poller to look for the port
|
||||||
reactor.callLater(0.1, self.resolve_port)
|
reactor.callLater(0.1, self.resolve_port)
|
||||||
# Run with setup/teardown
|
# Run with setup/teardown
|
||||||
self.setup()
|
if self.setup is not None:
|
||||||
|
self.setup()
|
||||||
try:
|
try:
|
||||||
self.server.run()
|
self.server.run()
|
||||||
finally:
|
finally:
|
||||||
self.teardown()
|
if self.teardown is not None:
|
||||||
|
self.teardown()
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
# Put the error on our queue so the parent gets it
|
# Put the error on our queue so the parent gets it
|
||||||
self.errors.put((e, traceback.format_exc()))
|
self.errors.put((e, traceback.format_exc()))
|
||||||
|
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
51
setup.cfg
51
setup.cfg
|
@ -1,3 +1,53 @@
|
||||||
|
[metadata]
|
||||||
|
name = daphne
|
||||||
|
version = attr: daphne.__version__
|
||||||
|
url = https://github.com/django/daphne
|
||||||
|
author = Django Software Foundation
|
||||||
|
author_email = foundation@djangoproject.com
|
||||||
|
description = Django ASGI (HTTP/WebSocket) server
|
||||||
|
long_description = file: README.rst
|
||||||
|
long_description_content_type = text/x-rst
|
||||||
|
license = BSD
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 4 - Beta
|
||||||
|
Environment :: Web Environment
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Programming Language :: Python
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: 3.11
|
||||||
|
Topic :: Internet :: WWW/HTTP
|
||||||
|
|
||||||
|
[options]
|
||||||
|
package_dir =
|
||||||
|
twisted=daphne/twisted
|
||||||
|
packages = find:
|
||||||
|
include_package_data = True
|
||||||
|
install_requires =
|
||||||
|
asgiref>=3.5.2,<4
|
||||||
|
autobahn>=22.4.2
|
||||||
|
twisted[tls]>=22.4
|
||||||
|
python_requires = >=3.7
|
||||||
|
setup_requires =
|
||||||
|
pytest-runner
|
||||||
|
zip_safe = False
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
daphne = daphne.cli:CommandLineInterface.entrypoint
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
tests =
|
||||||
|
django
|
||||||
|
hypothesis
|
||||||
|
pytest
|
||||||
|
pytest-asyncio
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = venv/*,tox/*,docs/*,testproject/*,js_client/*,.eggs/*
|
exclude = venv/*,tox/*,docs/*,testproject/*,js_client/*,.eggs/*
|
||||||
extend-ignore = E123, E128, E266, E402, W503, E731, W601
|
extend-ignore = E123, E128, E266, E402, W503, E731, W601
|
||||||
|
@ -8,3 +58,4 @@ profile = black
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
|
asyncio_mode = strict
|
||||||
|
|
48
setup.py
48
setup.py
|
@ -1,48 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
|
||||||
|
|
||||||
from daphne import __version__
|
|
||||||
|
|
||||||
# We use the README as the long_description
|
|
||||||
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
|
|
||||||
with open(readme_path) as fp:
|
|
||||||
long_description = fp.read()
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="daphne",
|
|
||||||
version=__version__,
|
|
||||||
url="https://github.com/django/daphne",
|
|
||||||
author="Django Software Foundation",
|
|
||||||
author_email="foundation@djangoproject.com",
|
|
||||||
description="Django ASGI (HTTP/WebSocket) server",
|
|
||||||
long_description=long_description,
|
|
||||||
license="BSD",
|
|
||||||
zip_safe=False,
|
|
||||||
package_dir={"twisted": "daphne/twisted"},
|
|
||||||
packages=find_packages() + ["twisted.plugins"],
|
|
||||||
include_package_data=True,
|
|
||||||
install_requires=["twisted[tls]>=18.7", "autobahn>=0.18", "asgiref>=3.2.10,<4"],
|
|
||||||
python_requires=">=3.6",
|
|
||||||
setup_requires=["pytest-runner"],
|
|
||||||
extras_require={
|
|
||||||
"tests": ["hypothesis==4.23", "pytest~=3.10", "pytest-asyncio~=0.8"]
|
|
||||||
},
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": ["daphne = daphne.cli:CommandLineInterface.entrypoint"]
|
|
||||||
},
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 4 - Beta",
|
|
||||||
"Environment :: Web Environment",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"License :: OSI Approved :: BSD License",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
|
||||||
],
|
|
||||||
)
|
|
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"
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from argparse import ArgumentError
|
from argparse import ArgumentError
|
||||||
from unittest import TestCase
|
from unittest import TestCase, skipUnless
|
||||||
|
|
||||||
from daphne.cli import CommandLineInterface
|
from daphne.cli import CommandLineInterface
|
||||||
from daphne.endpoints import build_endpoint_description_strings as build
|
from daphne.endpoints import build_endpoint_description_strings as build
|
||||||
|
@ -240,3 +241,27 @@ class TestCLIInterface(TestCase):
|
||||||
exc.exception.message,
|
exc.exception.message,
|
||||||
"--proxy-headers has to be passed for this parameter.",
|
"--proxy-headers has to be passed for this parameter.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_custom_servername(self):
|
||||||
|
"""
|
||||||
|
Passing `--server-name` will set the default server header
|
||||||
|
from 'daphne' to the passed one.
|
||||||
|
"""
|
||||||
|
self.assertCLI([], {"server_name": "daphne"})
|
||||||
|
self.assertCLI(["--server-name", ""], {"server_name": ""})
|
||||||
|
self.assertCLI(["--server-name", "python"], {"server_name": "python"})
|
||||||
|
|
||||||
|
def test_no_servername(self):
|
||||||
|
"""
|
||||||
|
Passing `--no-server-name` will set server name to '' (empty string)
|
||||||
|
"""
|
||||||
|
self.assertCLI(["--no-server-name"], {"server_name": ""})
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(os.getenv("ASGI_THREADS"), "ASGI_THREADS environment variable not set.")
|
||||||
|
class TestASGIThreads(TestCase):
|
||||||
|
def test_default_executor(self):
|
||||||
|
from daphne.server import twisted_loop
|
||||||
|
|
||||||
|
executor = twisted_loop._default_executor
|
||||||
|
self.assertEqual(executor._max_workers, int(os.getenv("ASGI_THREADS")))
|
||||||
|
|
49
tests/test_http_protocol.py
Normal file
49
tests/test_http_protocol.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from daphne.http_protocol import WebRequest
|
||||||
|
|
||||||
|
|
||||||
|
class MockServer:
|
||||||
|
"""
|
||||||
|
Mock server object for testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def protocol_connected(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockFactory:
|
||||||
|
"""
|
||||||
|
Mock factory object for testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.server = MockServer()
|
||||||
|
|
||||||
|
|
||||||
|
class MockChannel:
|
||||||
|
"""
|
||||||
|
Mock channel object for testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.factory = MockFactory()
|
||||||
|
self.transport = None
|
||||||
|
|
||||||
|
def getPeer(self, *args, **kwargs):
|
||||||
|
return "peer"
|
||||||
|
|
||||||
|
def getHost(self, *args, **kwargs):
|
||||||
|
return "host"
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPProtocol(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests the HTTP protocol classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_web_request_initialisation(self):
|
||||||
|
channel = MockChannel()
|
||||||
|
request = WebRequest(channel)
|
||||||
|
self.assertIsNone(request.client_addr)
|
||||||
|
self.assertIsNone(request.server_addr)
|
|
@ -13,10 +13,11 @@ class TestHTTPResponse(DaphneTestCase):
|
||||||
Lowercases and sorts headers, and strips transfer-encoding ones.
|
Lowercases and sorts headers, and strips transfer-encoding ones.
|
||||||
"""
|
"""
|
||||||
return sorted(
|
return sorted(
|
||||||
[
|
[(b"server", b"daphne")]
|
||||||
|
+ [
|
||||||
(name.lower(), value.strip())
|
(name.lower(), value.strip())
|
||||||
for name, value in headers
|
for name, value in headers
|
||||||
if name.lower() != b"transfer-encoding"
|
if name.lower() not in (b"server", b"transfer-encoding")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -342,7 +342,7 @@ async def cancelling_application(scope, receive, send):
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
# Stop the server after a short delay so that the teardown is run.
|
# Stop the server after a short delay so that the teardown is run.
|
||||||
reactor.callLater(2, lambda: reactor.stop())
|
reactor.callLater(2, reactor.stop)
|
||||||
await send({"type": "websocket.accept"})
|
await send({"type": "websocket.accept"})
|
||||||
raise asyncio.CancelledError()
|
raise asyncio.CancelledError()
|
||||||
|
|
||||||
|
|
6
tox.ini
6
tox.ini
|
@ -1,12 +1,8 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{36,37,38,39}-twisted{187,latest}
|
py{37,38,39,310,311}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = true
|
|
||||||
extras = tests
|
extras = tests
|
||||||
commands =
|
commands =
|
||||||
pytest -v {posargs}
|
pytest -v {posargs}
|
||||||
deps =
|
|
||||||
twisted187: twisted==18.7.0
|
|
||||||
twistedlatest: twisted>=20.3.0
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user