Use closure for your Django context processors

09 May 2015   11 comments   Python, Django

Powered by Fusion×

The idea with template context processors in Django is to inject some defaults thing to be available when rendering a template that is rendered with a request.

I.e. instead of...:

def view1(request):
    context = {
        'name': 'View 1', 
        'on_dev_server': request.get_host() in settings.DEV_HOSTNAMES
    }
    return render(request, 'view1.html', context)

def view2(request):
    context = {
        'name': 'View 2', 
        'other': 'things', 
        'on_dev_server': request.get_host() in settings.DEV_HOSTNAMES
    }
    return render(request, 'view2.html', context)

And in your nominal templates/base.html you might have something like this:

  ...
  <footer>
  <p>&copy; You 2015</p>
  {% if on_dev_server %}
    <p color="red">Note! We're currently on a dev server!</p>
  {% endif %}
  </footer>
  ...

Instead you do this trick; in your settings.py you write down the list of defaults plus the one you want to always have available:

TEMPLATE_CONTEXT_PROCESSORS = (
    "django.contrib.auth.context_processors.auth",
    "django.template.context_processors.static",
    "myproject.myapp.context_processors.debug_info",
)

And to accompany that you define your myprojects/myapp/context_processors.py like so:

def debug_info(request):
    return {
        'on_dev_server': request.get_host() in settings.DEV_HOSTNAMES,
    }

So far so good.

However, there's a problem with this. Two problems in fact.

First problem is that when all the templates in your big complicated website renders, it's quite possible that some pages don't need everything you set up in your context processors. That might mean a heck of a lot of extra computation when it won't ever be displayed.

For example, I have a project where most pages have a sidebar where I show "Trending Events" which is something I compute in a context_processors.py function called def sidebar_events(request):. But the sidebar is not always shown and on the pages where it's not shown it's a waste to compute the stuff that sidebar_events computes. Also, I have management pages which uses a totally different base.html template. So there's a big chance you're wasting precious CPU.

Another problem is that of code-readability (aka. how frustrating is this to debug for someone else or yourself after months of idle activity). If you're skimming through your base.html and you see this "random" variable called on_dev_server it's very very hard to tell where the heck that's defined. Hopefully grepping the whole source code is a way to go. A much better way to solve that problem would be sensible namespace naming.

And also, by being too liberal with globally scoped variables there's a chance you might clash from a different piece of functionality that uses the same variable names. That chance is smaller when you use namespaces.

So, to remedy this, let your template context processor functions return closures. It wraps the request automagically.

Let's rewrite our trivial example from above, the context_processors.py should now look like this:

def debug_info(request):
    def inner():
        return {
            'on_dev_server': request.get_host() in settings.DEV_HOSTNAMES,
        }
    return {'debug_info': inner}

Now executing that becomes more optional and more deliberate in the template instead. E.g.

  ...
  <footer>
  <p>&copy; You 2015</p>
  {% set debug_info = debug_info() %}
  {% if debug_info['on_dev_server'] %}
    <p color="red">Note! We're currently on a dev server!</p>
  {% endif %}
  </footer>
  ...

This makes it more explicity which is a good thing. It also has the potential to be avoided if the stuff in there isn't needed in some templates.

Comments

Thomas
Is « {% set debug_info = debug_info() %} » legal in Django templates ?
Peter Bengtsson
You know what, that's Jinja syntax. Imagine the equivalent in Django templates :)
Anonymous
Wouldn't be better to implement this with templatetags? As you said, context processors are for inserting variables for all your templates so, instead of doing that, why not write a templatetag and call whenever you need it?
Peter Bengtsson
It's not a bad point but isn't templatetags (that take in the request for context) there to inject content into the template? With template context processors you get *information* which you then deliberately insert into the template when you will.
Anonymous
I get your point. I guess there is a thin line between when to use one or the other in these cases but in my opinion, template tags feel less hacky than using closures for context processors. Mb some1 else can give his opinion ^^.
Tyler Davis
This is the difference between a filter or simple_tag and an assignment_tag. Definitely agree that if performance is a consideration, this is more cleanly accomplished with an assignment tag.
Peter Bengtsson
I don't even know what an "assignment_tag" is.
I've been living in Jinja-land so long that maybe this is a newfangled feature in modern Django templates.
Emma
Even though it was already possible to write such a tag before assignment_tag is indeed a pretty new feature it's only 3 years old https://docs.djangoproject.com/en/1.8/releases/1.4/#assignment-template-tags
Luke Plant
An extra trick to use with this is to memoize the closure so that it is only called once if used multiple times. I use this decorator to convert closures into memoized ones:

    def memoize_nullary(f):
        """
        Memoizes a function that takes no arguments. The memoization lasts only as
        long as we hold a reference to the return value.
        """
        def func():
            if not hasattr(func, 'retval'):
                func.retval = f()
            return func.retval
        return func
Saverio Trioni
Why not a simple lazy object?

from django.utils.functional import SimpleLazyObject

def debug_info(request):
    return {
        'on_dev_server': SimpleLazyObject(lambda: request.get_host() in settings.DEV_HOSTNAMES),
    }

The lambda won't be executed until needed and then the result will be inserted in the object's dlegation chain instead of the lambda.

>>> s = SimpleLazyObject(lambda: True)
>>> s
<SimpleLazyObject: <function <lambda> at ...>>
>>> if s: print 8
...
8
>>> s
<SimpleLazyObject: True>
>>>

>>> s = SimpleLazyObject(lambda: False)
>>> s
<SimpleLazyObject: <function <lambda> at ...>>
>>> if s: print 8
...
>>> s
<SimpleLazyObject: False>
>>>
Thank you for posting a comment

Your email will never ever be published


Related posts

Previous:
Using lazy loading images on Air Mozilla 23 April 2015
Next:
premailer 2.9.0 and new rules for `base_url` 11 May 2015
Related by Keyword:
Comparing Google Closure with UglifyJS 10 July 2011
Nasty JavaScript wart (or rather, don't take shortcuts) 18 October 2010
The awesomest way possible to serve your static stuff in Django with Nginx 24 March 2010
Related by Text:
Really simple Django view function timer decorator 08 December 2017
How to use django-cache-memoize 03 November 2017
How to unit test the innards of a Django view function 15 November 2008
Custom CacheMiddleware that tells Javascript a page is cached in Django 24 August 2009
mincss "Clears the junk out of your CSS" 21 January 2013