Build a Flask pluggable view that dispatches by object method (by function name)

Flask already provides MethodView that dispatches by HTTP method. But, can we dispatch by object method name?

For example, when we call http://127.0.0.1:5000/user/hello, the hello method of User class is triggered. When we call http://127.0.0.1:5000/user/world, the world method of User class is triggered.

How can we make this happen?

Define a base view class

from flask.views import View

class BaseActionView(View):
    methods = ['POST']  # modify this as you wish

    def dispatch_request(self, *args, **kwargs):
        action = kwargs.pop('action')  # action corresponds to the method name, we define it in the url rule
        assert action is not None, 'Miss action'

        meth = getattr(self, action, None)
        assert meth is not None and getattr(meth, '_api', False), f"Unimplemented action {action}"

        return meth()

Notice the second to last code line, getattr(meth, '_api', False) is used to distinguish published APIs and inner methods. We will further explain this later.

Define a decorator

from functools import wraps

def api(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    wrapper._api = True  # Mark as API
    return wrapper

The api decorator marks a function as an API. If a function is not marked, it's illegal to call it through a http request.

Create a view and register to the app

from flask import Flask

app = Flask(__name__)

class UserActionView(BaseActionView):  # inherit the base class we defined earlier
    def say(self, msg):  # inner method
        return {
            'msg': msg
        }

    @api
    def hello(self):  # API method
        return self.say('hello')

    @api
    def world(self):  # API method
        return self.say('world')

# "aciton" part in the url rule will be treated as the view class's method name 
app.add_url_rule('/user/<string:action>', view_func=UserActionView.as_view('user'))

app.run()

In class UserActionView, we define three methods:

  • say is an inner method that we cannot call it through a http request.
  • hello and world are marked as APIs with api decorator. We can call them through http requests.

The second to last code line use add_url_rule to register APIs of UserActionView.

Test UserActionView

Test hello method of UserActionView.

curl -X POST http://127.0.0.1:5000/user/hello
# output: {"msg":"hello"}

Test world method of UserActionView.

curl -X POST http://127.0.0.1:5000/user/world
# output: {"msg":"world"}

What if we call a non-API method?

curl -X POST http://127.0.0.1:5000/user/say
# got a 500 error

Look at the server console, you'll see AssertionError: Unimplemented action say.

Put together

from flask import Flask
from flask.views import View
from functools import wraps


app = Flask(__name__)


def api(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    wrapper._api = True  # Mark as API
    return wrapper


class BaseActionView(View):
    methods = ['POST']  # modify this as you wish

    def dispatch_request(self, *args, **kwargs):
        action = kwargs.pop('action')  # action corresponds to the method name, we define it in the url rule
        assert action is not None, 'Miss action'

        meth = getattr(self, action, None)
        assert meth is not None and getattr(meth, '_api', False), f"Unimplemented action {action}"

        return meth()


class UserActionView(BaseActionView):  # inherit the base class we defined earlier
    def say(self, msg):  # inner method
        return {
            'msg': msg
        }

    @api
    def hello(self):  # API method
        return self.say('hello')

    @api
    def world(self):  # API method
        return self.say('world')


# "aciton" part in the url rule will be treated as the view class's method name 
app.add_url_rule('/user/<string:action>', view_func=UserActionView.as_view('user'))

app.run(debug=False)
Posted on 2022-05-13