DRY Django URLs with Enums

DRY code is good code. Using named URL patterns in Django has always bothered me for the fact that if you decide to change the name of a particular URL you have to change it throughout your code in all of your {% url %} template tags or reverse function calls. There must be a better way! And of course there is: enter Enums, new in Python 3.4.

Enums

I’ve written about Enums before, in my post Tightening Up Python, but I thought I’d write about them again with specific use case. Enums are designed for cases where you have some set of values that are all related and you want to keep them together. They can automatically convert a variable from its simple value-type into a Enum type, which can be useful for validation of values; and you can iterate over Enum values in the order in which they are defined. We won’t use these features in this use case though.

URL Definitions

Creating Enums is straightforward. I’ll be using some example code from Dead Alert, my web site change monitoring service. First here are some of the Enums I’ve created.

@django_url_definition
class ManageUrl(enum.Enum):
    LIST = "manage"
    DETAIL = "managed-url-detail"
    ACTION = "managed-url-action"

    @classmethod
    def url_name(cls):
        return 'manage'


@django_url_definition
class AuthUrl(enum.Enum):
    SIGNUP = 'signup'
    LOGIN = 'login'
    LOGOUT = 'logout'

    @classmethod
    def url_name(cls):
        return 'auth'

These are fairly self explanatory, the first Enum contains URL names for managed (monitored) URLs, and the second contains URL names for authentication. The url_name method I’ll cover soon, the only other weird thing is the django_url_definition decorator.

Django’s do_not_call_in_templates attributes

When Django tries to render a variable in a template it first checks if the variable is callable, in which case it calls it and renders the result. If we pass in an Enum (or any class) into a template context and try to render one of its attributes (using something like {{ ManageUrl.list }}), Django evaluates this to {{ ManageUrl().list }}.

Enums can not be called without arguments (doing so raises a TypeError) so Django silently fails here (none of the rest of the expression is evaluated) and the result is an empty string.

If an object has an attribute do_not_call_in_templates set to True, then Django does not treat the object as callable and thus the code evaluates as we expect. The django_url_definition is implemented like so:

def django_url_definition(cls):
    cls.do_not_call_in_templates = True
    return cls

Why not just add a do_not_call_in_templates attribute in the Enum class definition? Since it’s an Enum this would be determined to be one of its possible values. We don’t want this, so it is added at run time with a decorator instead.

Usages in URL Patterns

Using the Enum values in the URL pattern definition is simple, just import the enum and place into the pattern list.

from sitemonitor.url_definitions import ManageUrl, AuthUrl

...

urlpatterns = [
    ...
    url(r'^manage/$', login_required(views.ManageListView.as_view()), name=ManageUrl.LIST.value),
    ...
    url(r'^accounts/signup/$', views.SignupView.as_view(), name=AuthUrl.SIGNUP.value),
    ...
]

Remember that the actual string value is accessed with the Enum’s value attribute.

Usage in Templates

The DRY concept is not very useful if the values aren’t readily available, so using a context processor allows them to be inserted into each template automatically. Here’s the code that does it:

from sitemonitor.url_definitions import ManageUrl, AuthUrl

def url_enums_processor(request):
    return {
        'url_names':
            {url_enum.url_name(): url_enum for url_enum in (ManageUrl, AuthUrl)}
    }

The context processor needs to be added in settings.py as well.

Here you can see the url_name class method mentioned earlier. This is used to identify the Enum with a simple identifier for use in the templates. Using the named URL in the template can be done like this:

<a href="{% url url_names.manage.DETAIL.value "date" "new" %}" class="btn btn-success">
    <span class="glyphicon glyphicon-plus"></span> Add Date URL to monitor&hellip;
</a>

Conclusion

Being DRY isn’t always about making it easier to update the definitions throughout the code if you decide to change the name of a URL —that’s probably unlikely to happen. There are also advantages with your IDE being able to assist you in making sure you’ve typed the right thing (or even autocompleting it for you).

Do you want expert help with Python or Django development? Please contact for more information or to discuss further.

Couldn’t you accomplish the same thing with normal classes, in which case you could skip the do_not_call_in_templates attribute setting and you wouldn’t have to append .value to everything, and you’d still get the IDE/autocomplete benefits?

Yes. Aren’t Enums so new and shiny though?

Previous entry

Related entries

Tightening Up Python