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:
parent
bb9ff7cec1
commit
2a44a27236
2
Makefile
2
Makefile
|
@ -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:
|
||||||
|
|
57
sanic/app.py
57
sanic/app.py
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user