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
``timedelta.total_seconds()`` instead. :pr:`3962`
- 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
-------------

View file

@ -233,3 +233,56 @@ registration code::
methods=['GET', 'PUT', 'DELETE'])
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 URLDefaultCallable
from .typing import URLValuePreprocessorCallable
from .typing import ViewFuncArgument
from .views import View
from .wrappers import Request
from .wrappers import Response
@ -1033,10 +1035,16 @@ class Flask(Scaffold):
self,
rule: str,
endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None,
view_func: ViewFuncArgument = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any,
) -> 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:
endpoint = _endpoint_from_view_func(view_func) # type: ignore
options["endpoint"] = endpoint
@ -1078,6 +1086,8 @@ class Flask(Scaffold):
rule.provide_automatic_options = provide_automatic_options # type: ignore
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:
old_func = self.view_functions.get(endpoint)
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 URLDefaultCallable
from .typing import URLValuePreprocessorCallable
from .typing import ViewFuncArgument
from .views import View
if t.TYPE_CHECKING:
from .app import Flask
@ -78,12 +80,15 @@ class BlueprintSetupState:
self,
rule: str,
endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None,
view_func: ViewFuncArgument = None,
**options: t.Any,
) -> None:
"""A helper method to register a rule (and optionally a view function)
to the application. The endpoint is automatically prefixed with the
blueprint's name.
.. versionchanged:: 2.0.0
Support to pass a view class as view function.
"""
if self.url_prefix is not None:
if rule:
@ -96,6 +101,8 @@ class BlueprintSetupState:
defaults = self.url_defaults
if "defaults" in options:
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(
rule,
f"{self.name_prefix}{self.blueprint.name}.{endpoint}",

View file

@ -405,9 +405,9 @@ class Scaffold:
return self._method_route("PATCH", rule, options)
def route(self, rule: str, **options: t.Any) -> t.Callable:
"""Decorate a view function to register it with the given URL
rule and options. Calls :meth:`add_url_rule`, which has more
details about the implementation.
"""Decorate a view function or view class to register it with
the given URL rule and options. Calls :meth:`add_url_rule`, which
has more details about the implementation.
.. code-block:: python
@ -426,6 +426,9 @@ class Scaffold:
:param rule: The URL rule string.
:param options: Extra options passed to the
: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:

View file

@ -5,6 +5,7 @@ if t.TYPE_CHECKING:
from werkzeug.datastructures import Headers # noqa: F401
from wsgiref.types import WSGIApplication # 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.
ResponseValue = t.Union[
@ -44,3 +45,4 @@ TemplateGlobalCallable = t.Callable[[], t.Any]
TemplateTestCallable = t.Callable[[t.Any], bool]
URLDefaultCallable = t.Callable[[str, 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.post("/").status_code == 405
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)