Prepare for release

This commit is contained in:
Adam Hopkins 2020-11-05 09:34:02 +02:00
commit 9b24fbb2f3
5 changed files with 64 additions and 17 deletions

View File

@ -1 +1 @@
__version__ = "19.12.3" __version__ = "19.12.4"

View File

@ -1474,3 +1474,5 @@ class Sanic:
self.asgi = True self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app() await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import warnings import warnings
from inspect import isawaitable from inspect import isawaitable
from typing import ( from typing import (
Any, Any,
@ -15,6 +16,7 @@ from typing import (
from urllib.parse import quote from urllib.parse import quote
import sanic.app # noqa import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import InvalidUsage, ServerError from sanic.exceptions import InvalidUsage, ServerError
from sanic.log import logger from sanic.log import logger
@ -23,6 +25,7 @@ from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.server import StreamBuffer from sanic.server import StreamBuffer
from sanic.websocket import WebSocketConnection from sanic.websocket import WebSocketConnection
ASGIScope = MutableMapping[str, Any] ASGIScope = MutableMapping[str, Any]
ASGIMessage = MutableMapping[str, Any] ASGIMessage = MutableMapping[str, Any]
ASGISend = Callable[[ASGIMessage], Awaitable[None]] ASGISend = Callable[[ASGIMessage], Awaitable[None]]
@ -65,7 +68,9 @@ class MockProtocol:
class MockTransport: class MockTransport:
_protocol: Optional[MockProtocol] _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.scope = scope
self._receive = receive self._receive = receive
self._send = send self._send = send
@ -141,7 +146,9 @@ class Lifespan:
) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) ) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
for handler in listeners: 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): if isawaitable(response):
await response await response
@ -159,7 +166,9 @@ class Lifespan:
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", []) ) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
for handler in listeners: 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): if isawaitable(response):
await response await response
@ -204,13 +213,19 @@ class ASGIApp:
for key, value in scope.get("headers", []) 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) instance.lifespan = Lifespan(instance)
if scope["type"] == "lifespan": if scope["type"] == "lifespan":
await instance.lifespan(scope, receive, send) await instance.lifespan(scope, receive, send)
else: 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 = "/".join([scope.get("root_path", ""), quote(path)])
url_bytes = url.encode("latin-1") url_bytes = url.encode("latin-1")
url_bytes += b"?" + scope["query_string"] url_bytes += b"?" + scope["query_string"]
@ -233,11 +248,18 @@ class ASGIApp:
request_class = sanic_app.request_class or Request request_class = sanic_app.request_class or Request
instance.request = request_class( 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: 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: if is_stream_handler:
instance.request.stream = StreamBuffer( instance.request.stream = StreamBuffer(
sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE
@ -287,13 +309,17 @@ class ASGIApp:
callback = None if self.ws else self.stream_callback callback = None if self.ws else self.stream_callback
await handler(self.request, None, callback) await handler(self.request, None, callback)
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
async def stream_callback(self, response: HTTPResponse) -> None: async def stream_callback(self, response: HTTPResponse) -> None:
""" """
Write the response. Write the response.
""" """
headers: List[Tuple[bytes, bytes]] = [] headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {} cookies: Dict[str, str] = {}
content_length: List[str] = []
try: try:
content_length = response.headers.popall("content-length", [])
cookies = { cookies = {
v.key: v v.key: v
for _, v in list( for _, v in list(
@ -316,7 +342,9 @@ class ASGIApp:
type(response), type(response),
) )
exception = ServerError("Invalid response type") 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 = [ headers = [
(str(name).encode("latin-1"), str(value).encode("latin-1")) (str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items() for name, value in response.headers.items()
@ -324,14 +352,28 @@ class ASGIApp:
] ]
response.asgi = True response.asgi = True
is_streaming = isinstance(response, StreamingHTTPResponse)
if "content-length" not in response.headers and not isinstance( if is_streaming and getattr(response, "chunked", False):
response, StreamingHTTPResponse # disable sanic chunking, this is done at the ASGI-server level
): setattr(response, "chunked", False)
headers += [(b"content-length", str(len(response.body)).encode("latin-1"))] # content-length header is removed to signal to the ASGI-server
# to use automatic-chunking if it supports it
elif len(content_length) > 0:
headers += [
(b"content-length", str(content_length[0]).encode("latin-1"))
]
elif not is_streaming:
headers += [
(
b"content-length",
str(len(getattr(response, "body", b""))).encode("latin-1"),
)
]
if "content-type" not in response.headers: 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: if response.cookies:
cookies.update( cookies.update(
@ -343,7 +385,8 @@ class ASGIApp:
) )
headers += [ 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( await self.transport.send(

View File

@ -80,6 +80,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes: if type(data) != bytes:
data = self._encode_body(data) data = self._encode_body(data)
# `chunked` will always be False in ASGI-mode, even if the underlying
# ASGI Transport implements Chunked transport. That does it itself.
if self.chunked: if self.chunked:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
else: else:

View File

@ -235,7 +235,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
request, response = await streaming_app.asgi_client.get("/") request, response = await streaming_app.asgi_client.get("/")
assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n" assert response.text == "foo,bar"
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):