diff --git a/sanic/views.py b/sanic/views.py index 32b70b4a..64a872a4 100644 --- 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 @@ class HTTPMethodView: 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 @@ class HTTPMethodView: 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 diff --git a/tests/test_views.py b/tests/test_views.py index 558496c8..23cbd9ce 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -77,6 +77,56 @@ def test_with_bp(app): 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 test_composition_view_runs_methods_as_expected(app, method): 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()