lost-theory

The blog of Steven Kryskalla.


 

Archive for March 2010

Spec runner using withhacks

written by stevek, on Mar 13, 2010 2:16 PM.

Here is an interesting video (and blog post) on Ruby vs. Python by Gary Bernhardt. One of the advantages Gary gives to Ruby is the ability to develop and use tools like rspec or cucumber, which use Ruby's block syntax for really nice looking unit tests, runnable specs, etc.

In the talk Gary shows that he was able to create a spec runner with syntax similar to rspec by using "really nasty" and "ugly" hacks (sys.settrace I believe). But, even if these techniques are ugly, they are becoming more easily accessible and more easy to develop with tools like withhacks, which abstracts the ugliness away.

I'm sure this does much less than what Gary's mote does, but here is a simple spec runner using withhacks:

from __future__ import with_statement

from withhacks import CaptureOrderedLocals, CaptureBytecode

class specs(CaptureOrderedLocals,CaptureBytecode):
    def __init__(self,what, *args, **kwargs):
        self.__what = what
        self.__args = args
        self.__kwargs = kwargs
        self.results = []
        super(specs,self).__init__()

    def __exit__(self,*args):
        retcode = super(specs,self).__exit__(*args)
        results = self.run_specs(self.locals)
        self.results = results

    def run_specs(self, cases):
        results = []
        num_pass, num_fail = 0,0
        print "Testing %s specs for %s:" % (len(cases), repr(self.__what))
        for (name, func) in cases:
            if not callable(func): continue
            name = name.replace('_', ' ')
            name = name.capitalize() + '.'
            print "->",
            try:
                func()
                error = None
                num_pass += 1
            except BaseException, e:
                error = repr(e)
                num_fail += 1
            if error:
                print "[FAIL]", name
                print "--->", error
                results.append((name, False, error))
            else:
                print "[pass]", name
                results.append((name, True, None))
        print "Result: %s/%s passed, %s/%s failed" % (num_pass, len(cases), num_fail, len(cases))
        print "-"*20
        return results

And here is an example spec:

class MyClass(object):
    def add(self, a, b):
        return a+b

with specs(MyClass):
    def it_adds_two_and_two():
        c = MyClass()
        assert c.add(2,2) == 4

    def it_adds_negatives():
        c = MyClass()
        assert c.add(10,-10) == 0

    def it_fails_adding_int_and_string():
        c = MyClass()
        try:
            c.add(10, 'foo')
        except TypeError:
            pass #correct!

    def testing_what_a_spec_failure_looks_like():
        c = MyClass()
        c.thisdoesntexist()

And here is the output:

Testing 4 specs for <class '__main__.MyClass'>:
-> [pass] It adds two and two.
-> [pass] It adds negatives.
-> [pass] It fails adding int and string.
-> [FAIL] Testing what a spec failure looks like.
---> AttributeError("'MyClass' object has no attribute 'thisdoesntexist'",)
Result: 3/4 passed, 1/4 failed
--------------------

I put the code on bitbucket here.

Decorator for preventing recursion

written by stevek, on Mar 9, 2010 7:53 AM.

Here's a decorator that will prevent a recursive function from calling itself:

def norecursion(default=None):
    '''Prevents recursion into the wrapped function.'''
    def entangle(f):
        def inner(*args, **kwds):
            if not hasattr(f, 'callcount'):
                f.callcount = 0
            if f.callcount >= 1:
                print "recursion detected %s calls deep. exiting." % f.callcount
                return default
            else:
                f.callcount += 1
                x = f(*args, **kwds)
                f.callcount -= 1
                return x
        return inner
    return entangle

It's based on this recipe. The function in that recipe relies on keeping track of which arguments were passed into the function, which means that it could not work on a function without any arguments. The decorator above works by attaching an attribute to the wrapped function for keeping track of how many calls have been made and exiting when the number of nested calls goes above a certain number.

Here's how you use it:

@norecursion(default=1)
def fact(x):
  if x <= 1:
    return 1
  else:
    return x*fact(x-1)
Now when you call fact it won't make the recursive call, instead it will return the default value of 1:
>>> fact(0)
1
>>> fact(1)
1
>>> fact(2)
recursion detected 1 calls deep. exiting.
2
>>> fact(3)
recursion detected 1 calls deep. exiting.
3

Why I needed this: I have a function on a Jinja2 template which builds a list of all pages and their metadata (a bunch of variables defined at the top of the template). Let's say I use the function on index.html. When it iterates over all the pages, it comes to index.html and then tries to get the list of all pages again. This causes the infinite recursion. On the second call deep, I don't need the whole page list, I only need the template metadata, so I can safely wrap the function in @norecursion(default=[]) to prevent it from running subsequent times.

Note that this is probably not threadsafe, I think there would need to be a lock around where the increment-call-decrement part happens.

Writing Mercurial plugins

written by stevek, on Mar 1, 2010 10:44 PM.

Getting my feet wet with writing some Mercurial plugins... First glance is that the API is very low-level, but I guess that makes sense since HG (and its plugins) have to be low-level to perform well.
#!/usr/bin/env python

from mercurial import hg
from binascii import hexlify
from mercurial import util

def interact(ui, repo, **opts):
    """poke around the mercurial API for this repo in a python interpreter"""
    print "Locals are:", dir()
    import code; code.interact(local=locals())

def short_incoming(ui, repo, **opts):
    """Shows a shortened form of 'hg incoming'"""
    default = hg.repository(ui, ui.expandpath('default'))
    inc = repo.findincoming(default)
    nodes = default.changelog.nodesbetween(inc, None)[0]
    for node in nodes:
        cs = default.changelog.read(node)
        print hexlify(cs[0])[:6], '|', cs[1], '|', util.datestr(cs[2]), \
              '|', len(cs[3]), 'files', '|', cs[5], '|', cs[4]

cmdtable = {
    "interact": (
        interact,
        [],
        interact.__doc__
    ),
    "short": (
        short_incoming,
        [],
        short_incoming.__doc__
    ),
}
Part of me just wants to scrape the text of the different subcommands.