pallets/flask

class-based endpoints do not accept positional arguments

miguelgrinberg opened this issue · 3 comments

There are some Flask extensions that have decorators that inject positional arguments into decorated view functions. The one that comes to mind is webargs, in particular its use_args decorator. Here is an example based on their documentation:

from flask import Flask
from webargs import fields
from webargs.flaskparser import use_args

app = Flask(__name__)

@app.route("/user/<int:uid>")
@use_args({"per_page": fields.Int()}, location="query")
def user_detail(args, uid):
    return ("The user page for user {uid}, showing {per_page} posts.").format(
        uid=uid, per_page=args["per_page"]
    )

This is how the endpoint is invoked using curl:

$ curl -X GET http://localhost:5000/user/1\?per_page=20
The user page for user 1, showing 20 posts.%

After adapting this example to a MethodView, this is what we end up with:

from flask import Flask
from flask.views import MethodView
from webargs import fields
from webargs.flaskparser import use_args

app = Flask(__name__)

class UserEndpoint(MethodView):
    decorators = [use_args({"per_page": fields.Int()}, location="query")]

    def get(self, args, uid):
        return ("The user page for user {uid}, showing {per_page} posts.").format(
            uid=uid, per_page=args["per_page"]
        )

app.add_url_rule("/user/<int:uid>", view_func=UserEndpoint.as_view('get_user'))

Sending the same curl request as above to the MethodView app causes the following exception:

[2023-07-15 23:32:57,473] ERROR in app: Exception on /user/1 [GET]
Traceback (most recent call last):
  File "/home/miguel/Documents/dev/flask/src/flask/app.py", line 2190, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/miguel/Documents/dev/flask/src/flask/app.py", line 1486, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/miguel/Documents/dev/flask/src/flask/app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/miguel/Documents/dev/flask/src/flask/app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/miguel/Documents/dev/flask/venv/lib/python3.11/site-packages/webargs/core.py", line 649, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
TypeError: View.as_view.<locals>.view() takes 0 positional arguments but 1 was given
127.0.0.1 - - [15/Jul/2023 23:32:57] "GET /user/1?per_page=20 HTTP/1.1" 500 -

The reason of this failure is that the positional argument(s) injected by the use_args decorator are not accepted by the view function returned by View.as_view(). My expectation is that any positional arguments passed into the view function generated from the View or MethodView instance would be accepted by its dispatch_request() method, which in turn would pass them on to the get(), post(), etc. when MethodView is used.

Environment:

  • Python version: 3.11
  • Flask version: 2.3
greyli commented

I don't know if #5200 should be merged. But I have some related info to share.

From webargs 9, the arguments passed to the view function will always be keyword arguments (docs).

I tried to do the same thing with #4776. Then I request to support keyword argument for webargs's use_args in marshmallow-code/webargs#830, which is implemented in marshmallow-code/webargs#833 and released with 8.3.0. For webargs 8, you could set the config USE_ARGS_POSITIONAL to enable this feature:

class KeywordOnlyParser(FlaskParser):
    USE_ARGS_POSITIONAL = False

Thanks for reporting it to webargs too, I hadn't seen that. I was going to link to your previous issue as well, since it's the same answer. I don't think accepting *args was intentional in the past, view functions are only passed **kwargs. WebArgs, APIFlask, and Quart-Schema all support passing by kwargs.

This does not affect me in any way as I'm not a fan of class-based views, but it is noneless a disapointing decision. You may not like positional args, but injecting args into functions through decorators is a well established practice that trascends Flask views.

Here you had two options. One option would have been to say I'll just allow decorators for class-based views to work exactly like the do for regular function-based views, and if someone wants to inject positional args it is their business. Instead you have chosen to restrict some decorators that work fine for function-based views from being used with class-based views, causing an obscure error that requires people like myself to spend time debugging to be able to figure out, and forcing developers to adopt your own views of how a Flask view function should be constructed.

As I said, I don't need this myself and I personally do not care, but you have made a bad design decision here.