Repository: litestar-org/litestar

Issue:
Bug: Caching response throws AttributeError on GET: Can't pickle local object 'DependencyCleanupGroup.wrap_asgi.<locals>.wrapped'
**Describe the bug**
When caching the response of a get request for a `Template`, I get the following exception which throws a server 500 error:

`AttributeError on GET:  Can't pickle local object 'DependencyCleanupGroup.wrap_asgi.<locals>.wrapped'`

**To Reproduce**
Here is a reproducible example:  

```
import uvicorn

from starlite import Starlite, get, Request, State, Template, Controller, TemplateConfig
from starlite.template.jinja import JinjaTemplateEngine


class FrontEnd(Controller):
    @get("/", cache=60)
    async def serveHomepage(self, request: Request, state: State) -> Template:
        return Template(
            name="homepage.html.j2",
            context={"request": request},
        )

    @get("/hello-world", cache=60)
    async def hello_world(self) -> dict[str, str]:
        """Handler function that returns a greeting dictionary."""
        return {"hello": "world"}


app = Starlite(
    route_handlers=[FrontEnd],
    template_config=TemplateConfig(
        directory="src/templates", engine=JinjaTemplateEngine
    ),
    debug=True,
)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)


```

You can use this for the Jinja template:

```
<html>
<head>
</head>
<body>
    <h1>Homepage Header</h1>
</body>
</html>
```

**Additional context**
If you remove the cache value from the `Template` route (or set it to zero), the `get` requests are served as expected.  

In the example code, I also included a route for a non-template request, `/hello-world`, which does not throw an error.  So the problem seems to lie with `Template` responses, or some subset of responses. 

Bug: Caching response throws AttributeError on GET: Can't pickle local object 'DependencyCleanupGroup.wrap_asgi.<locals>.wrapped'
**Describe the bug**
When caching the response of a get request for a `Template`, I get the following exception which throws a server 500 error:

`AttributeError on GET:  Can't pickle local object 'DependencyCleanupGroup.wrap_asgi.<locals>.wrapped'`

**To Reproduce**
Here is a reproducible example:  

```
import uvicorn

from starlite import Starlite, get, Request, State, Template, Controller, TemplateConfig
from starlite.template.jinja import JinjaTemplateEngine


class FrontEnd(Controller):
    @get("/", cache=60)
    async def serveHomepage(self, request: Request, state: State) -> Template:
        return Template(
            name="homepage.html.j2",
            context={"request": request},
        )

    @get("/hello-world", cache=60)
    async def hello_world(self) -> dict[str, str]:
        """Handler function that returns a greeting dictionary."""
        return {"hello": "world"}


app = Starlite(
    route_handlers=[FrontEnd],
    template_config=TemplateConfig(
        directory="src/templates", engine=JinjaTemplateEngine
    ),
    debug=True,
)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)


```

You can use this for the Jinja template:

```
<html>
<head>
</head>
<body>
    <h1>Homepage Header</h1>
</body>
</html>
```

**Additional context**
If you remove the cache value from the `Template` route (or set it to zero), the `get` requests are served as expected.  

In the example code, I also included a route for a non-template request, `/hello-world`, which does not throw an error.  So the problem seems to lie with `Template` responses, or some subset of responses. 



Discussion:
Attaching traceback: 

![image](https://user-images.githubusercontent.com/114229148/205334660-bd1f2208-1824-498b-b8d7-2444771fa3ad.png)

Have you switched python versions recently?
Nope, I’ve been on 3.11 since the day it came out 
@Goldziher I think it has nothing to do with the Python version. The offending code is the fact that now response is wrapped into DependencyCleanupGroup to be able to do cleanup - [here](https://github.com/starlite-api/starlite/blob/main/starlite/routes/http.py#L167)

Obviously this can not be cached and before saving response to cache we need to unwrap it first. 
@jtraub You're right. `DependencyCleaupGroup` essentially creates an ASGI middleware here.
The reasoning so we don't have to pass the cleanup group all the way up to where the response is returned, which we need to we can consistently call the cleanup immediately before the response is returned.  I'm thinking of either making a transparent wrapper so it can be unwrapped if needed, or forego the wrapping and simply accept that we have to pass it around a bit. WDYT?
@provinzkraut I am a bit torn here because having nice and pure "ASGIApp" typings makes me happy but on the other hand we might forget to unwrap it again somewhere else.

However, it seems like making a transparent wrapper with a way to access original response object is a bit better.
We can do both I think. The wrapper can be made into an ASGI callable that can be pickled.
After thinking about this, the proper way to do this seems to be calling the cleanup **before** the response is cached. This would also mean we can omit passing the cleanup group around.
Attaching traceback: 

![image](https://user-images.githubusercontent.com/114229148/205334660-bd1f2208-1824-498b-b8d7-2444771fa3ad.png)

Have you switched python versions recently?
Nope, I’ve been on 3.11 since the day it came out 
@Goldziher I think it has nothing to do with the Python version. The offending code is the fact that now response is wrapped into DependencyCleanupGroup to be able to do cleanup - [here](https://github.com/starlite-api/starlite/blob/main/starlite/routes/http.py#L167)

Obviously this can not be cached and before saving response to cache we need to unwrap it first. 
@jtraub You're right. `DependencyCleaupGroup` essentially creates an ASGI middleware here.
The reasoning so we don't have to pass the cleanup group all the way up to where the response is returned, which we need to we can consistently call the cleanup immediately before the response is returned.  I'm thinking of either making a transparent wrapper so it can be unwrapped if needed, or forego the wrapping and simply accept that we have to pass it around a bit. WDYT?
@provinzkraut I am a bit torn here because having nice and pure "ASGIApp" typings makes me happy but on the other hand we might forget to unwrap it again somewhere else.

However, it seems like making a transparent wrapper with a way to access original response object is a bit better.
We can do both I think. The wrapper can be made into an ASGI callable that can be pickled.
After thinking about this, the proper way to do this seems to be calling the cleanup **before** the response is cached. This would also mean we can omit passing the cleanup group around.

Resolving patch:
diff --git a/starlite/datastructures/provide.py b/starlite/datastructures/provide.py
--- a/starlite/datastructures/provide.py
+++ b/starlite/datastructures/provide.py
@@ -23,7 +23,7 @@
     from inspect import Traceback
 
     from starlite.signature import SignatureModel
-    from starlite.types import AnyCallable, AnyGenerator, ASGIApp, Receive, Scope, Send
+    from starlite.types import AnyCallable, AnyGenerator
 
 
 class Provide:
@@ -155,22 +155,6 @@ async def cleanup(self) -> None:
             for generator in self._generators:
                 task_group.start_soon(self._wrap_next(generator))
 
-    def wrap_asgi(self, app: "ASGIApp") -> "ASGIApp":
-        """Wrap an ASGI callable such that all generators will be called before it.
-
-        Args:
-            app: An ASGI callable
-
-        Returns:
-            An ASGI callable
-        """
-
-        async def wrapped(scope: "Scope", receive: "Receive", send: "Send") -> None:
-            await self.cleanup()
-            await app(scope, receive, send)
-
-        return wrapped
-
     async def __aenter__(self) -> None:
         """Support the async contextmanager protocol to allow for easier catching and throwing of exceptions into the
         generators.
diff --git a/starlite/routes/http.py b/starlite/routes/http.py
--- a/starlite/routes/http.py
+++ b/starlite/routes/http.py
@@ -163,10 +163,11 @@ async def _call_handler_function(
                 plugins=request.app.plugins,
                 request=request,
             )
+
         if cleanup_group:
-            return cleanup_group.wrap_asgi(response)
+            await cleanup_group.cleanup()
 
-        return response
+        return response  # noqa: R504
 
     @staticmethod
     async def _get_response_data(


Test patch:
diff --git a/tests/kwargs/test_generator_dependencies.py b/tests/kwargs/test_generator_dependencies.py
--- a/tests/kwargs/test_generator_dependencies.py
+++ b/tests/kwargs/test_generator_dependencies.py
@@ -54,8 +54,10 @@ async def dependency() -> AsyncGenerator[str, None]:
     return dependency
 
 
+@pytest.mark.parametrize("cache", [False, True])
 @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"])
 def test_generator_dependency(
+    cache: bool,
     request: FixtureRequest,
     dependency_fixture: str,
     cleanup_mock: MagicMock,
@@ -64,7 +66,7 @@ def test_generator_dependency(
 ) -> None:
     dependency = request.getfixturevalue(dependency_fixture)
 
-    @get("/", dependencies={"dep": Provide(dependency)})
+    @get("/", dependencies={"dep": Provide(dependency)}, cache=cache)
     def handler(dep: str) -> Dict[str, str]:
         return {"value": dep}