Repository: encode/starlette

Issue:
Generic interface for storing per-request state.
From this: https://github.com/encode/starlette/issues/374#issuecomment-461684385

I think we should probably support a public interface for storing arbitary per-request state, within the ASGI scope.

That could either be:

1. `request['whatever'] = ...` - Expose the ASGI scope, and let the user handle it directly if they really need to.
2. `request.scope['whatever'] = ...` - Same as before, but let's move it into a different bit of public API.
3. `request.state.whatever = ...` - Store arbitrary state, and always keep it isolated from the rest of the namespace in the ASGI scope. (Implementation wise, store it all in a scope['state'] dictionary)

With (1) and (2) we might need to think a bit about mutability and it's implications. Eg. If the scope is modified, do we update the `request.url`, `request.headers` or whatever else it might affect? Probably we just put a disclaimer on "request.scope" gives you access to the underlying ASGI scope - make sure you've got a bit of an idea what you're doing.

Incidentally, I think I prefer (2) over the "request is dict-like" interface that we currently have.


Discussion:
Also note that (2) doesn't neccessarily preclude (3)
(Ie. we could expose the `scope` dictionary, *and* have a generic "some other request state" interface)
From a beginner perspective, I prefer 3 then 2 because I know that it's my own space to mess around with and I don't have to wonder if I'm doing something dangerous like in 1.  Being immutable might actually prevent it, but you don't know until you try for yourself and perhaps in multiple ways to be confident you are OK to do that.  I would also imagine that it saves from potential breaking changes when I happen to have named something in a way you would like to use in Starlette.
Okay, then. So I'd be very happy to have a `request.state` interface added.

Implementation-wise this should just store everything into a dictionary within the ASGI scope, named `"state"`.

Eg. `request.state.time_started = time.time()` --> scope["state"]["time_started"] = time.time()

Resolving patch:
diff --git a/starlette/requests.py b/starlette/requests.py
--- a/starlette/requests.py
+++ b/starlette/requests.py
@@ -18,6 +18,10 @@ class ClientDisconnect(Exception):
     pass
 
 
+class State:
+    pass
+
+
 class HTTPConnection(Mapping):
     """
     A base class for incoming HTTP connections, that is used to provide
@@ -88,16 +92,6 @@ def session(self) -> dict:
         ), "SessionMiddleware must be installed to access request.session"
         return self._scope["session"]
 
-    @property
-    def database(self) -> typing.Any:  # pragma: no cover
-        # NOTE: Pending deprecation. You probably want to look at the
-        # stand-alone `databases` package instead.
-        # https://github.com/encode/databases
-        assert (
-            "database" in self._scope
-        ), "DatabaseMiddleware must be installed to access request.database"
-        return self._scope["database"]
-
     @property
     def auth(self) -> typing.Any:
         assert (
@@ -112,6 +106,12 @@ def user(self) -> typing.Any:
         ), "AuthenticationMiddleware must be installed to access request.user"
         return self._scope["user"]
 
+    @property
+    def state(self) -> State:
+        if "state" not in self._scope:
+            self._scope["state"] = State()
+        return self._scope["state"]
+
     def url_for(self, name: str, **path_params: typing.Any) -> str:
         router = self._scope["router"]
         url_path = router.url_path_for(name, **path_params)


Test patch:
diff --git a/tests/test_requests.py b/tests/test_requests.py
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -280,6 +280,21 @@ async def asgi(receive, send):
     assert disconnected_after_response
 
 
+def test_request_state():
+    def app(scope):
+        async def asgi(receive, send):
+            request = Request(scope, receive)
+            request.state.example = 123
+            response = JSONResponse({"state.example": request["state"].example})
+            await response(receive, send)
+
+        return asgi
+
+    client = TestClient(app)
+    response = client.get("/123?a=abc")
+    assert response.json() == {"state.example": 123}
+
+
 def test_request_cookies():
     def app(scope):
         async def asgi(receive, send):