diff --git a/.gitignore b/.gitignore index 830060b..b3cff06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ *.egg-info *.pyc __pycache__ diff --git a/.travis.yml b/.travis.yml index d87df46..bf7be68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.txt b/CHANGELOG.txt index af0dc4b..d096da4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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) ------------------ diff --git a/daphne/__init__.py b/daphne/__init__.py index 2e6c576..0adc07a 100755 --- a/daphne/__init__.py +++ b/daphne/__init__.py @@ -1 +1 @@ -__version__ = "2.3.0a1" +__version__ = "2.4.1a1" diff --git a/daphne/server.py b/daphne/server.py index de04b8a..5ede808 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -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 - self.check_headers_type(message) + 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) diff --git a/daphne/testing.py b/daphne/testing.py index f5f3724..0da03d0 100644 --- a/daphne/testing.py +++ b/daphne/testing.py @@ -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) diff --git a/setup.py b/setup.py index b6cfdc8..357e294 100755 --- a/setup.py +++ b/setup.py @@ -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", ], ) diff --git a/tests/http_base.py b/tests/http_base.py index 71d9618..f3a8340 100644 --- a/tests/http_base.py +++ b/tests/http_base.py @@ -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) diff --git a/tests/test_http_response.py b/tests/test_http_response.py index afb8e39..08c34bb 100644 --- a/tests/test_http_response.py +++ b/tests/test_http_response.py @@ -169,3 +169,21 @@ class TestHTTPResponse(DaphneTestCase): str(context.exception), "Header value 'True' expected to be `bytes`, but got ``", ) + + 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"))