diff --git a/CHANGES.rst b/CHANGES.rst index 232b144a5c..f3d625f173 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,6 +28,9 @@ Unreleased it's disabled in config. Previously, only disabling worked. :issue:`5916` - ``Flask.select_jinja_autoescape`` uses case-insensitive comparison instead of only lower case file extensions. :pr:`6012` +- Add ``query`` method route shortcut for the ``QUERY`` HTTP method + (:rfc:`10008`). ``MethodView`` dispatches ``QUERY`` requests to a ``query`` + handler. :issue:`6065` Version 3.1.3 diff --git a/src/flask/sansio/scaffold.py b/src/flask/sansio/scaffold.py index a04c38adc6..015b710e23 100644 --- a/src/flask/sansio/scaffold.py +++ b/src/flask/sansio/scaffold.py @@ -332,6 +332,17 @@ def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """ return self._method_route("PATCH", rule, options) + @setupmethod + def query(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["QUERY"]``. + + The ``QUERY`` method is a safe, idempotent request method that + carries content in the request body, defined in :rfc:`10008`. + + .. versionadded:: 3.2 + """ + return self._method_route("QUERY", rule, options) + @setupmethod def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Decorate a view function to register it with the given URL diff --git a/src/flask/views.py b/src/flask/views.py index 53fe976dc2..10107c7d52 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -9,7 +9,7 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any]) http_method_funcs = frozenset( - ["get", "post", "head", "options", "delete", "put", "trace", "patch"] + ["get", "post", "head", "options", "delete", "put", "trace", "patch", "query"] ) diff --git a/tests/test_basic.py b/tests/test_basic.py index 1d9d83f8cb..57fd42f225 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -69,6 +69,19 @@ def test_method_route_no_methods(app): app.get("/", methods=["GET", "POST"]) +def test_query_method_route(app, client): + @app.query("/") + def query(): + return flask.request.get_data(as_text=True) + + rv = client.open("/", method="QUERY", data="select=*") + assert rv.status_code == 200 + assert rv.data == b"select=*" + # The QUERY method is advertised like any other registered method. + rv = client.open("/", method="OPTIONS") + assert "QUERY" in rv.allow + + def test_provide_automatic_options_attr_disable( app: flask.Flask, client: FlaskClient ) -> None: diff --git a/tests/test_views.py b/tests/test_views.py index d002b50439..cad4903cb4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -39,6 +39,20 @@ def post(self): common_test(app) +def test_query_method_view(app, client): + class Index(flask.views.MethodView): + def query(self): + return flask.request.get_data(as_text=True) + + app.add_url_rule("/", view_func=Index.as_view("index")) + + assert Index.methods == {"QUERY"} + rv = client.open("/", method="QUERY", data="select=*") + assert rv.status_code == 200 + assert rv.data == b"select=*" + assert client.get("/").status_code == 405 + + def test_view_patching(app): class Index(flask.views.MethodView): def get(self):