From 42d66e0daf217f1f5aa839f5b0fdd6077ee53886 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Thu, 14 Apr 2022 17:01:46 +0300 Subject: [PATCH 01/12] Added scope property to request --- sanic/request.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 5f0de3625f..4dd3f5357f 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 @@ -819,6 +820,15 @@ def url_for(self, view_name: str, **kwargs) -> str: view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs ) + @property + def scope(self) -> Optional[ASGIScope]: + """ + :return: the URL + :rtype: str + """ + + return self.app._asgi_app.transport.scope if self.app._asgi_app else None + class File(NamedTuple): """ From 396cc1523bede0bb77ab5e06878ab2e1760e3278 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Thu, 14 Apr 2022 19:09:13 +0300 Subject: [PATCH 02/12] added test and changed if --- sanic/request.py | 2 +- tests/test_request.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4dd3f5357f..62c4bd1c59 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -827,7 +827,7 @@ def scope(self) -> Optional[ASGIScope]: :rtype: str """ - return self.app._asgi_app.transport.scope if self.app._asgi_app else None + return self.app._asgi_app.transport.scope if self.app.asgi else None class File(NamedTuple): diff --git a/tests/test_request.py b/tests/test_request.py index 8de22df1d8..e9808d631c 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,9 +1,11 @@ +import logging from unittest.mock import Mock from uuid import UUID, uuid4 import pytest +import uvicorn -from sanic import Sanic, response +from sanic import Sanic, response, text from sanic.exceptions import BadURL from sanic.request import Request, uuid from sanic.server import HttpProtocol @@ -191,3 +193,15 @@ def test_bad_url_parse(): Mock(), Mock(), ) + +def test_request_scope_is_none_when_no_asgi(): + app = Sanic("no_asgi") + + @app.get("/") + async def get(request): + return response.empty() + + request, _ = app.test_client.get( + "/" + ) + assert request.scope == None \ No newline at end of file From b31d3a1fbd0bf287af3661bf7fe827101733eada Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 18 Apr 2022 14:58:23 +0300 Subject: [PATCH 03/12] set scope outside request --- sanic/asgi.py | 1 + sanic/request.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index 2614016866..19f732887d 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -162,6 +162,7 @@ async def create( instance.request.stream = instance instance.request_body = True instance.request.conn_info = ConnInfo(instance.transport) + instance.request._scope = scope return instance diff --git a/sanic/request.py b/sanic/request.py index 62c4bd1c59..e62ed9418c 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -92,6 +92,7 @@ class Request: "_protocol", "_remote_addr", "_socket", + "_scope", "_match_info", "_name", "app", @@ -168,6 +169,7 @@ def __init__( self.stream: Optional[Http] = None self.route: Optional[Route] = None self._protocol = None + self._scope: Optional[ASGIScope] = None self.responded: bool = False def __repr__(self): @@ -827,7 +829,7 @@ def scope(self) -> Optional[ASGIScope]: :rtype: str """ - return self.app._asgi_app.transport.scope if self.app.asgi else None + return self._scope class File(NamedTuple): From 1c4e6a2d65fc72edcf28f8b98779c6e5636d5091 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 18 Apr 2022 17:21:59 +0300 Subject: [PATCH 04/12] get scope from protocol --- sanic/asgi.py | 1 - sanic/request.py | 4 +-- tests/test_request.py | 81 ++++++++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index 19f732887d..2614016866 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -162,7 +162,6 @@ async def create( instance.request.stream = instance instance.request_body = True instance.request.conn_info = ConnInfo(instance.transport) - instance.request._scope = scope return instance diff --git a/sanic/request.py b/sanic/request.py index e62ed9418c..5987335287 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -92,7 +92,6 @@ class Request: "_protocol", "_remote_addr", "_socket", - "_scope", "_match_info", "_name", "app", @@ -169,7 +168,6 @@ def __init__( self.stream: Optional[Http] = None self.route: Optional[Route] = None self._protocol = None - self._scope: Optional[ASGIScope] = None self.responded: bool = False def __repr__(self): @@ -829,7 +827,7 @@ def scope(self) -> Optional[ASGIScope]: :rtype: str """ - return self._scope + return self.transport.scope if self.app.asgi else None class File(NamedTuple): diff --git a/tests/test_request.py b/tests/test_request.py index e9808d631c..3913801815 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,11 +1,9 @@ -import logging from unittest.mock import Mock from uuid import UUID, uuid4 import pytest -import uvicorn -from sanic import Sanic, response, text +from sanic import Sanic, response from sanic.exceptions import BadURL from sanic.request import Request, uuid from sanic.server import HttpProtocol @@ -62,12 +60,12 @@ def test_name_from_set(): @pytest.mark.parametrize( - "request_id,expected_type", - ( - (99, int), - (uuid4(), UUID), - ("foo", str), - ), + "request_id,expected_type", + ( + (99, int), + (uuid4(), UUID), + ("foo", str), + ), ) def test_request_id(request_id, expected_type): app = Sanic("req-generator") @@ -77,7 +75,7 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", headers={"X-REQUEST-ID": f"{request_id}"} + "/", headers={"X-REQUEST-ID": f"{request_id}"} ) assert request.id == request_id assert type(request.id) == expected_type @@ -98,7 +96,7 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", headers={"SOME-OTHER-REQUEST-ID": f"{REQUEST_ID}"} + "/", headers={"SOME-OTHER-REQUEST-ID": f"{REQUEST_ID}"} ) assert request.id == REQUEST_ID * 2 @@ -131,10 +129,10 @@ def test_ipv6_address_is_not_wrapped(app): @app.get("/") async def get(request): return response.json( - { - "client_ip": request.conn_info.client_ip, - "client": request.conn_info.client, - } + { + "client_ip": request.conn_info.client_ip, + "client": request.conn_info.client, + } ) request, resp = app.test_client.get("/", host="::1") @@ -153,10 +151,10 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", - headers={ - "Accept": "text/*, text/plain, text/plain;format=flowed, */*" - }, + "/", + headers={ + "Accept": "text/*, text/plain, text/plain;format=flowed, */*" + }, ) assert request.accept == [ "text/plain;format=flowed", @@ -166,12 +164,12 @@ async def get(request): ] request, _ = app.test_client.get( - "/", - headers={ - "Accept": ( - "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c" - ) - }, + "/", + headers={ + "Accept": ( + "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c" + ) + }, ) assert request.accept == [ "text/html", @@ -185,15 +183,16 @@ def test_bad_url_parse(): message = "Bad URL: my.redacted-domain.com:443" with pytest.raises(BadURL, match=message): Request( - b"my.redacted-domain.com:443", - Mock(), - Mock(), - Mock(), - Mock(), - Mock(), - Mock(), + b"my.redacted-domain.com:443", + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), ) + def test_request_scope_is_none_when_no_asgi(): app = Sanic("no_asgi") @@ -201,7 +200,17 @@ def test_request_scope_is_none_when_no_asgi(): async def get(request): return response.empty() - request, _ = app.test_client.get( - "/" - ) - assert request.scope == None \ No newline at end of file + request, _ = app.test_client.get("/") + assert request.scope == None + + +@pytest.mark.asyncio +async def test_request_scope_is_not_none_when_runnin_in_asgi(app): + @app.get("/") + async def get(request): + return response.empty() + + request, _ = await app.asgi_client.get("/") + assert request.scope != None + assert request.scope["method"].lower() == "get" + assert request.scope["path"].lower() == "/" From f0c7b3430d93b9aedb1cd67e4d3c27c8e7668332 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 18 Apr 2022 17:25:59 +0300 Subject: [PATCH 05/12] remove indentation --- tests/test_request.py | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/test_request.py b/tests/test_request.py index 3913801815..385bd66466 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -60,12 +60,12 @@ def test_name_from_set(): @pytest.mark.parametrize( - "request_id,expected_type", - ( - (99, int), - (uuid4(), UUID), - ("foo", str), - ), + "request_id,expected_type", + ( + (99, int), + (uuid4(), UUID), + ("foo", str), + ), ) def test_request_id(request_id, expected_type): app = Sanic("req-generator") @@ -75,7 +75,7 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", headers={"X-REQUEST-ID": f"{request_id}"} + "/", headers={"X-REQUEST-ID": f"{request_id}"} ) assert request.id == request_id assert type(request.id) == expected_type @@ -96,7 +96,7 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", headers={"SOME-OTHER-REQUEST-ID": f"{REQUEST_ID}"} + "/", headers={"SOME-OTHER-REQUEST-ID": f"{REQUEST_ID}"} ) assert request.id == REQUEST_ID * 2 @@ -129,10 +129,10 @@ def test_ipv6_address_is_not_wrapped(app): @app.get("/") async def get(request): return response.json( - { - "client_ip": request.conn_info.client_ip, - "client": request.conn_info.client, - } + { + "client_ip": request.conn_info.client_ip, + "client": request.conn_info.client, + } ) request, resp = app.test_client.get("/", host="::1") @@ -151,10 +151,10 @@ async def get(request): return response.empty() request, _ = app.test_client.get( - "/", - headers={ - "Accept": "text/*, text/plain, text/plain;format=flowed, */*" - }, + "/", + headers={ + "Accept": "text/*, text/plain, text/plain;format=flowed, */*" + }, ) assert request.accept == [ "text/plain;format=flowed", @@ -164,12 +164,12 @@ async def get(request): ] request, _ = app.test_client.get( - "/", - headers={ - "Accept": ( - "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c" - ) - }, + "/", + headers={ + "Accept": ( + "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c" + ) + }, ) assert request.accept == [ "text/html", @@ -183,13 +183,13 @@ def test_bad_url_parse(): message = "Bad URL: my.redacted-domain.com:443" with pytest.raises(BadURL, match=message): Request( - b"my.redacted-domain.com:443", - Mock(), - Mock(), - Mock(), - Mock(), - Mock(), - Mock(), + b"my.redacted-domain.com:443", + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), ) From 584b9b71f9039f4e4043497c36bca1089afd4ebf Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 18 Apr 2022 17:41:22 +0300 Subject: [PATCH 06/12] Add docstring --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 5987335287..cab8e58317 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -823,8 +823,8 @@ def url_for(self, view_name: str, **kwargs) -> str: @property def scope(self) -> Optional[ASGIScope]: """ - :return: the URL - :rtype: str + :return: The ASGI scope of the request. If the app isn't an ASGI app, then returns None. + :rtype: Optional[ASGIScope] """ return self.transport.scope if self.app.asgi else None From 7d321740d979fd5ec8f44ae9ff338558a94fd997 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 18 Apr 2022 17:42:44 +0300 Subject: [PATCH 07/12] != to is not / == to is --- tests/test_request.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_request.py b/tests/test_request.py index 385bd66466..4e1132142d 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -201,7 +201,7 @@ async def get(request): return response.empty() request, _ = app.test_client.get("/") - assert request.scope == None + assert request.scope is None @pytest.mark.asyncio @@ -211,6 +211,7 @@ async def get(request): return response.empty() request, _ = await app.asgi_client.get("/") - assert request.scope != None + + assert request.scope is not None assert request.scope["method"].lower() == "get" assert request.scope["path"].lower() == "/" From 0450728ca39c6e1c9039e06ceed4aa1b05156cb5 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Sun, 24 Apr 2022 15:55:51 +0300 Subject: [PATCH 08/12] removed Optional and raise an exception if not in ASGI mode --- sanic/request.py | 8 +++++--- tests/test_request.py | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index cab8e58317..938554fc56 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -821,13 +821,15 @@ def url_for(self, view_name: str, **kwargs) -> str: ) @property - def scope(self) -> Optional[ASGIScope]: + def scope(self) -> ASGIScope: """ - :return: The ASGI scope of the request. If the app isn't an ASGI app, then returns None. + :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 if self.app.asgi else None + return self.transport.scope class File(NamedTuple): diff --git a/tests/test_request.py b/tests/test_request.py index 4e1132142d..5c21b99d8e 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -193,19 +193,19 @@ def test_bad_url_parse(): ) -def test_request_scope_is_none_when_no_asgi(): +def test_request_scope_raises_exception_when_no_asgi(): app = Sanic("no_asgi") @app.get("/") async def get(request): - return response.empty() + return request.scope - request, _ = app.test_client.get("/") - assert request.scope is None + _, response = app.test_client.get("/") + assert response.status == 500 @pytest.mark.asyncio -async def test_request_scope_is_not_none_when_runnin_in_asgi(app): +async def test_request_scope_is_not_none_when_running_in_asgi(app): @app.get("/") async def get(request): return response.empty() From 14a20941757ccacc589725fe31a65a60a04218b9 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Sun, 24 Apr 2022 16:36:39 +0300 Subject: [PATCH 09/12] added assertRaises --- tests/test_request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_request.py b/tests/test_request.py index 5c21b99d8e..83e2f8e613 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -200,8 +200,10 @@ def test_request_scope_raises_exception_when_no_asgi(): async def get(request): return request.scope - _, response = app.test_client.get("/") + request, response = app.test_client.get("/") assert response.status == 500 + with pytest.raises(NotImplementedError): + _ = request.scope @pytest.mark.asyncio From 5de6ce6b3e6a26143785146403962ed5793f7dee Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Sun, 24 Apr 2022 17:11:00 +0300 Subject: [PATCH 10/12] fix linter fails --- sanic/request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 938554fc56..6435a2e0f3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -823,11 +823,13 @@ def url_for(self, view_name: str, **kwargs) -> str: @property def scope(self) -> ASGIScope: """ - :return: The ASGI scope of the request. If the app isn't an ASGI app, then raises an exception. + :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.") + raise NotImplementedError("App isn't running in ASGI mode. " + "Scope is only available for ASGI apps.") return self.transport.scope From dd83a9e5f86e51ca8d43ff6a09655118eecbba42 Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 25 Apr 2022 11:02:37 +0300 Subject: [PATCH 11/12] make pretty --- sanic/request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 6435a2e0f3..d7015ea15b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -828,8 +828,10 @@ def scope(self) -> ASGIScope: :rtype: Optional[ASGIScope] """ if not self.app.asgi: - raise NotImplementedError("App isn't running in ASGI mode. " - "Scope is only available for ASGI apps.") + raise NotImplementedError( + "App isn't running in ASGI mode. " + "Scope is only available for ASGI apps." + ) return self.transport.scope From 1679ac8663168403704e9fad6f34ac73382805ea Mon Sep 17 00:00:00 2001 From: azimovMichael Date: Mon, 25 Apr 2022 14:12:31 +0300 Subject: [PATCH 12/12] added scope member to TransportProtocol for py3.8 --- sanic/models/protocol_types.py | 4 ++++ tests/test_requests.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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/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"])