An Answer That Most Students Won't Understand

This post originally appeared on the Software Carpentry website.

Two days ago, I asked how to generates tests from tables of fixtures using Nose:

...does Nose already have a tool for running through a table of fixtures and expected results? My hand-rolled version is:

Tests = (
    #  R1                 R2                  Expected
    ( ((0, 0), (0, 0)),   ((0, 0), (0, 0)),   None ),
    ( ((0, 0), (0, 0)),   ((0, 0), (1, 1)),   None ),
    ( ((0, 0), (1, 1)),   ((0, 0), (1, 1)),   ((0, 0), (1, 1)) ),
    ( ((0, 3), (2, 5)),   ((1, 0), (2, 4)),   ((1, 3), (2, 4)) )
)

def test_table():
    for (R1, R2, expected) in Tests:
        yield run_it, R1, R2, expected

def run_it(R1, R2, expected):
    assert overlap(R1, R2) == expected

which is simple enough if students already understand generators and function application, but hell to explain if they don't—and they won't.

After some back and forth, Jacob Kaplan-Moss (of Django fame) came up with this:

def tabletest(table):
    def decorator(func):
        def _inner():
            for args in table:
                yield tuple([func] + list(args))
        _inner.__name__ = 'test_'+func.__name__
        return _inner
    return decorator

table = [(1, 2), (3, 4)]

@tabletest(table)
def check_pair(left, right):
    assert left > right

The outer function tabletest takes the table of fixtures as an argument, and produces a function of one argument. That argument is supposed to be the function that is being wrapped up by the decorator, so:

@tabletest(table)
def check_pair(...):
    ...

means:

decorator = tabletest(table)
check_pair = ...what the 'def' creates...
check_pair = decorator(check_pair)

With me so far? Now, what decorator does is take a function F as an argument, and create a new function F' that produces each combination of the original F with the entries in the table: in jargon, it creates a generator that yields F and the arguments that F should be applied to.

But what's that inner_.__name__ stuff? That's to make sure that the wrapped function's name starts with the letters "test_", because that's how Nose knows to run it.

This does exactly what I wanted, but sparks three comments:

  1. Thanks, Jacob: I can understand the solution once it's in front of me, but it would have taken me a long time to figure this out myself.
  2. Treating programs as data, i.e., manipulating code just as you'd manipulate arrays or strings, is incredibly powerful.
  3. Only a tiny fraction of the students who complete this course will understand how this works. I'm sure they all could, if they wanted to invest the time, but given their usual starting point, they'd have to invest a lot of time.

#3 is what many advocates of new technology (functional languages! GPUs! functional languages on GPUs!) consistently overlook. What Jacob did here is really quite elegant, but in the same way that the classic proof of Euler's theorem is elegant: you have to know quite a lot to understand it, and even more to understand its grace. People who have that understanding often forget what the world looks like to people who don't; we're trying hard not to, and would be grateful if readers and viewers could tell us when we slip up.

Dialogue & Discussion

Comments must follow our Code of Conduct.

Edit this page on Github