Repository: sanic-org/sanic
Pull request: Expose scope parameter in request object
From #2417 

Problem statement:
Expose ASGI scope on Request object
**Is your feature request related to a problem? Please describe.**
I'm trying to write an application framework that works across ASGI compatible server implementations and to do this I'd like to make the ASGI scope available to users. Sanic does not make the scope publicly available though. 


**Describe the solution you'd like**
I'd like the `sanic.request.Request` object to provide a `scope` attribute which is the current `ASGIScope`


**Additional context**
Presently I'm working around this by accessing `request.app._asgi_app.transport.scope`

Hints from issue discussion:
That's a good idea, and sounds easy enough that we can probably squeeze that in for the upcoming release.
You'll probably need to handle the case where the server isn't being run via ASGI and thus there is no `_asgi_app` with some informative error message, but from what I can tell it seems like the project is moving away from running without ASGI.
> from what I can tell it seems like the project is moving away from running without ASGI.

Just curious, what makes you think this is the case? The built-in Sanic server (TMK) is still the most common deployment method. But, I do agree that ASGI support is an important alternative.
I can't recall exactly, but I may have skimmed [this thread](https://community.sanicframework.org/t/considering-asgi-support/70/3) and came away with the wrong idea.
No, not necessarily. ASGI is very important and I hope will continue to be. But we are not getting rid of the internal server either. Being geared specifically for Sanic it has the benefit of being optimized for performance in ways that ASGI servers cannot be.
Hi, I would like to take this issue, as my first contribution to the project.
So from the little I understand in the codebase, I thought about doing it like this:
1. Adding an Optional `scope` argument to the `Request` object that will default `None`.
2. When creating an `ASGIApp` instance, pass the scope param to the `request_class` variable.
WDYT? @ahopkins @rmorshea 
That sounds reasonable, but then again, you could also make it a `@property` and return `None` if there's no `_asgi_app`.

I'm not very familiar with Sanic's internals though, so I can't say what the best option would be.
Yeah, I can do that, but I guess the downside is that currently the Request object is decoupled completely from any ASGI internals, and this change will change that a bit.
I'll wait for @ahopkins to weigh in
@azimovMichael I would be happy for you to take this on. I see you have been assigned it already. Sorry for my delay, I am just now getting back to my regular schedule after some time off.

Instead of altering the `__init__`, we could just assign it:

```python
instance.request = request_class(
    ...
)
instance.request.stream = instance
...
instance.request._scope = scope
```

Using `@property` as suggested by @rmorshea is a nice idea so that it is less easily changed and a pattern used elsewhere on the `Request` object.
I would go so far as to do something like this:

```python
@property
def scope(self):
    if not self._scope:
        raise NotImplementedError("...")
    return self._scope
```

Commit message: remove indentation

Patch to review:
diff --git a/sanic/request.py b/sanic/request.py
index 5f0de3625f..5987335287 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.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..385bd66466 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -191,3 +191,26 @@ 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
+
+
+@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() == "/"


Code review comments:
diff_hunk: @@ -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
path: sanic/request.py
text: @user1:
```suggestion
        :return: the ASGI scope object if running in ASGI mode
```

diff_hunk: @@ -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
path: sanic/request.py
text: @user1:
```suggestion
        :rtype: Optional[ASGIScope]
```

diff_hunk: @@ -191,3 +191,26 @@ 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
path: tests/test_request.py
text: @user1:
Small nitpick
```suggestion
    assert request.scope is None
```

diff_hunk: @@ -191,3 +191,26 @@ 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
+
+
+@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
path: tests/test_request.py
text: @user1:
```suggestion
    assert request.scope is not None
```