diff --git a/Makefile b/Makefile index a5af7243..519c74ec 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ black: black --config ./.black.toml sanic tests fix-import: black - isort -rc sanic tests + isort sanic tests docs-clean: diff --git a/sanic/app.py b/sanic/app.py index 65e84809..d12dc59b 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -194,6 +194,12 @@ class Sanic: 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: @@ -205,14 +211,16 @@ class Sanic: 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 @@ -476,6 +484,13 @@ class Sanic: 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): @@ -516,13 +531,15 @@ class Sanic: 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 @@ -813,6 +830,14 @@ class Sanic: "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 @@ -845,7 +870,7 @@ class Sanic: 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: diff --git a/sanic/response.py b/sanic/response.py index 0b92d2bd..4a84cf47 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -65,6 +65,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): 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. @@ -202,16 +203,14 @@ class HTTPResponse(BaseHTTPResponse): 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( diff --git a/sanic/server.py b/sanic/server.py index 2e6be4a5..a61278ca 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -731,6 +731,26 @@ class AsyncioServer: 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) diff --git a/tests/performance/wheezy/simple_server.py b/tests/performance/wheezy/simple_server.py index 70a6338a..9928eb27 100644 --- a/tests/performance/wheezy/simple_server.py +++ b/tests/performance/wheezy/simple_server.py @@ -39,6 +39,7 @@ main = WSGIApplication( if __name__ == "__main__": import sys + from wsgiref.simple_server import make_server try: diff --git a/tests/test_app.py b/tests/test_app.py index 77171094..0def9207 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -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" ) @@ -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): diff --git a/tests/test_response.py b/tests/test_response.py index 87bda1bf..c6e16dd2 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -15,13 +15,13 @@ from aiofiles import os as async_os 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 @@ -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" @@ -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( @@ -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) diff --git a/tests/test_routes.py b/tests/test_routes.py index 3b24389f..c896f854 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -551,6 +551,35 @@ def test_route_duplicate(app): 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):