Support using the route decorator on view classes

This commit is contained in:
Grey Li 2021-04-28 13:44:53 +08:00
parent 77db3d5ede
commit a7ed19cb6e
7 changed files with 134 additions and 6 deletions

View file

@ -81,7 +81,8 @@ Unreleased
- ``helpers.total_seconds()`` is deprecated. Use - ``helpers.total_seconds()`` is deprecated. Use
``timedelta.total_seconds()`` instead. :pr:`3962` ``timedelta.total_seconds()`` instead. :pr:`3962`
- Add type hinting. :pr:`3973`. - Add type hinting. :pr:`3973`.
- Support using the ``route`` decorator on view classes (i.e.
``View`` and ``MethodView`` subclasses). :issue:`3404`
Version 1.1.2 Version 1.1.2
------------- -------------

View file

@ -233,3 +233,56 @@ registration code::
methods=['GET', 'PUT', 'DELETE']) methods=['GET', 'PUT', 'DELETE'])
register_api(UserAPI, 'user_api', '/users/', pk='user_id') register_api(UserAPI, 'user_api', '/users/', pk='user_id')
Use the ``route`` Decorator on View Classes
-------------------------------------------
For simple use cases, you can use the :func:`~flask.Flask.route` decorator
as a shortcut of :meth:`~flask.views.View.as_view` class method to register
a view class::
from flask.views import View
@app.route('/users/', endpoint='show_users')
class ShowUsers(View):
def dispatch_request(self):
users = User.query.all()
return render_template('users.html', objects=users)
Or on a method-based view class::
from flask.views import MethodView
@app.route('/users/', endpoint='users')
class UserAPI(MethodView):
def get(self):
users = User.query.all()
...
def post(self):
user = User.from_form_data(request.form)
...
You can also pass a view class to :meth:`~flask.Flask.add_url_rule`
directly::
from flask.views import MethodView
class UserAPI(MethodView):
def get(self):
users = User.query.all()
...
def post(self):
user = User.from_form_data(request.form)
...
app.add_url_rule('/users/', endpoint='users', view_func=UserAPI)
Beware that if the ``endpoint`` argument isn't provided, the class name
will be used as the endpoint. Also, you can't pass any class arguments in
this way.

View file

@ -68,6 +68,8 @@ from .typing import TemplateGlobalCallable
from .typing import TemplateTestCallable from .typing import TemplateTestCallable
from .typing import URLDefaultCallable from .typing import URLDefaultCallable
from .typing import URLValuePreprocessorCallable from .typing import URLValuePreprocessorCallable
from .typing import ViewFuncArgument
from .views import View
from .wrappers import Request from .wrappers import Request
from .wrappers import Response from .wrappers import Response
@ -1033,10 +1035,16 @@ class Flask(Scaffold):
self, self,
rule: str, rule: str,
endpoint: t.Optional[str] = None, endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None, view_func: ViewFuncArgument = None,
provide_automatic_options: t.Optional[bool] = None, provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:
"""A helper method to register a rule (and optionally a view function)
to the application.
.. versionchanged:: 2.0.0
Support to pass a view class as view function.
"""
if endpoint is None: if endpoint is None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore endpoint = _endpoint_from_view_func(view_func) # type: ignore
options["endpoint"] = endpoint options["endpoint"] = endpoint
@ -1078,6 +1086,8 @@ class Flask(Scaffold):
rule.provide_automatic_options = provide_automatic_options # type: ignore rule.provide_automatic_options = provide_automatic_options # type: ignore
self.url_map.add(rule) self.url_map.add(rule)
if isinstance(view_func, type) and issubclass(view_func, View):
view_func = view_func.as_view(endpoint)
if view_func is not None: if view_func is not None:
old_func = self.view_functions.get(endpoint) old_func = self.view_functions.get(endpoint)
if getattr(old_func, "_flask_sync_wrapper", False): if getattr(old_func, "_flask_sync_wrapper", False):

View file

@ -15,6 +15,8 @@ from .typing import TemplateGlobalCallable
from .typing import TemplateTestCallable from .typing import TemplateTestCallable
from .typing import URLDefaultCallable from .typing import URLDefaultCallable
from .typing import URLValuePreprocessorCallable from .typing import URLValuePreprocessorCallable
from .typing import ViewFuncArgument
from .views import View
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from .app import Flask from .app import Flask
@ -78,12 +80,15 @@ class BlueprintSetupState:
self, self,
rule: str, rule: str,
endpoint: t.Optional[str] = None, endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None, view_func: ViewFuncArgument = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:
"""A helper method to register a rule (and optionally a view function) """A helper method to register a rule (and optionally a view function)
to the application. The endpoint is automatically prefixed with the to the application. The endpoint is automatically prefixed with the
blueprint's name. blueprint's name.
.. versionchanged:: 2.0.0
Support to pass a view class as view function.
""" """
if self.url_prefix is not None: if self.url_prefix is not None:
if rule: if rule:
@ -96,6 +101,8 @@ class BlueprintSetupState:
defaults = self.url_defaults defaults = self.url_defaults
if "defaults" in options: if "defaults" in options:
defaults = dict(defaults, **options.pop("defaults")) defaults = dict(defaults, **options.pop("defaults"))
if isinstance(view_func, type) and issubclass(view_func, View):
view_func = view_func.as_view(endpoint)
self.app.add_url_rule( self.app.add_url_rule(
rule, rule,
f"{self.name_prefix}{self.blueprint.name}.{endpoint}", f"{self.name_prefix}{self.blueprint.name}.{endpoint}",

View file

@ -405,9 +405,9 @@ class Scaffold:
return self._method_route("PATCH", rule, options) return self._method_route("PATCH", rule, options)
def route(self, rule: str, **options: t.Any) -> t.Callable: def route(self, rule: str, **options: t.Any) -> t.Callable:
"""Decorate a view function to register it with the given URL """Decorate a view function or view class to register it with
rule and options. Calls :meth:`add_url_rule`, which has more the given URL rule and options. Calls :meth:`add_url_rule`, which
details about the implementation. has more details about the implementation.
.. code-block:: python .. code-block:: python
@ -426,6 +426,9 @@ class Scaffold:
:param rule: The URL rule string. :param rule: The URL rule string.
:param options: Extra options passed to the :param options: Extra options passed to the
:class:`~werkzeug.routing.Rule` object. :class:`~werkzeug.routing.Rule` object.
.. versionchanged:: 2.0.0
The ``route`` decorator can be used on view classes.
""" """
def decorator(f: t.Callable) -> t.Callable: def decorator(f: t.Callable) -> t.Callable:

View file

@ -5,6 +5,7 @@ if t.TYPE_CHECKING:
from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401
from wsgiref.types import WSGIApplication # noqa: F401 from wsgiref.types import WSGIApplication # noqa: F401
from .wrappers import Response # noqa: F401 from .wrappers import Response # noqa: F401
from .views import View # noqa: F401
# The possible types that are directly convertible or are a Response object. # The possible types that are directly convertible or are a Response object.
ResponseValue = t.Union[ ResponseValue = t.Union[
@ -44,3 +45,4 @@ TemplateGlobalCallable = t.Callable[[], t.Any]
TemplateTestCallable = t.Callable[[t.Any], bool] TemplateTestCallable = t.Callable[[t.Any], bool]
URLDefaultCallable = t.Callable[[str, dict], None] URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
ViewFuncArgument = t.Optional[t.Union[t.Callable, t.Type["View"]]]

View file

@ -240,3 +240,55 @@ def test_remove_method_from_parent(app, client):
assert client.get("/").data == b"GET" assert client.get("/").data == b"GET"
assert client.post("/").status_code == 405 assert client.post("/").status_code == 405
assert sorted(View.methods) == ["GET"] assert sorted(View.methods) == ["GET"]
def test_route_on_view(app):
@app.route("/", endpoint="index")
class Index(flask.views.View):
methods = ["GET", "POST"]
def dispatch_request(self):
return flask.request.method
common_test(app)
def test_route_on_methodview(app):
@app.route("/", endpoint="index")
class Index(flask.views.MethodView):
def get(self):
return "GET"
def post(self):
return "POST"
common_test(app)
def test_bp_route_on_view(app):
bp = flask.Blueprint("test", __name__)
@bp.route("/", endpoint="index")
class Index(flask.views.View):
methods = ["GET", "POST"]
def dispatch_request(self):
return flask.request.method
app.register_blueprint(bp)
common_test(app)
def test_bp_route_on_methodview(app):
bp = flask.Blueprint("test", __name__)
@bp.route("/", endpoint="index")
class Index(flask.views.MethodView):
def get(self):
return "GET"
def post(self):
return "POST"
app.register_blueprint(bp)
common_test(app)