From 5d683c6ea4b615e80c51d80189436437b824cce6 Mon Sep 17 00:00:00 2001 From: Michael Azimov Date: Tue, 26 Apr 2022 17:25:29 +0300 Subject: [PATCH] Expose scope parameter in request object (#2432) Co-authored-by: Adam Hopkins --- sanic/models/protocol_types.py | 4 ++++ sanic/request.py | 16 ++++++++++++++++ tests/test_request.py | 26 ++++++++++++++++++++++++++ tests/test_requests.py | 2 +- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/sanic/models/protocol_types.py b/sanic/models/protocol_types.py index 85d8343b..14bc275c 100644 --- a/sanic/models/protocol_types.py +++ b/sanic/models/protocol_types.py @@ -2,6 +2,8 @@ import sys from typing import Any, AnyStr, TypeVar, Union +from sanic.models.asgi import ASGIScope + if sys.version_info < (3, 8): from asyncio import BaseTransport @@ -17,6 +19,8 @@ else: from typing import Protocol class TransportProtocol(Protocol): + scope: ASGIScope + def get_protocol(self): ... diff --git a/sanic/request.py b/sanic/request.py index 4b3033c9..3b0153f5 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -14,6 +14,7 @@ from typing import ( from sanic_routing.route import Route # type: ignore +from sanic.models.asgi import ASGIScope from sanic.models.http_types import Credentials @@ -831,6 +832,21 @@ class Request: view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs ) + @property + def scope(self) -> ASGIScope: + """ + :return: The ASGI scope of the request. + If the app isn't an ASGI app, then raises an exception. + :rtype: Optional[ASGIScope] + """ + if not self.app.asgi: + raise NotImplementedError( + "App isn't running in ASGI mode. " + "Scope is only available for ASGI apps." + ) + + return self.transport.scope + class File(NamedTuple): """ diff --git a/tests/test_request.py b/tests/test_request.py index 8de22df1..83e2f8e6 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -191,3 +191,29 @@ def test_bad_url_parse(): Mock(), Mock(), ) + + +def test_request_scope_raises_exception_when_no_asgi(): + app = Sanic("no_asgi") + + @app.get("/") + async def get(request): + return request.scope + + request, response = app.test_client.get("/") + assert response.status == 500 + with pytest.raises(NotImplementedError): + _ = request.scope + + +@pytest.mark.asyncio +async def test_request_scope_is_not_none_when_running_in_asgi(app): + @app.get("/") + async def get(request): + return response.empty() + + request, _ = await app.asgi_client.get("/") + + assert request.scope is not None + assert request.scope["method"].lower() == "get" + assert request.scope["path"].lower() == "/" diff --git a/tests/test_requests.py b/tests/test_requests.py index 84d6380e..4d7fb0aa 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1051,7 +1051,6 @@ async def test_post_form_urlencoded_keep_blanks_asgi(app): assert request.form.get("test") == "" # For request.parsed_form - def test_post_form_urlencoded_drop_blanks(app): @app.route("/", methods=["POST"]) async def handler(request): @@ -1066,6 +1065,7 @@ def test_post_form_urlencoded_drop_blanks(app): assert "test" not in request.form.keys() + @pytest.mark.asyncio async def test_post_form_urlencoded_drop_blanks_asgi(app): @app.route("/", methods=["POST"])