Merge remote-tracking branch 'django-master/master'

# Conflicts:
#	daphne/__init__.py
This commit is contained in:
Bill Schumacher 2020-02-20 13:21:57 -06:00
commit b8a251a14b
9 changed files with 103 additions and 29 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea/
*.egg-info
*.pyc
__pycache__

View File

@ -3,6 +3,8 @@ sudo: false
language: python
python:
- '3.8'
- '3.7'
- '3.6'
- '3.5'
@ -25,16 +27,8 @@ stages:
jobs:
include:
- python: '3.7'
env: TWISTED="twisted==18.7.0"
dist: xenial
sudo: required
- python: '3.7'
env: TWISTED="twisted"
dist: xenial
sudo: required
- stage: lint
python: 3.6
install: pip install -U -e .[tests] black pyflakes isort
script:
- pyflakes daphne tests
@ -42,6 +36,7 @@ jobs:
- isort --check-only --diff --recursive daphne tests
- stage: release
python: 3.6
script: skip
deploy:
provider: pypi

View File

@ -1,3 +1,27 @@
2.4.1 (2019-12-18)
------------------
* Avoids Twisted using the default event loop, for compatibility with Django
3.0's ``async_unsafe()`` decorator in threaded contexts, such as using the
auto-reloader.
2.4.0 (2019-11-20)
------------------
* Adds CI testing against and support for Python 3.8.
* Adds support for ``raw_path`` in ASGI scope.
* Ensures an error response is sent to the client if the application sends
malformed headers.
* Resolves an asyncio + multiprocessing problem when testing that would cause
the test suite to fail/hang on macOS.
* Requires installing Twisted's TLS extras, via ``install_requires``.
* Adds missing LICENSE to distribution.
2.3.0 (2019-04-09)
------------------

View File

@ -1 +1 @@
__version__ = "2.3.0a1"
__version__ = "2.4.1a1"

View File

@ -1,8 +1,10 @@
# This has to be done first as Twisted is import-order-sensitive with reactors
import asyncio # isort:skip
import sys # isort:skip
import warnings # isort:skip
from twisted.internet import asyncioreactor # isort:skip
twisted_loop = asyncio.new_event_loop()
current_reactor = sys.modules.get("twisted.internet.reactor", None)
if current_reactor is not None:
if not isinstance(current_reactor, asyncioreactor.AsyncioSelectorReactor):
@ -13,14 +15,12 @@ if current_reactor is not None:
UserWarning,
)
del sys.modules["twisted.internet.reactor"]
asyncioreactor.install()
asyncioreactor.install(twisted_loop)
else:
asyncioreactor.install()
asyncioreactor.install(twisted_loop)
import asyncio
import logging
import time
import traceback
from concurrent.futures import CancelledError
from twisted.internet import defer, reactor
@ -219,7 +219,12 @@ class Server(object):
"disconnected", None
):
return
try:
self.check_headers_type(message)
except ValueError:
# Ensure to send SOME reply.
protocol.basic_error(500, b"Server Error", "Server Error")
raise
# Let the protocol handle it
protocol.handle_reply(message)
@ -277,13 +282,10 @@ class Server(object):
# Protocol is asking the server to exit (likely during test)
self.stop()
else:
exception_output = "{}\n{}{}".format(
exception,
"".join(traceback.format_tb(exception.__traceback__)),
" {}".format(exception),
)
logger.error(
"Exception inside application: %s", exception_output
"Exception inside application: %s",
exception,
exc_info=exception,
)
if not disconnected:
protocol.handle_exception(exception)

View File

@ -6,11 +6,6 @@ import tempfile
import traceback
from concurrent.futures import CancelledError
from twisted.internet import reactor
from .endpoints import build_endpoint_description_strings
from .server import Server
class DaphneTestingInstance:
"""
@ -121,6 +116,17 @@ class DaphneProcess(multiprocessing.Process):
self.errors = multiprocessing.Queue()
def run(self):
# OK, now we are in a forked child process, and want to use the reactor.
# However, FreeBSD systems like MacOS do not fork the underlying Kqueue,
# which asyncio (hence asyncioreactor) is built on.
# Therefore, we should uninstall the broken reactor and install a new one.
_reinstall_reactor()
from twisted.internet import reactor
from .server import Server
from .endpoints import build_endpoint_description_strings
try:
# Create the server class
endpoints = build_endpoint_description_strings(host=self.host, port=0)
@ -143,6 +149,8 @@ class DaphneProcess(multiprocessing.Process):
self.errors.put((e, traceback.format_exc()))
def resolve_port(self):
from twisted.internet import reactor
if self.server.listening_addresses:
self.port.value = self.server.listening_addresses[0][1]
self.ready.set()
@ -249,3 +257,24 @@ class TestApplication:
os.unlink(cls.result_storage)
except OSError:
pass
def _reinstall_reactor():
import sys
import asyncio
from twisted.internet import asyncioreactor
# Uninstall the reactor.
if "twisted.internet.reactor" in sys.modules:
del sys.modules["twisted.internet.reactor"]
# The daphne.server module may have already installed the reactor.
# If so, using this module will use uninstalled one, thus we should
# reimport this module too.
if "daphne.server" in sys.modules:
del sys.modules["daphne.server"]
event_loop = asyncio.new_event_loop()
asyncioreactor.install(event_loop)
asyncio.set_event_loop(event_loop)

View File

@ -22,7 +22,7 @@ setup(
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.0"],
install_requires=["twisted[tls]>=18.7", "autobahn>=0.18", "asgiref~=3.2"],
setup_requires=["pytest-runner"],
extras_require={
"tests": ["hypothesis==4.23", "pytest~=3.10", "pytest-asyncio~=0.8"]
@ -41,6 +41,7 @@ setup(
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Internet :: WWW/HTTP",
],
)

View File

@ -56,12 +56,16 @@ class DaphneTestCase(unittest.TestCase):
# Return scope, messages, response
return test_app.get_received() + (response,)
def run_daphne_raw(self, data, timeout=1):
def run_daphne_raw(self, data, *, responses=None, timeout=1):
"""
Runs daphne and sends it the given raw bytestring over a socket. Returns what it sends back.
Runs Daphne and sends it the given raw bytestring over a socket.
Accepts list of response messages the application will reply with.
Returns what Daphne sends back.
"""
assert isinstance(data, bytes)
with DaphneTestingInstance() as test_app:
if responses is not None:
test_app.add_send_messages(responses)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

View File

@ -169,3 +169,21 @@ class TestHTTPResponse(DaphneTestCase):
str(context.exception),
"Header value 'True' expected to be `bytes`, but got `<class 'bool'>`",
)
def test_headers_type_raw(self):
"""
Daphne returns a 500 error response if the application sends invalid
headers.
"""
response = self.run_daphne_raw(
b"GET / HTTP/1.0\r\n\r\n",
responses=[
{
"type": "http.response.start",
"status": 200,
"headers": [["foo", b"bar"]],
},
{"type": "http.response.body", "body": b""},
],
)
self.assertTrue(response.startswith(b"HTTP/1.0 500 Internal Server Error"))