Repository: sanic-org/sanic

Issue:
deprecate CompositionView ? 
Currently sanic offers a class called `CompositionView`

I really am struggling to find any utility in this class, since 

```python
from sanic.views import CompositionView

def get_handler(request):
    return text("I am a get method")

view = CompositionView()
view.add(["GET"], get_handler)
view.add(["POST", "PUT"], lambda request: text("I am a post/put method"))

# Use the new view to handle requests to the base URL
app.add_route(view, "/")
```


Seems much more confusing to me than

```python
def get_handler(request):
    return text("I am a get method")

app.route("/", methods=["GET"])(get_handler)
app.route("/", methods=["POST", "PUT"])(lambda request: text("I am a post/put method"))
```

Can anyone offer a compelling use case for CompositionView?

If not, I would suggest to deprecate it 


https://github.com/sanic-org/sanic/blob/master/sanic/views.py


Discussion:
For HTTPMethodView I can somehow see the utility since everything is defined within the class so you can have class methods that are common to the request types, but for CompositionView where you don't implement a subclass that value is lost
Only have time to leave a quick note. I'll respond with some thoughts more fully later this weekend. 

CompositionView is a PITA to support.
I think this is another one of those features that's been in Sanic from the start, and its stayed around for a long time, and its only still there because "its always been that way".
I was looking back trough some code history and it looks like CompositionView was implemented initially as a quick and easy way do something like a MethodView class. But real MethodViews were added not long after, and seems like CompositionView should be deprecated.
Thanks @ashleysommer for taking a look through that. I agree that this is a good example of another feature that Sanic has probably outgrown.

I agree with deprecation notice from 21.6 and removal in 21.12.
At the same time as this change, it could be an idea to provide HTTPMethodView with a method like

```python
def attach_routes_to_app(self, app, route_prefix):
    # probably completely wrong syntax I am just making this up 
    for method, handler in self.methods.items():
         app.route(handler, method, route_prefix)
```

And then in the future requiring people to use

`view.attach_routes_to_app(app, "/example")`

instead of 

`app.add_route(view.as_view(), "/example")`

Would take care of the maintenance frustration @ahopkins mentioned, which also carries forward to sanic plugins/ addons like e.g. sanic-openapi
Not a bad idea. I am generally a fan, however, to use concise method names inside the framework where possible. It helps people from having to refer back to the documentation so much (of course type annotations will help). I would suggest just `attach(...)`.

It also feels like it should be a class method:

```python
@classmethod
def attach(cls, instance: Union[Sanic, Blueprint], url_prefix: str):
```

Then...

```python
class MyView(HTTPMethodView):
    ...

MyView.attach(Sanic.get_app(), "/view")
```

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this  is incorrect, please respond with an update. Thank you for your contributions.


Resolving patch:
diff --git a/sanic/views.py b/sanic/views.py
--- a/sanic/views.py
+++ b/sanic/views.py
@@ -1,9 +1,25 @@
-from typing import Any, Callable, List
+from __future__ import annotations
+
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Iterable,
+    List,
+    Optional,
+    Union,
+)
+from warnings import warn
 
 from sanic.constants import HTTP_METHODS
 from sanic.exceptions import InvalidUsage
 
 
+if TYPE_CHECKING:
+    from sanic import Sanic
+    from sanic.blueprints import Blueprint
+
+
 class HTTPMethodView:
     """Simple class based implementation of view for the sanic.
     You should implement methods (get, post, put, patch, delete) for the class
@@ -40,6 +56,31 @@ def get(self, request, my_param_here, *args, **kwargs):
 
     decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
 
+    def __init_subclass__(
+        cls,
+        attach: Optional[Union[Sanic, Blueprint]] = None,
+        uri: str = "",
+        methods: Iterable[str] = frozenset({"GET"}),
+        host: Optional[str] = None,
+        strict_slashes: Optional[bool] = None,
+        version: Optional[int] = None,
+        name: Optional[str] = None,
+        stream: bool = False,
+        version_prefix: str = "/v",
+    ) -> None:
+        if attach:
+            cls.attach(
+                attach,
+                uri=uri,
+                methods=methods,
+                host=host,
+                strict_slashes=strict_slashes,
+                version=version,
+                name=name,
+                stream=stream,
+                version_prefix=version_prefix,
+            )
+
     def dispatch_request(self, request, *args, **kwargs):
         handler = getattr(self, request.method.lower(), None)
         return handler(request, *args, **kwargs)
@@ -65,6 +106,31 @@ def view(*args, **kwargs):
         view.__name__ = cls.__name__
         return view
 
+    @classmethod
+    def attach(
+        cls,
+        to: Union[Sanic, Blueprint],
+        uri: str,
+        methods: Iterable[str] = frozenset({"GET"}),
+        host: Optional[str] = None,
+        strict_slashes: Optional[bool] = None,
+        version: Optional[int] = None,
+        name: Optional[str] = None,
+        stream: bool = False,
+        version_prefix: str = "/v",
+    ) -> None:
+        to.add_route(
+            cls.as_view(),
+            uri=uri,
+            methods=methods,
+            host=host,
+            strict_slashes=strict_slashes,
+            version=version,
+            name=name,
+            stream=stream,
+            version_prefix=version_prefix,
+        )
+
 
 def stream(func):
     func.is_stream = True
@@ -91,6 +157,11 @@ class CompositionView:
     def __init__(self):
         self.handlers = {}
         self.name = self.__class__.__name__
+        warn(
+            "CompositionView has been deprecated and will be removed in "
+            "v21.12. Please update your view to HTTPMethodView.",
+            DeprecationWarning,
+        )
 
     def __name__(self):
         return self.name


Test patch:
diff --git a/tests/test_views.py b/tests/test_views.py
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -77,6 +77,56 @@ def get(self, request):
     assert response.text == "I am get method"
 
 
+def test_with_attach(app):
+    class DummyView(HTTPMethodView):
+        def get(self, request):
+            return text("I am get method")
+
+    DummyView.attach(app, "/")
+
+    request, response = app.test_client.get("/")
+
+    assert response.text == "I am get method"
+
+
+def test_with_sub_init(app):
+    class DummyView(HTTPMethodView, attach=app, uri="/"):
+        def get(self, request):
+            return text("I am get method")
+
+    request, response = app.test_client.get("/")
+
+    assert response.text == "I am get method"
+
+
+def test_with_attach_and_bp(app):
+    bp = Blueprint("test_text")
+
+    class DummyView(HTTPMethodView):
+        def get(self, request):
+            return text("I am get method")
+
+    DummyView.attach(bp, "/")
+
+    app.blueprint(bp)
+    request, response = app.test_client.get("/")
+
+    assert response.text == "I am get method"
+
+
+def test_with_sub_init_and_bp(app):
+    bp = Blueprint("test_text")
+
+    class DummyView(HTTPMethodView, attach=bp, uri="/"):
+        def get(self, request):
+            return text("I am get method")
+
+    app.blueprint(bp)
+    request, response = app.test_client.get("/")
+
+    assert response.text == "I am get method"
+
+
 def test_with_bp_with_url_prefix(app):
     bp = Blueprint("test_text", url_prefix="/test1")
 
@@ -218,15 +268,15 @@ def first(request):
         assert response.status == 200
         assert response.text == "first method"
 
-        # response = view(request)
-        # assert response.body.decode() == "first method"
+        response = view(request)
+        assert response.body.decode() == "first method"
 
-    # if method in ["DELETE", "PATCH"]:
-    #     request, response = getattr(app.test_client, method.lower())("/")
-    #     assert response.text == "second method"
+    if method in ["DELETE", "PATCH"]:
+        request, response = getattr(app.test_client, method.lower())("/")
+        assert response.text == "second method"
 
-    #     response = view(request)
-    #     assert response.body.decode() == "second method"
+        response = view(request)
+        assert response.body.decode() == "second method"
 
 
 @pytest.mark.parametrize("method", HTTP_METHODS)
@@ -244,3 +294,12 @@ def test_composition_view_rejects_invalid_methods(app, method):
     if method in ["DELETE", "PATCH"]:
         request, response = getattr(app.test_client, method.lower())("/")
         assert response.status == 405
+
+
+def test_composition_view_deprecation():
+    message = (
+        "CompositionView has been deprecated and will be removed in v21.12. "
+        "Please update your view to HTTPMethodView."
+    )
+    with pytest.warns(DeprecationWarning, match=message):
+        CompositionView()