Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport to 1912 #1900

Merged
merged 3 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests

fix-import: black
isort -rc sanic tests
isort sanic tests


docs-clean:
Expand Down
57 changes: 41 additions & 16 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ def route(
strict_slashes = self.strict_slashes

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())

if not args:
Expand All @@ -205,14 +211,16 @@ def response(handler):
if stream:
handler.is_stream = stream

routes = self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
routes.extend(
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
)
return routes, handler

Expand Down Expand Up @@ -476,6 +484,13 @@ def websocket(
strict_slashes = self.strict_slashes

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):
request.app = self
if not getattr(handler, "__blueprintname__", False):
Expand Down Expand Up @@ -516,13 +531,15 @@ async def websocket_handler(request, *args, **kwargs):
self.websocket_tasks.remove(fut)
await ws.close()

routes = self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
routes.extend(
self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
)
)
return routes, handler

Expand Down Expand Up @@ -813,6 +830,14 @@ def url_for(self, view_name: str, **kwargs):
"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"):
filename = kwargs.pop("filename", None)
# it's static folder
Expand Down Expand Up @@ -845,7 +870,7 @@ def url_for(self, view_name: str, **kwargs):

netloc = kwargs.pop("_server", None)
if netloc is None and external:
netloc = self.config.get("SERVER_NAME", "")
netloc = host or self.config.get("SERVER_NAME", "")

if external:
if not scheme:
Expand Down
7 changes: 3 additions & 4 deletions sanic/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(
self.headers = Header(headers or {})
self.chunked = chunked
self._cookies = None
self.protocol = None

async def write(self, data):
"""Writes a chunk of data to the streaming response.
Expand Down Expand Up @@ -202,16 +203,14 @@ def cookies(self):
return self._cookies


def empty(
status=204, headers=None,
):
def empty(status=204, headers=None):
"""
Returns an empty response to the client.

:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body_bytes=b"", status=status, headers=headers,)
return HTTPResponse(body_bytes=b"", status=status, headers=headers)


def json(
Expand Down
20 changes: 20 additions & 0 deletions sanic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,26 @@ def close(self):
task = asyncio.ensure_future(coro, loop=self.loop)
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):
"""Starts the asyncio server, returns AsyncServerCoro"""
task = asyncio.ensure_future(self.serve_coro)
Expand Down
1 change: 1 addition & 0 deletions tests/performance/wheezy/simple_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def get(self):

if __name__ == "__main__":
import sys

from wsgiref.simple_server import make_server

try:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ def test_create_asyncio_server(app):
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(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
Expand All @@ -53,6 +67,10 @@ def test_asyncio_server_start_serving(app):
)
srv = loop.run_until_complete(asyncio_srv_coro)
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):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
from sanic.response import (
HTTPResponse,
StreamingHTTPResponse,
empty,
file,
file_stream,
json,
raw,
stream,
)
from sanic.response import empty
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT

Expand Down Expand Up @@ -240,7 +240,7 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):


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("/")
assert response.text == "foo,bar"
Expand All @@ -255,7 +255,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
keep_alive_timeout,
):
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers(
Expand Down Expand Up @@ -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(
streaming_app
streaming_app,
):
response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,35 @@ async def handler4(request, dynamic):
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):
@app.route("/test", methods=["GET"])
async def handler(request):
Expand Down