diff --git a/sanic/models/protocol_types.py b/sanic/models/protocol_types.py index 85d8343bdd..14bc275cbf 100644 --- a/sanic/models/protocol_types.py +++ b/sanic/models/protocol_types.py @@ -2,6 +2,8 @@ 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 @@ from typing import Protocol class TransportProtocol(Protocol): + scope: ASGIScope + def get_protocol(self): ... diff --git a/sanic/request.py b/sanic/request.py index 4b3033c982..3b0153f5ac 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -14,6 +14,7 @@ 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 @@ def url_for(self, view_name: str, **kwargs) -> str: 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 8de22df1d8..83e2f8e613 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 84d6380e80..4d7fb0aa13 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1051,7 +1051,6 @@ async def handler(request): 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 @@ async def handler(request): assert "test" not in request.form.keys() + @pytest.mark.asyncio async def test_post_form_urlencoded_drop_blanks_asgi(app): @app.route("/", methods=["POST"])