From c09129ec635c94bdd879e195d98b178b0c461ce3 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 25 Oct 2020 10:40:08 +0200 Subject: [PATCH 1/3] Resolve headers as body in ASGI mode --- sanic/asgi.py | 52 +++++++----------------------- sanic/response.py | 72 +++++++++++------------------------------- tests/test_response.py | 69 ++++++++++++++++------------------------ 3 files changed, 58 insertions(+), 135 deletions(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index f08cc45421..cf29a0cccc 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -1,6 +1,5 @@ import asyncio import warnings - from inspect import isawaitable from typing import ( Any, @@ -16,7 +15,6 @@ from urllib.parse import quote import sanic.app # noqa - from sanic.compat import Header from sanic.exceptions import InvalidUsage, ServerError from sanic.log import logger @@ -25,7 +23,6 @@ from sanic.server import StreamBuffer from sanic.websocket import WebSocketConnection - ASGIScope = MutableMapping[str, Any] ASGIMessage = MutableMapping[str, Any] ASGISend = Callable[[ASGIMessage], Awaitable[None]] @@ -68,9 +65,7 @@ async def drain(self) -> None: class MockTransport: _protocol: Optional[MockProtocol] - def __init__( - self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend - ) -> None: + def __init__(self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> None: self.scope = scope self._receive = receive self._send = send @@ -146,9 +141,7 @@ async def startup(self) -> None: ) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) for handler in listeners: - response = handler( - self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop - ) + response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) if isawaitable(response): await response @@ -166,9 +159,7 @@ async def shutdown(self) -> None: ) + self.asgi_app.sanic_app.listeners.get("after_server_stop", []) for handler in listeners: - response = handler( - self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop - ) + response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) if isawaitable(response): await response @@ -213,19 +204,13 @@ async def create( for key, value in scope.get("headers", []) ] ) - instance.do_stream = ( - True if headers.get("expect") == "100-continue" else False - ) + instance.do_stream = True if headers.get("expect") == "100-continue" else False instance.lifespan = Lifespan(instance) if scope["type"] == "lifespan": await instance.lifespan(scope, receive, send) else: - path = ( - scope["path"][1:] - if scope["path"].startswith("/") - else scope["path"] - ) + path = scope["path"][1:] if scope["path"].startswith("/") else scope["path"] url = "/".join([scope.get("root_path", ""), quote(path)]) url_bytes = url.encode("latin-1") url_bytes += b"?" + scope["query_string"] @@ -248,18 +233,11 @@ async def create( request_class = sanic_app.request_class or Request instance.request = request_class( - url_bytes, - headers, - version, - method, - instance.transport, - sanic_app, + url_bytes, headers, version, method, instance.transport, sanic_app, ) if sanic_app.is_request_stream: - is_stream_handler = sanic_app.router.is_stream_handler( - instance.request - ) + is_stream_handler = sanic_app.router.is_stream_handler(instance.request) if is_stream_handler: instance.request.stream = StreamBuffer( sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE @@ -313,6 +291,7 @@ async def stream_callback(self, response: HTTPResponse) -> None: """ Write the response. """ + response.asgi = True headers: List[Tuple[bytes, bytes]] = [] cookies: Dict[str, str] = {} try: @@ -338,9 +317,7 @@ async def stream_callback(self, response: HTTPResponse) -> None: type(response), ) exception = ServerError("Invalid response type") - response = self.sanic_app.error_handler.response( - self.request, exception - ) + response = self.sanic_app.error_handler.response(self.request, exception) headers = [ (str(name).encode("latin-1"), str(value).encode("latin-1")) for name, value in response.headers.items() @@ -350,14 +327,10 @@ async def stream_callback(self, response: HTTPResponse) -> None: if "content-length" not in response.headers and not isinstance( response, StreamingHTTPResponse ): - headers += [ - (b"content-length", str(len(response.body)).encode("latin-1")) - ] + headers += [(b"content-length", str(len(response.body)).encode("latin-1"))] if "content-type" not in response.headers: - headers += [ - (b"content-type", str(response.content_type).encode("latin-1")) - ] + headers += [(b"content-type", str(response.content_type).encode("latin-1"))] if response.cookies: cookies.update( @@ -369,8 +342,7 @@ async def stream_callback(self, response: HTTPResponse) -> None: ) headers += [ - (b"set-cookie", cookie.encode("utf-8")) - for k, cookie in cookies.items() + (b"set-cookie", cookie.encode("utf-8")) for k, cookie in cookies.items() ] await self.transport.send( diff --git a/sanic/response.py b/sanic/response.py index 4a84cf474c..2cb83987ad 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -10,7 +10,6 @@ from sanic.headers import format_http1 from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers - try: from ujson import dumps as json_dumps except ImportError: @@ -81,30 +80,25 @@ async def write(self, data): await self.protocol.push_data(data) await self.protocol.drain() - async def stream( - self, version="1.1", keep_alive=False, keep_alive_timeout=None - ): + async def stream(self, version="1.1", keep_alive=False, keep_alive_timeout=None): """Streams headers, runs the `streaming_fn` callback that writes content to the response body, then finalizes the response body. """ if version != "1.1": self.chunked = False headers = self.get_headers( - version, - keep_alive=keep_alive, - keep_alive_timeout=keep_alive_timeout, + version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout, ) - await self.protocol.push_data(headers) - await self.protocol.drain() + if not getattr(self, "asgi", False): + await self.protocol.push_data(headers) + await self.protocol.drain() await self.streaming_fn(self) if self.chunked: await self.protocol.push_data(b"0\r\n\r\n") # no need to await drain here after this write, because it is the # very last thing we write and nothing needs to wait for it. - def get_headers( - self, version="1.1", keep_alive=False, keep_alive_timeout=None - ): + def get_headers(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b"" @@ -138,12 +132,7 @@ class HTTPResponse(BaseHTTPResponse): __slots__ = ("body", "status", "content_type", "headers", "_cookies") def __init__( - self, - body=None, - status=200, - headers=None, - content_type=None, - body_bytes=b"", + self, body=None, status=200, headers=None, content_type=None, body_bytes=b"", ): self.content_type = content_type @@ -184,9 +173,7 @@ def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): else: status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") - return ( - b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b" - ) % ( + return (b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b") % ( version.encode(), self.status, status, @@ -237,9 +224,7 @@ def json( ) -def text( - body, status=200, headers=None, content_type="text/plain; charset=utf-8" -): +def text(body, status=200, headers=None, content_type="text/plain; charset=utf-8"): """ Returns response object with body in text format. @@ -248,14 +233,10 @@ def text( :param headers: Custom Headers. :param content_type: the content type (string) of the response """ - return HTTPResponse( - body, status=status, headers=headers, content_type=content_type - ) + return HTTPResponse(body, status=status, headers=headers, content_type=content_type) -def raw( - body, status=200, headers=None, content_type="application/octet-stream" -): +def raw(body, status=200, headers=None, content_type="application/octet-stream"): """ Returns response object without encoding the body. @@ -265,10 +246,7 @@ def raw( :param content_type: the content type (string) of the response. """ return HTTPResponse( - body_bytes=body, - status=status, - headers=headers, - content_type=content_type, + body_bytes=body, status=status, headers=headers, content_type=content_type, ) @@ -281,20 +259,12 @@ def html(body, status=200, headers=None): :param headers: Custom Headers. """ return HTTPResponse( - body, - status=status, - headers=headers, - content_type="text/html; charset=utf-8", + body, status=status, headers=headers, content_type="text/html; charset=utf-8", ) async def file( - location, - status=200, - mime_type=None, - headers=None, - filename=None, - _range=None, + location, status=200, mime_type=None, headers=None, filename=None, _range=None, ): """Return a response object with file data. @@ -326,10 +296,7 @@ async def file( mime_type = mime_type or guess_type(filename)[0] or "text/plain" return HTTPResponse( - status=status, - headers=headers, - content_type=mime_type, - body_bytes=out_stream, + status=status, headers=headers, content_type=mime_type, body_bytes=out_stream, ) @@ -437,9 +404,7 @@ async def streaming_fn(response): ) -def redirect( - to, headers=None, status=302, content_type="text/html; charset=utf-8" -): +def redirect(to, headers=None, status=302, content_type="text/html; charset=utf-8"): """Abort execution and cause a 302 redirect (by default). :param to: path or fully qualified URL to redirect to @@ -456,6 +421,5 @@ def redirect( # According to RFC 7231, a relative URI is now permitted. headers["Location"] = safe_to - return HTTPResponse( - status=status, headers=headers, content_type=content_type - ) + return HTTPResponse(status=status, headers=headers, content_type=content_type) + diff --git a/tests/test_response.py b/tests/test_response.py index c6e16dd29c..07bfc18a98 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,7 +1,6 @@ import asyncio import inspect import os - from collections import namedtuple from mimetypes import guess_type from random import choice @@ -9,7 +8,6 @@ from urllib.parse import unquote import pytest - from aiofiles import os as async_os from sanic.response import ( @@ -25,7 +23,6 @@ from sanic.server import HttpProtocol from sanic.testing import HOST, PORT - JSON_DATA = {"ok": True} @@ -103,14 +100,10 @@ async def response_without_space(request): ) _, response = app.test_client.get("/response_with_space") - content_length_for_response_with_space = response.headers.get( - "Content-Length" - ) + content_length_for_response_with_space = response.headers.get("Content-Length") _, response = app.test_client.get("/response_without_space") - content_length_for_response_without_space = response.headers.get( - "Content-Length" - ) + content_length_for_response_without_space = response.headers.get("Content-Length") assert ( content_length_for_response_with_space @@ -232,6 +225,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app): assert response.text == "foo,bar" +@pytest.mark.asyncio +async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): + request, response = await streaming_app.asgi_client.get("/") + assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n" + + def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): request, response = non_chunked_streaming_app.test_client.get("/") assert "Transfer-Encoding" not in response.headers @@ -239,9 +238,17 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): assert response.headers["Content-Length"] == "7" -def test_non_chunked_streaming_returns_correct_content( +@pytest.mark.asyncio +async def test_non_chunked_streaming_adds_correct_headers_asgi( non_chunked_streaming_app, ): + request, response = await non_chunked_streaming_app.asgi_client.get("/") + assert "Transfer-Encoding" not in response.headers + assert response.headers["Content-Type"] == "text/csv" + assert response.headers["Content-Length"] == "7" + + +def test_non_chunked_streaming_returns_correct_content(non_chunked_streaming_app,): request, response = non_chunked_streaming_app.test_client.get("/") assert response.text == "foo,bar" @@ -254,9 +261,7 @@ def test_stream_response_status_returns_correct_headers(status): @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) -def test_stream_response_keep_alive_returns_correct_headers( - keep_alive_timeout, -): +def test_stream_response_keep_alive_returns_correct_headers(keep_alive_timeout,): response = StreamingHTTPResponse(sample_streaming_fn) headers = response.get_headers( keep_alive=True, keep_alive_timeout=keep_alive_timeout @@ -340,13 +345,9 @@ async def mock_push_data(data): @streaming_app.listener("after_server_start") async def run_stream(app, loop): await response.stream(version="1.0") - assert response.protocol.transport.write.call_args_list[1][0][0] == ( - b"foo," - ) + assert response.protocol.transport.write.call_args_list[1][0][0] == (b"foo,") - assert response.protocol.transport.write.call_args_list[2][0][0] == ( - b"bar" - ) + assert response.protocol.transport.write.call_args_list[2][0][0] == (b"bar") assert len(response.protocol.transport.write.call_args_list) == 3 @@ -391,9 +392,7 @@ def get_file_content(static_file_directory, file_name): return file.read() -@pytest.mark.parametrize( - "file_name", ["test.file", "decode me.txt", "python.png"] -) +@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) @pytest.mark.parametrize("status", [200, 401]) def test_file_response(app, file_name, static_file_directory, status): @app.route("/files/", methods=["GET"]) @@ -420,9 +419,7 @@ def file_route(request, filename): ("python.png", "logo.png"), ], ) -def test_file_response_custom_filename( - app, source, dest, static_file_directory -): +def test_file_response_custom_filename(app, source, dest, static_file_directory): @app.route("/files/", methods=["GET"]) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -449,8 +446,7 @@ async def file_route(request, filename): headers["Content-Length"] = str(stats.st_size) if request.method == "HEAD": return HTTPResponse( - headers=headers, - content_type=guess_type(file_path)[0] or "text/plain", + headers=headers, content_type=guess_type(file_path)[0] or "text/plain", ) else: return file( @@ -468,9 +464,7 @@ async def file_route(request, filename): ) -@pytest.mark.parametrize( - "file_name", ["test.file", "decode me.txt", "python.png"] -) +@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) def test_file_stream_response(app, file_name, static_file_directory): @app.route("/files/", methods=["GET"]) def file_route(request, filename): @@ -496,9 +490,7 @@ def file_route(request, filename): ("python.png", "logo.png"), ], ) -def test_file_stream_response_custom_filename( - app, source, dest, static_file_directory -): +def test_file_stream_response_custom_filename(app, source, dest, static_file_directory): @app.route("/files/", methods=["GET"]) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -527,8 +519,7 @@ async def file_route(request, filename): stats = await async_os.stat(file_path) headers["Content-Length"] = str(stats.st_size) return HTTPResponse( - headers=headers, - content_type=guess_type(file_path)[0] or "text/plain", + headers=headers, content_type=guess_type(file_path)[0] or "text/plain", ) else: return file_stream( @@ -551,12 +542,8 @@ async def file_route(request, filename): ) -@pytest.mark.parametrize( - "file_name", ["test.file", "decode me.txt", "python.png"] -) -@pytest.mark.parametrize( - "size,start,end", [(1024, 0, 1024), (4096, 1024, 8192)] -) +@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) +@pytest.mark.parametrize("size,start,end", [(1024, 0, 1024), (4096, 1024, 8192)]) def test_file_stream_response_range( app, file_name, static_file_directory, size, start, end ): From eb3d0a3f87f7c1fe9d8d5c202951886601774ece Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 25 Oct 2020 10:45:22 +0200 Subject: [PATCH 2/3] squash --- sanic/asgi.py | 3 +- sanic/request.py | 10 ++--- sanic/response.py | 67 +++++++++++++++++++++++++------- sanic/router.py | 2 +- sanic/worker.py | 2 +- tests/test_keep_alive_timeout.py | 4 +- tests/test_response.py | 57 ++++++++++++++++++++------- 7 files changed, 106 insertions(+), 39 deletions(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index cf29a0cccc..00fc7f1269 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -291,7 +291,6 @@ async def stream_callback(self, response: HTTPResponse) -> None: """ Write the response. """ - response.asgi = True headers: List[Tuple[bytes, bytes]] = [] cookies: Dict[str, str] = {} try: @@ -324,6 +323,8 @@ async def stream_callback(self, response: HTTPResponse) -> None: if name not in (b"Set-Cookie",) ] + response.asgi = True + if "content-length" not in response.headers and not isinstance( response, StreamingHTTPResponse ): diff --git a/sanic/request.py b/sanic/request.py index 246eb351ef..3c765fa392 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -129,27 +129,27 @@ def __repr__(self): def get(self, key, default=None): """.. deprecated:: 19.9 - Custom context is now stored in `request.custom_context.yourkey`""" + Custom context is now stored in `request.custom_context.yourkey`""" return self.ctx.__dict__.get(key, default) def __contains__(self, key): """.. deprecated:: 19.9 - Custom context is now stored in `request.custom_context.yourkey`""" + Custom context is now stored in `request.custom_context.yourkey`""" return key in self.ctx.__dict__ def __getitem__(self, key): """.. deprecated:: 19.9 - Custom context is now stored in `request.custom_context.yourkey`""" + Custom context is now stored in `request.custom_context.yourkey`""" return self.ctx.__dict__[key] def __delitem__(self, key): """.. deprecated:: 19.9 - Custom context is now stored in `request.custom_context.yourkey`""" + Custom context is now stored in `request.custom_context.yourkey`""" del self.ctx.__dict__[key] def __setitem__(self, key, value): """.. deprecated:: 19.9 - Custom context is now stored in `request.custom_context.yourkey`""" + Custom context is now stored in `request.custom_context.yourkey`""" setattr(self.ctx, key, value) def body_init(self): diff --git a/sanic/response.py b/sanic/response.py index 2cb83987ad..03f7bea8b4 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -10,6 +10,7 @@ from sanic.headers import format_http1 from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers + try: from ujson import dumps as json_dumps except ImportError: @@ -80,14 +81,18 @@ async def write(self, data): await self.protocol.push_data(data) await self.protocol.drain() - async def stream(self, version="1.1", keep_alive=False, keep_alive_timeout=None): + async def stream( + self, version="1.1", keep_alive=False, keep_alive_timeout=None + ): """Streams headers, runs the `streaming_fn` callback that writes content to the response body, then finalizes the response body. """ if version != "1.1": self.chunked = False headers = self.get_headers( - version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout, + version, + keep_alive=keep_alive, + keep_alive_timeout=keep_alive_timeout, ) if not getattr(self, "asgi", False): await self.protocol.push_data(headers) @@ -98,7 +103,9 @@ async def stream(self, version="1.1", keep_alive=False, keep_alive_timeout=None) # no need to await drain here after this write, because it is the # very last thing we write and nothing needs to wait for it. - def get_headers(self, version="1.1", keep_alive=False, keep_alive_timeout=None): + def get_headers( + self, version="1.1", keep_alive=False, keep_alive_timeout=None + ): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b"" @@ -132,7 +139,12 @@ class HTTPResponse(BaseHTTPResponse): __slots__ = ("body", "status", "content_type", "headers", "_cookies") def __init__( - self, body=None, status=200, headers=None, content_type=None, body_bytes=b"", + self, + body=None, + status=200, + headers=None, + content_type=None, + body_bytes=b"", ): self.content_type = content_type @@ -173,7 +185,9 @@ def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): else: status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") - return (b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b") % ( + return ( + b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b" + ) % ( version.encode(), self.status, status, @@ -224,7 +238,9 @@ def json( ) -def text(body, status=200, headers=None, content_type="text/plain; charset=utf-8"): +def text( + body, status=200, headers=None, content_type="text/plain; charset=utf-8" +): """ Returns response object with body in text format. @@ -233,10 +249,14 @@ def text(body, status=200, headers=None, content_type="text/plain; charset=utf-8 :param headers: Custom Headers. :param content_type: the content type (string) of the response """ - return HTTPResponse(body, status=status, headers=headers, content_type=content_type) + return HTTPResponse( + body, status=status, headers=headers, content_type=content_type + ) -def raw(body, status=200, headers=None, content_type="application/octet-stream"): +def raw( + body, status=200, headers=None, content_type="application/octet-stream" +): """ Returns response object without encoding the body. @@ -246,7 +266,10 @@ def raw(body, status=200, headers=None, content_type="application/octet-stream") :param content_type: the content type (string) of the response. """ return HTTPResponse( - body_bytes=body, status=status, headers=headers, content_type=content_type, + body_bytes=body, + status=status, + headers=headers, + content_type=content_type, ) @@ -259,12 +282,20 @@ def html(body, status=200, headers=None): :param headers: Custom Headers. """ return HTTPResponse( - body, status=status, headers=headers, content_type="text/html; charset=utf-8", + body, + status=status, + headers=headers, + content_type="text/html; charset=utf-8", ) async def file( - location, status=200, mime_type=None, headers=None, filename=None, _range=None, + location, + status=200, + mime_type=None, + headers=None, + filename=None, + _range=None, ): """Return a response object with file data. @@ -296,7 +327,10 @@ async def file( mime_type = mime_type or guess_type(filename)[0] or "text/plain" return HTTPResponse( - status=status, headers=headers, content_type=mime_type, body_bytes=out_stream, + status=status, + headers=headers, + content_type=mime_type, + body_bytes=out_stream, ) @@ -404,7 +438,9 @@ async def streaming_fn(response): ) -def redirect(to, headers=None, status=302, content_type="text/html; charset=utf-8"): +def redirect( + to, headers=None, status=302, content_type="text/html; charset=utf-8" +): """Abort execution and cause a 302 redirect (by default). :param to: path or fully qualified URL to redirect to @@ -421,5 +457,6 @@ def redirect(to, headers=None, status=302, content_type="text/html; charset=utf- # According to RFC 7231, a relative URI is now permitted. headers["Location"] = safe_to - return HTTPResponse(status=status, headers=headers, content_type=content_type) - + return HTTPResponse( + status=status, headers=headers, content_type=content_type + ) diff --git a/sanic/router.py b/sanic/router.py index 2d8817a3b9..698589a57b 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -484,7 +484,7 @@ def _get(self, url, method, host): return route_handler, [], kwargs, route.uri, route.name def is_stream_handler(self, request): - """ Handler for request is stream or not. + """Handler for request is stream or not. :param request: Request object :return: bool """ diff --git a/sanic/worker.py b/sanic/worker.py index 777f12cfdb..d42662dcb6 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -174,7 +174,7 @@ async def _check_alive(self): @staticmethod def _create_ssl_context(cfg): - """ Creates SSLContext instance for usage in asyncio.create_server. + """Creates SSLContext instance for usage in asyncio.create_server. See ssl.SSLSocket.__init__ for more details. """ ctx = ssl.SSLContext(cfg.ssl_version) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index a59d6c5bbb..bec433be14 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -230,8 +230,8 @@ async def handler3(request): def test_keep_alive_timeout_reuse(): """If the server keep-alive timeout and client keep-alive timeout are - both longer than the delay, the client _and_ server will successfully - reuse the existing connection.""" + both longer than the delay, the client _and_ server will successfully + reuse the existing connection.""" try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) diff --git a/tests/test_response.py b/tests/test_response.py index 07bfc18a98..488a76e7b9 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,6 +1,7 @@ import asyncio import inspect import os + from collections import namedtuple from mimetypes import guess_type from random import choice @@ -8,6 +9,7 @@ from urllib.parse import unquote import pytest + from aiofiles import os as async_os from sanic.response import ( @@ -23,6 +25,7 @@ from sanic.server import HttpProtocol from sanic.testing import HOST, PORT + JSON_DATA = {"ok": True} @@ -100,10 +103,14 @@ async def response_without_space(request): ) _, response = app.test_client.get("/response_with_space") - content_length_for_response_with_space = response.headers.get("Content-Length") + content_length_for_response_with_space = response.headers.get( + "Content-Length" + ) _, response = app.test_client.get("/response_without_space") - content_length_for_response_without_space = response.headers.get("Content-Length") + content_length_for_response_without_space = response.headers.get( + "Content-Length" + ) assert ( content_length_for_response_with_space @@ -248,7 +255,9 @@ async def test_non_chunked_streaming_adds_correct_headers_asgi( assert response.headers["Content-Length"] == "7" -def test_non_chunked_streaming_returns_correct_content(non_chunked_streaming_app,): +def test_non_chunked_streaming_returns_correct_content( + non_chunked_streaming_app, +): request, response = non_chunked_streaming_app.test_client.get("/") assert response.text == "foo,bar" @@ -261,7 +270,9 @@ def test_stream_response_status_returns_correct_headers(status): @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) -def test_stream_response_keep_alive_returns_correct_headers(keep_alive_timeout,): +def test_stream_response_keep_alive_returns_correct_headers( + keep_alive_timeout, +): response = StreamingHTTPResponse(sample_streaming_fn) headers = response.get_headers( keep_alive=True, keep_alive_timeout=keep_alive_timeout @@ -345,9 +356,13 @@ async def mock_push_data(data): @streaming_app.listener("after_server_start") async def run_stream(app, loop): await response.stream(version="1.0") - assert response.protocol.transport.write.call_args_list[1][0][0] == (b"foo,") + assert response.protocol.transport.write.call_args_list[1][0][0] == ( + b"foo," + ) - assert response.protocol.transport.write.call_args_list[2][0][0] == (b"bar") + assert response.protocol.transport.write.call_args_list[2][0][0] == ( + b"bar" + ) assert len(response.protocol.transport.write.call_args_list) == 3 @@ -392,7 +407,9 @@ def get_file_content(static_file_directory, file_name): return file.read() -@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) +@pytest.mark.parametrize( + "file_name", ["test.file", "decode me.txt", "python.png"] +) @pytest.mark.parametrize("status", [200, 401]) def test_file_response(app, file_name, static_file_directory, status): @app.route("/files/", methods=["GET"]) @@ -419,7 +436,9 @@ def file_route(request, filename): ("python.png", "logo.png"), ], ) -def test_file_response_custom_filename(app, source, dest, static_file_directory): +def test_file_response_custom_filename( + app, source, dest, static_file_directory +): @app.route("/files/", methods=["GET"]) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -446,7 +465,8 @@ async def file_route(request, filename): headers["Content-Length"] = str(stats.st_size) if request.method == "HEAD": return HTTPResponse( - headers=headers, content_type=guess_type(file_path)[0] or "text/plain", + headers=headers, + content_type=guess_type(file_path)[0] or "text/plain", ) else: return file( @@ -464,7 +484,9 @@ async def file_route(request, filename): ) -@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) +@pytest.mark.parametrize( + "file_name", ["test.file", "decode me.txt", "python.png"] +) def test_file_stream_response(app, file_name, static_file_directory): @app.route("/files/", methods=["GET"]) def file_route(request, filename): @@ -490,7 +512,9 @@ def file_route(request, filename): ("python.png", "logo.png"), ], ) -def test_file_stream_response_custom_filename(app, source, dest, static_file_directory): +def test_file_stream_response_custom_filename( + app, source, dest, static_file_directory +): @app.route("/files/", methods=["GET"]) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -519,7 +543,8 @@ async def file_route(request, filename): stats = await async_os.stat(file_path) headers["Content-Length"] = str(stats.st_size) return HTTPResponse( - headers=headers, content_type=guess_type(file_path)[0] or "text/plain", + headers=headers, + content_type=guess_type(file_path)[0] or "text/plain", ) else: return file_stream( @@ -542,8 +567,12 @@ async def file_route(request, filename): ) -@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"]) -@pytest.mark.parametrize("size,start,end", [(1024, 0, 1024), (4096, 1024, 8192)]) +@pytest.mark.parametrize( + "file_name", ["test.file", "decode me.txt", "python.png"] +) +@pytest.mark.parametrize( + "size,start,end", [(1024, 0, 1024), (4096, 1024, 8192)] +) def test_file_stream_response_range( app, file_name, static_file_directory, size, start, end ): From 8c628c69fbf8579335de8e6591cf9044f2e58b26 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 15 Feb 2021 14:23:30 +0200 Subject: [PATCH 3/3] fix uvloop version --- setup.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index e762a088c8..43952819d9 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import os import re import sys + from distutils.util import strtobool from setuptools import setup @@ -24,6 +25,7 @@ def initialize_options(self): def run_tests(self): import shlex + import pytest errno = pytest.main(shlex.split(self.pytest_args)) @@ -38,7 +40,9 @@ def open_local(paths, mode="r", encoding="utf8"): with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: try: - version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] + version = re.findall( + r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M + )[0] except IndexError: raise RuntimeError("Unable to determine version.") @@ -68,9 +72,11 @@ def open_local(paths, mode="r", encoding="utf8"): ], } -env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' +env_dependency = ( + '; sys_platform != "win32" ' 'and implementation_name == "cpython"' +) ujson = "ujson>=1.35" + env_dependency -uvloop = "uvloop>=0.5.3" + env_dependency +uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency requirements = [ "httptools>=0.0.10",