diff --git a/docs/views.rst b/docs/views.rst index f221027067..3af415bc65 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -237,6 +237,10 @@ method maps to a method of the class with the same (lowercase) name. methods defined by the class. It even knows how to handle subclasses that override or define other methods. +The standard HTTP ``QUERY`` method is also supported by defining a +``query`` method. This is useful for safe, idempotent queries that need +to send a request body. + We can make a generic ``ItemAPI`` class that provides get (detail), patch (edit), and delete methods for a given model. A ``GroupAPI`` can provide get (list) and post (create) methods. diff --git a/src/flask/sansio/scaffold.py b/src/flask/sansio/scaffold.py index a04c38adc6..0e4dd9c6f2 100644 --- a/src/flask/sansio/scaffold.py +++ b/src/flask/sansio/scaffold.py @@ -332,6 +332,14 @@ 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"]``. + + .. 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..a2b321fc4b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -52,16 +52,20 @@ def index_put(): assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] -@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) +@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch", "query"]) def test_method_route(app, client, method): method_route = getattr(app, method) - client_method = getattr(client, method) @method_route("/") def hello(): return "Hello" - assert client_method("/").data == b"Hello" + if method == "query": + response = client.open("/", method="QUERY") + else: + response = getattr(client, method)("/") + + assert response.data == b"Hello" def test_method_route_no_methods(app): diff --git a/tests/test_views.py b/tests/test_views.py index d002b50439..1670f89d0f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -39,6 +39,17 @@ def post(self): common_test(app) +def test_query_method_view(app, client): + class Index(flask.views.MethodView): + def query(self): + return "QUERY" + + app.add_url_rule("/", view_func=Index.as_view("index")) + + assert client.open("/", method="QUERY").data == b"QUERY" + assert "QUERY" in app.url_map._rules_by_endpoint["index"][0].methods + + def test_view_patching(app): class Index(flask.views.MethodView): def get(self):