Backport to 1912 (#1900)

* Cherry pick PRs to backport to 19.12LTS

Includes commits from:
https://github.com/huge-success/sanic/pull/1762
https://github.com/huge-success/sanic/pull/1764
https://github.com/huge-success/sanic/pull/1789

* Fix type annotation issue; run black and isort

* Update Makefile

Co-authored-by: Ashley Sommer <ashleysommer@gmail.com>
This commit is contained in:
Adam Hopkins 2020-07-29 13:54:33 +03:00 committed by GitHub
parent bb9ff7cec1
commit 2a44a27236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 25 deletions

View File

@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests black --config ./.black.toml sanic tests
fix-import: black fix-import: black
isort -rc sanic tests isort sanic tests
docs-clean: docs-clean:

View File

@ -194,6 +194,12 @@ class Sanic:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
def response(handler): def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
args = list(signature(handler).parameters.keys()) args = list(signature(handler).parameters.keys())
if not args: if not args:
@ -205,14 +211,16 @@ class Sanic:
if stream: if stream:
handler.is_stream = stream handler.is_stream = stream
routes = self.router.add( routes.extend(
uri=uri, self.router.add(
methods=methods, uri=uri,
handler=handler, methods=methods,
host=host, handler=handler,
strict_slashes=strict_slashes, host=host,
version=version, strict_slashes=strict_slashes,
name=name, version=version,
name=name,
)
) )
return routes, handler return routes, handler
@ -476,6 +484,13 @@ class Sanic:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
def response(handler): def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
async def websocket_handler(request, *args, **kwargs): async def websocket_handler(request, *args, **kwargs):
request.app = self request.app = self
if not getattr(handler, "__blueprintname__", False): if not getattr(handler, "__blueprintname__", False):
@ -516,13 +531,15 @@ class Sanic:
self.websocket_tasks.remove(fut) self.websocket_tasks.remove(fut)
await ws.close() await ws.close()
routes = self.router.add( routes.extend(
uri=uri, self.router.add(
handler=websocket_handler, uri=uri,
methods=frozenset({"GET"}), handler=websocket_handler,
host=host, methods=frozenset({"GET"}),
strict_slashes=strict_slashes, host=host,
name=name, strict_slashes=strict_slashes,
name=name,
)
) )
return routes, handler return routes, handler
@ -813,6 +830,14 @@ class Sanic:
"Endpoint with name `{}` was not found".format(view_name) "Endpoint with name `{}` was not found".format(view_name)
) )
# If the route has host defined, split that off
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
if view_name == "static" or view_name.endswith(".static"): if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None) filename = kwargs.pop("filename", None)
# it's static folder # it's static folder
@ -845,7 +870,7 @@ class Sanic:
netloc = kwargs.pop("_server", None) netloc = kwargs.pop("_server", None)
if netloc is None and external: if netloc is None and external:
netloc = self.config.get("SERVER_NAME", "") netloc = host or self.config.get("SERVER_NAME", "")
if external: if external:
if not scheme: if not scheme:

View File

@ -65,6 +65,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self.chunked = chunked self.chunked = chunked
self._cookies = None self._cookies = None
self.protocol = None
async def write(self, data): async def write(self, data):
"""Writes a chunk of data to the streaming response. """Writes a chunk of data to the streaming response.
@ -202,16 +203,14 @@ class HTTPResponse(BaseHTTPResponse):
return self._cookies return self._cookies
def empty( def empty(status=204, headers=None):
status=204, headers=None,
):
""" """
Returns an empty response to the client. Returns an empty response to the client.
:param status Response code. :param status Response code.
:param headers Custom Headers. :param headers Custom Headers.
""" """
return HTTPResponse(body_bytes=b"", status=status, headers=headers,) return HTTPResponse(body_bytes=b"", status=status, headers=headers)
def json( def json(

View File

@ -731,6 +731,26 @@ class AsyncioServer:
task = asyncio.ensure_future(coro, loop=self.loop) task = asyncio.ensure_future(coro, loop=self.loop)
return task return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self):
if self.server:
try:
return self.server.serve_forever()
except AttributeError:
raise NotImplementedError(
"server.serve_forever not available in this version "
"of asyncio or uvloop."
)
def __await__(self): def __await__(self):
"""Starts the asyncio server, returns AsyncServerCoro""" """Starts the asyncio server, returns AsyncServerCoro"""
task = asyncio.ensure_future(self.serve_coro) task = asyncio.ensure_future(self.serve_coro)

View File

@ -39,6 +39,7 @@ main = WSGIApplication(
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
try: try:

View File

@ -41,6 +41,20 @@ def test_create_asyncio_server(app):
assert srv.is_serving() is True assert srv.is_serving() is True
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
@pytest.mark.skipif( @pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher" sys.version_info < (3, 7), reason="requires python3.7 or higher"
) )
@ -53,6 +67,10 @@ def test_asyncio_server_start_serving(app):
) )
srv = loop.run_until_complete(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
srv.close()
# Looks like we can't easily test `serve_forever()`
def test_app_loop_not_running(app): def test_app_loop_not_running(app):

View File

@ -15,13 +15,13 @@ from aiofiles import os as async_os
from sanic.response import ( from sanic.response import (
HTTPResponse, HTTPResponse,
StreamingHTTPResponse, StreamingHTTPResponse,
empty,
file, file,
file_stream, file_stream,
json, json,
raw, raw,
stream, stream,
) )
from sanic.response import empty
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT from sanic.testing import HOST, PORT
@ -240,7 +240,7 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
def test_non_chunked_streaming_returns_correct_content( def test_non_chunked_streaming_returns_correct_content(
non_chunked_streaming_app non_chunked_streaming_app,
): ):
request, response = non_chunked_streaming_app.test_client.get("/") request, response = non_chunked_streaming_app.test_client.get("/")
assert response.text == "foo,bar" assert response.text == "foo,bar"
@ -255,7 +255,7 @@ def test_stream_response_status_returns_correct_headers(status):
@pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30])
def test_stream_response_keep_alive_returns_correct_headers( def test_stream_response_keep_alive_returns_correct_headers(
keep_alive_timeout keep_alive_timeout,
): ):
response = StreamingHTTPResponse(sample_streaming_fn) response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers( headers = response.get_headers(
@ -284,7 +284,7 @@ def test_stream_response_does_not_include_chunked_header_if_disabled():
def test_stream_response_writes_correct_content_to_transport_when_chunked( def test_stream_response_writes_correct_content_to_transport_when_chunked(
streaming_app streaming_app,
): ):
response = StreamingHTTPResponse(sample_streaming_fn) response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol) response.protocol = MagicMock(HttpProtocol)

View File

@ -551,6 +551,35 @@ def test_route_duplicate(app):
pass pass
def test_double_stack_route(app):
@app.route("/test/1")
@app.route("/test/2")
async def handler1(request):
return text("OK")
request, response = app.test_client.get("/test/1")
assert response.status == 200
request, response = app.test_client.get("/test/2")
assert response.status == 200
@pytest.mark.asyncio
async def test_websocket_route_asgi(app):
ev = asyncio.Event()
@app.websocket("/test/1")
@app.websocket("/test/2")
async def handler(request, ws):
ev.set()
request, response = await app.asgi_client.websocket("/test/1")
first_set = ev.is_set()
ev.clear()
request, response = await app.asgi_client.websocket("/test/1")
second_set = ev.is_set()
assert first_set and second_set
def test_method_not_allowed(app): def test_method_not_allowed(app):
@app.route("/test", methods=["GET"]) @app.route("/test", methods=["GET"])
async def handler(request): async def handler(request):