Support using the route decorator on view classes
This commit is contained in:
parent
77db3d5ede
commit
a7ed19cb6e
7 changed files with 134 additions and 6 deletions
|
|
@ -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
|
||||||
-------------
|
-------------
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"]]]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue