Planet London Python

October 22, 2014

Harry Percival

Decorators!

Someone recently wrote to me asking about decorators, and saying they found them a bit confusing. Here's a post based on the email I replied to them with.

The best way to understand decorators is to build a couple of them, so here are two examples for you to try out. The first is in the Django world, the second is actually a simpler, pure-python one.

Challenge: build a decorator in a simple Django app

We've built a very basic todo lists app using Django. It has views to deal with viewing lists, creating new lists, and adding to existing lists. Two of these views end up doing some similar work, which is to retrieve a list object from the database based on its list ID:

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/%d/' % (list_.id,))


def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    return render(request, 'list.html', {'list': list_})

(Full code here)

This is a good use case for a decorator.

A decorator can be used to extract duplicated work, and also to change the arguments to a function. So we should be able to build a decorator that does the list-getting for us. Here's the target:

@get_list
def add_item(request, list_):
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/%d/' % (list_.id,))


@get_list
def view_list(request, list_):
    return render(request, 'list.html', {'list': list_})

So how do we build a decorator that does that? A decorator is a function that takes a function, and returns another function that does a slightly modified version of the work the original function was doing. We want our decorator to transform the simplified view functions we have above, into something that looks like the original functions.

(you end up saying "function" a lot in any explanation of decorators...)

Here's a template:

def get_list(view_fn):

    def decorated_view(...?):
        ???
        return view_fn(...?)

    return decorated_view

Can you get it working? Thankfully, our code has tests, so they'll tell you when you get it right...

git clone -b chapter_06 https://github.com/hjwp/book-example
python3 manage.py test lists # dependencies: django 1.7

Some rules of thumb for decorators:

  • they usually contain an "internal" function definition, which ends up being what the decorator returns.
  • that internal function usually calls the original function.
  • that internal function also needs to return something.

Decorators definitely are a bit brain-melting, so it may take a bit of effort to wrap your head around it. Once you get the hang of them, they're dead useful though,

A simpler decorator challenge:

If you're finding it impossible, you could start with a simpler challenge... say, building a decorator to make functions return an absolute value:

def absolute(fn):
    # this decorator currently does nothing
    def modified_fn(x):
        return fn(x)
    return modified_fn


def foo(x):
    return 1 - x

assert foo(3) == -2


@absolute
def foo(x):
    return 1 - x

assert foo(3) == 2  # this will fail, get is passing!

Try it out:

git clone https://gist.github.com/2cc523b66d9c0fe41c4b.git deccy
python3 deccy/deccy.py

Enjoy!

[update 2014-10-23 at 3pm, see also @baroque, the decorating decorator decorator]

by Harry at October 22, 2014 11:00 PM

October 20, 2014

Peter Bengtsson

django-html-validator

In action
A couple of weeks ago we had accidentally broken our production server (for a particular report) because of broken HTML. It was an unclosed tag which rendered everything after that tag to just plain white. Our comprehensive test suite failed to notice it because it didn't look at details like that. And when it was tested manually we simply missed the conditional situation when it was caused. Neither good excuses. So it got me thinking how can we incorporate HTML (html5 in particular) validation into our test suite.

So I wrote a little gist and used it a bit on a couple of projects and was quite pleased with the results. But I thought this might be something worthwhile to keep around for future projects or for other people who can't just copy-n-paste a gist.

With that in mind I put together a little package with a README and a setup.py and now you can use it too.

There are however some caveats. Especially if you intend to run it as part of your test suite.

Caveat number 1

You can't flood htmlvalidator.nu. Well, you can I guess. It would be really evil of you and kittens will die. If you have a test suite that does things like response = self.client.get(reverse('myapp:myview')) and there are many tests you might be causing an obscene amount of HTTP traffic to them. Which brings us on to...

Caveat number 2

The htmlvalidator.nu site is written in Java and it's open source. You can basically download their validator and point django-html-validator to it locally. Basically the way it works is java -jar vnu.jar myfile.html. However, it's slow. Like really slow. It takes about 2 seconds to run just one modest HTML file. So, you need to be patient.

October 20, 2014 04:48 AM