From eb3d0a3f87f7c1fe9d8d5c202951886601774ece Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 25 Oct 2020 10:45:22 +0200 Subject: [PATCH] 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 cf29a0cc..00fc7f12 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -291,7 +291,6 @@ class ASGIApp: """ Write the response. """ - response.asgi = True headers: List[Tuple[bytes, bytes]] = [] cookies: Dict[str, str] = {} try: @@ -324,6 +323,8 @@ class ASGIApp: 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 246eb351..3c765fa3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -129,27 +129,27 @@ class Request: 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 2cb83987..03f7bea8 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -10,6 +10,7 @@ from sanic.cookies import CookieJar 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 @@ class StreamingHTTPResponse(BaseHTTPResponse): 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 @@ class StreamingHTTPResponse(BaseHTTPResponse): # 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 @@ class HTTPResponse(BaseHTTPResponse): 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 @@ def stream( ) -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 2d8817a3..698589a5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -484,7 +484,7 @@ class Router: 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 777f12cf..d42662dc 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -174,7 +174,7 @@ class GunicornWorker(base.Worker): @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 a59d6c5b..bec433be 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 07bfc18a..488a76e7 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 unittest.mock import MagicMock from urllib.parse import unquote import pytest + from aiofiles import os as async_os from sanic.response import ( @@ -23,6 +25,7 @@ from sanic.response import ( from sanic.server import HttpProtocol from sanic.testing import HOST, PORT + JSON_DATA = {"ok": True} @@ -100,10 +103,14 @@ def test_response_content_length(app): ) _, 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 @@ def test_stream_response_writes_correct_content_to_transport_when_not_chunked( @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 test_file_response(app, file_name, static_file_directory, status): ("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 @@ def test_file_head_response(app, file_name, static_file_directory): 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 @@ def test_file_head_response(app, file_name, static_file_directory): ) -@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 test_file_stream_response(app, file_name, static_file_directory): ("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 @@ def test_file_stream_head_response(app, file_name, static_file_directory): 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 @@ def test_file_stream_head_response(app, file_name, static_file_directory): ) -@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 ):