Repository: django/channels

Issue:
v3 Consumers need as_view() equivalent.

From https://github.com/django/channels/pull/1528/commits/8b605e530bbef5bba6be8ae0af7c5000d34375e1 we need **consumer instances**: 

```python
    websocket_urlpatterns = [
        re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer()),
    ]
```

The routers use `guarantee_single_callable()` which if it hits a class assumes an ASGI 2 double callable. 

**BUT** this gives us the very same consumer instance for every connection, where we need one instance per-connection. 

This was the exact problem for which class-based views got `View.as_view()`, so an `as_consumer()` — it's a bit long but... 


Discussion:
> Why not drop `guarantee_single_callable()` and instantiate the Consumer in the router? 

* Because we're not ready to drop `guarantee_single_callable()`  yet, and **more**…
* We want to be compatible with any ASGI app, not just Channels consumers. 

Resolving patch:
diff --git a/channels/consumer.py b/channels/consumer.py
--- a/channels/consumer.py
+++ b/channels/consumer.py
@@ -80,6 +80,26 @@ async def send(self, message):
         """
         await self.base_send(message)
 
+    @classmethod
+    def as_asgi(cls, **initkwargs):
+        """
+        Return an ASGI v3 single callable that instantiates a consumer instance
+        per scope. Similar in purpose to Django's as_view().
+
+        initkwargs will be used to instantiate the consumer instance.
+        """
+
+        async def app(scope, receive, send):
+            consumer = cls(**initkwargs)
+            return await consumer(scope, receive, send)
+
+        app.consumer_class = cls
+        app.consumer_initkwargs = initkwargs
+
+        # take name and docstring from class
+        functools.update_wrapper(app, cls, updated=())
+        return app
+
 
 class SyncConsumer(AsyncConsumer):
     """


Test patch:
diff --git a/tests/test_generic_http.py b/tests/test_generic_http.py
--- a/tests/test_generic_http.py
+++ b/tests/test_generic_http.py
@@ -34,3 +34,33 @@ async def handle(self, body):
     assert response["body"] == b'{"value": 42}'
     assert response["status"] == 200
     assert response["headers"] == [(b"Content-Type", b"application/json")]
+
+
+@pytest.mark.asyncio
+async def test_per_scope_consumers():
+    """
+    Tests that a distinct consumer is used per scope, with AsyncHttpConsumer as
+    the example consumer class.
+    """
+
+    class TestConsumer(AsyncHttpConsumer):
+        async def handle(self, body):
+            await self.send_response(
+                200,
+                repr(self).encode("utf-8"),
+                headers={b"Content-Type": b"text/plain"},
+            )
+
+    app = TestConsumer.as_asgi()
+
+    # Open a connection
+    communicator = HttpCommunicator(app, method="GET", path="/test/")
+    response = await communicator.get_response()
+    assert response["status"] == 200
+
+    # And another one.
+    communicator = HttpCommunicator(app, method="GET", path="/test2/")
+    second_response = await communicator.get_response()
+    assert second_response["status"] == 200
+
+    assert response["body"] != second_response["body"]