Register
Dispatch, Don't Decorate

A typical Python web application uses decorators like this:

@app.route('/secrets') @requires_auth def api_hello(): return "Shhh this is top secret spy stuff!"

The @app.route and @requires_auth syntax is known as a decorator in Python, which is a powerful technique to extend functionality to existing functions. You can learn more about them from Jeff Knupp.

Web frameworks make extensive use of decorators @require_auth to protect functions and @app.route to bind URIs to functions. In this article, I discuss some issues with decoration, and suggest you use dynamic dispatch instead.

Static Decoration Hinders Reuse

Python's decoration syntax is static. However, the concept of decoration is usually referred to in a dynamic context. From Wikipedia, for example, the Decorator Pattern"allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class." Interestingly, the examples are all written in Java, which is generally thought of as a static language, which unlike Python, does not allow you to define decorators statically.

Static decoration is problematic, because it reduces reuse. Consider this example:

@app.route('/') def show_entries(): cur = g.db.execute('select title, text from entries order by id desc') entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] return render_template('show_entries.html', entries=entries)

The @app.route decorator statically binds the URI "/" to the function show_entries. The decorator also checks show_entries's parameters to see if they match the URI.

The decorator also has the property of making it impossible to call show_entries outside of the context of a Flask application. app is an instance, which must be passed in to the module both statically and globally. The module cannot be compiled without app being initialized.

You cannot reuse show_entries in any other execution context. For example, you might want to invoke a command line program that calls show_entries. The command line program must initialize a Flask app and provide it to the module. What's more insidious in this case is that app is a global attribute of the containing module, which makes it very difficult to workaround, for example, by monkey patching app as a mock object before the decorator is called.

Dispatch Enables Reuse

A different approach to wrapping functionality around existing functions is with a Dispatch Table. Instead of statically binding the routes to functions, you can bind the functions in a dispatch table like this:

ROUTES = { '/': show_entries, '/add': add_entry, ... }

This simple change decouples show_entries from a Flask app, allowing it to be used elsewhere without the need to load Flask.

When you want to use Flask, use the add_url_rule method:

import my_module app = Flask(__name__) for url in my_module.ROUTES: app.add_url_rule(url, view_func=my_module.ROUTES[url])

This simple change also improves cohesion: Flask routing is co-located with Flask app management.

Dispatch Improves Security

You may be wondering about how to implement dispatch with the @requires_auth decorator in the first example, which I'll repeat here:

@requires_auth def api_hello(): return "Shhh this is top secret spy stuff!"

One problem with this way of ensure authentication is that the default is "no auth". If you leave off the @requires_auth decorator, the web framework will not do any authentication or authorization. Anybody can execute api_hello. That's not a good default behavior.

Let's change the dispatch table a bit:

ROUTES = { '/': (show_entries, ANYBODY), '/add': (add_entry, ADMIN), }

With this change, we are declaring roles required to access a particular function. Not only that, we have an overview of all the required authorizations at a glance. This can be very helpful for security audits.

When we register the endpoint, we can then do something like this:

for url in my_module.ROUTES: fn, role = my_module.ROUTES[url] app.add_url_rule(url, view_func=auth_wrapper(fn, role))

The function auth_wrapper ensures that role is valid, and returns a new function that wraps the view function fn with authorization code. All view functions are wrapped so there is no "default" security model. Authorization is required.

Experienced Pythonistas reading the above example may say, "that's just another way of decorating functions!" True. I've purposefully kept out of the weeds here. Dispatches should happen without wrapping functions, since they are framework specific. In Flask, I might use a signal to lookup the authorized role(s) in the dispatch table.

Dispatch with Patterns

In general, I don't like dispatch tables for URL routing. That's a complex subject, but the more general rule is that you should use pattern-matching for dispatching when you can.

Here's a less complex problem, implementing logging with a decorator, which will demonstrate the dispatch using regular expressions:

def delete_invoice(self, invoiceid, **options): # delete the object

This decorator causes a log message to be output when the method delete_invoice is called. Let's say that we want this type of logging only when debugging. Indeed, we may only want to debug the methods having to do with invoicing. To keep things simple, I'll assume that we have a way of hooking into the request processing. The before_request function might look like this:

def before_request(request, view_func): if LOG_REGEXP.search(view_func.__name__): _log(view_func, request)

Every time a request is called, we check the name of the view function. If it matches, _log is called with the view function and the request object. _log might introspect the arguments for the view function and extract those from the request or simply output the URL and/or POST arguments. The important thing is that you know that every function which matches LOG_REGEXP will be called without having to worry that someone missed adding a decorator.

Special Cases

There are certainly decorators that cannot be dispatched. The @classmethod and @staticmethod decorators are two obvious examples. These decorators define semantics about the functions themselves. Python needs to use decorators in this case to adjust the arguments of the calling function so that they get the correct object.

I would not call these decorators according to the Decorator Pattern: to extend functionality to existing function. They are syntactic sugar to simplify idiosyncrasies of Python's method dispatch mechanism.

There are certainly other special cases, and I'll be happy to hear about them. The vast majority of uses of decorators I've seen (thus far) would be better off defined in centralized dispatchers.

Via Rob 8/24/15