views:

348

answers:

6

Let's say I have the following:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    return decorated

@with_connection
def spam(connection):
    # Do something

I want to test the spam function without going through the hassle of setting up a connection (or whatever the decorator is doing).

Given spam, how do I strip the decorator from it and get the underlying "undecorated" function?

+4  A: 

In the general case, you can't, because

@with_connection
def spam(connection):
    # Do something

is equivalent to

def spam(connection):
    # Do something

spam = with_connection(spam)

which means that the "original" spam might not even exist anymore. A (not too pretty) hack would be this:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    decorated._original = f
    return decorated

@with_connection
def spam(connection):
    # Do something

spam._original(testcon) # calls the undecorated function
balpha
If you going to modify the code to call `_original` you might as well comment off the decorator.
eduffy
@eduffy: That's the point of the question.
balpha
You're right .. I didn't think about it in a testing sense.
eduffy
+2  A: 

Behold, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

 orig_spam = spam.func_closure[0].cell_contents

Edit: For functions/methods decorated more than once and with more complicated decorators you can try using the following code. It relies on the fact, that decorated functions are __name__d differently than the original function.

def search_for_orig(decorated, orig_name):
    for obj in (c.cell_contents for c in decorated.__closure__):
     if hasattr(obj, "__name__") and obj.__name__ == orig_name:
      return obj
     if hasattr(obj, "__closure__") and obj.__closure__:
      found = search_for_orig(obj, orig_name)
      if found:
       return found
    return None

 >>> search_for_orig(spam, "spam")
 <function spam at 0x027ACD70>

It's not fool proof though. It will fail if the name of the function returned from a decorator is the same as the decorated one. The order of hasattr() checks is also a heuristic, there are decoration chains that return wrong results in any case.

wuub
`func_closure` is being replaced by `__closure__` in 3.x and it's already in 2.6
Ivan Baldin
I saw that when I was playing around with functions, but it sort of gets complicated if you are using more than one decorator on a function. You wind up calling `.func_closure[0].cell_contents` until `cell_contents is None`. I was hoping for a more elegant solution.
Herge
+1  A: 

balpha's solution can be made more generalizable with this meta-decorator:

def include_original(dec):
    def meta_decorator(f):
        decorator = dec(f)
        decorator._original = f
        return decorator
    return meta_decorator

Then you can decorate your decorators with @include_original, and every one will have a testable (undecorated) version tucked away inside it.

jcdyer
+1  A: 

The usual approach to testing such functions is to make any dependencies, such as get_connection, configurable. Then you can override it with a mock while testing. Basically the same as dependency injection in the Java world but a lot simpler thanks to Pythons dynamic nature.

Code for it might look something like this:

# decorator definition
def with_connection(f):
    def decorated(*args, **kwargs):
        f(with_connection.connection_getter(), *args, **kwargs)
    return decorated

# normal configuration
with_connection.connection_getter = lambda: get_connection(...)

# inside testsuite setup override it
with_connection.connection_getter = lambda: "a mock connection"

Depending on your code you could find a better object than the decorator to stick the factory function on. The issue with having it on the decorator is that you'd have to remember to restore it to the old value in the teardown method.

Ants Aasma
A: 

Instead of doing..

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    return decorated

@with_connection
def spam(connection):
    # Do something

orig_spam = magic_hack_of_a_function(spam)

You could just do..

def with_connection(f):
    ....

def spam_f(connection):
    ...

spam = with_connection(spam_f)

..which is all the @decorator syntax does - you can then obviously access the original spam_f normally

dbr
A: 

Add a do-nothing decorator:

def do_nothing(f):
    return f

After defining or importing with_connection but before you get to the methods that use it as a decorator, add:

if TESTING:
    with_connection = do_nothing

Then if you set the global TESTING to True, you will have replaced with_connection with a do-nothing decorator.

Paul McGuire