Werkzeug

wsgi utility collection


Using named rules with Werkzeug's routing

NB! This requires at least Werkzeug-0.6

Here's a quick extension to use 'named' rules with Werkzeug's routing.

First, let me explain which problem I'm solving. In a typical app, you define a bunch of rules. Taking the example from Werkzeug's documnentation:

url_map = Map([
    Rule('/', endpoint='blog/index'),
    Rule('/<int:year>/', endpoint='blog/archive'),
    ...
])

For this to work, you'd have to setup a map that points 'blog/index' and 'blog/archive' to their respective handlers. For example:

    handler_map = {
        'blog/index': 'views.blog:index_handler',
        'blog/archive': 'views.blog:archive_handler',
        ...
    }

    urls = url_map.bind_to_environ(environ)
    endpoint, args = urls.match()
    handler = import_string(handler_map[endpoint])

The problem here is that you need to maintain a separate map. This information could be attached to the rule. A simpler way is to set the endpoint as the handler to be imported. This would not require the extra map:

url_map = Map([
    Rule('/', endpoint='views.blog:index_handler'),
    Rule('/<int:year>/', endpoint='views.blog:archive_handler'),
    ...
])

    urls = url_map.bind_to_environ(environ)
    endpoint, args = urls.match()
    handler = import_string(endpoint)

No need for extra map, but the endpoints can become lengthy, and not convenient to build url's. For example: url_for('views.blog:archive_handler', ...). Also, if a handler changes location you have to replace all occurrences of url_for() in templates and code.

Enter named rules

Now here is a more convenient solution to set the rules above:

url_map = Map([
    Rule('/', endpoint='blog/index', handler='views.blog:index_handler'),
    Rule('/<int:year>/', endpoint='blog/archive', handler='views.blog:archive_handler'),
    ...
])

    urls = url_map.bind_to_environ(environ)
    rule, args = urls.match(return_rule=True)
    handler = import_string(rule.handler)

Url's are built using user friendly endpoint values like 'blog/index', which is more or less a static value. If the handler changes location, only the handler value in the rule need to be changed. And no extra map is required for the handlers definitions.

How? Simply extend Werkzeug's Rule a lil bit, and use it instead:

from werkzeug.routing import Rule as WerkzeugRule

class Rule(WerkzeugRule):
    """Extends Werkzeug routing to support named routes. Names are the url
    identifiers that don't change, or should not change so often. If the map
    changes when using names, all url_for() calls remain the same.

    The endpoint in each rule becomes the 'name' and a new keyword argument
    'handler' defines the class it maps to. To get the handler, set
    return_rule=True when calling MapAdapter.match(), then access rule.handler.
    """
    def __init__(self, *args, **kwargs):
        self.handler = kwargs.pop('handler', kwargs.get('endpoint', None))
        WerkzeugRule.__init__(self, *args, **kwargs)

    def empty(self):
        """Return an unbound copy of this rule.  This can be useful if you
        want to reuse an already bound URL for another map."""
        defaults = None
        if self.defaults is not None:
            defaults = dict(self.defaults)
        return Rule(self.rule, defaults, self.subdomain, self.methods,
                    self.build_only, self.endpoint, self.strict_slashes,
                    self.redirect_to, handler=self.handler)