How to strip decorators from a function in Python
Asked Answered
S

10

97

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?

Sherleysherline answered 22/7, 2009 at 15:29 Comment(0)
C
57

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
Chronic answered 22/7, 2009 at 15:42 Comment(2)
If you going to modify the code to call _original you might as well comment off the decorator.Weekley
This is out of date for Python 3. See the answer below: https://mcmap.net/q/217156/-how-to-strip-decorators-from-a-function-in-pythonBristle
C
104

There's been a bit of an update for this question. If you're using Python 3, and @functools.wraps you can use __wrapped__ property for decorators from stdlib.

Here's an example from Python Cookbook, 3rd edition, section 9.3 Unwrapping decorators

>>> @somedecorator
>>> def add(x, y):
...     return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>

If you are trying to unwrap a function from custom decorator, the decorator function needs to use wraps function from functools See discussion in Python Cookbook, 3rd edition, section 9.2 Preserving function metadata when writing decorators

>>> from functools import wraps
>>> def somedecorator(func):
...    @wraps(func)
...    def wrapper(*args, **kwargs):
...       # decorator implementation here
...       # ......
...       return func(*args, **kwargs)
...
...    return wrapper
Convexoconcave answered 8/10, 2015 at 19:48 Comment(3)
This is untrue. The decorated function only has a __wrapped__ attribute if you decorate it with functools.wraps. Also, the link is dead.Jellaba
I fixed link to the book and expanded the answer for the cases when implementing own decorator.Convexoconcave
This worked for me. Additional info: If your function has multiple decorators you can chain multiple .__wrapped__ to get to the original function.Jacobian
C
57

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
Chronic answered 22/7, 2009 at 15:42 Comment(2)
If you going to modify the code to call _original you might as well comment off the decorator.Weekley
This is out of date for Python 3. See the answer below: https://mcmap.net/q/217156/-how-to-strip-decorators-from-a-function-in-pythonBristle
O
34

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

def include_original(dec):
    def meta_decorator(f):
        decorated = dec(f)
        decorated._original = f
        return decorated
    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.

@include_original
def shout(f):
    def _():
        string = f()
        return string.upper()
    return _



@shout
def function():
    return "hello world"

>>> print function()
HELLO_WORLD
>>> print function._original()
hello world
Olmsted answered 22/7, 2009 at 18:31 Comment(5)
Is there a way to extend this so that the deepest level original is accessible at the outermost decorated function, so I don't have to do ._original._original._original for a function wrapped in three decorators?Bruiser
@Olmsted What exactly does decorate your decorators mean? Can I do something like \@include_original (next line) \@decorator_which_I_dont_control (next line) function_definition ?Pane
@Harshdeep: You'd want to do something like now_i_control = include_original(decorator_i_dont_control), and then decorate your function with @now_i_control\ndef function():. Note that y = foo(y) is syntactically equivalent to @foo\ndef y():. If you tried your suggestion, you end up with include_original(decorator_i_dont_control(function)), when what you want is include_original(decorator_i_dont_control)(function)Olmsted
@Pane I just edited my response with example usage. Again, if you didn't define the decorator yourself, you can wrap it with decorator = include_original(decorator)Olmsted
@Olmsted Thanks :) .. I too arrived at similar solution def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decoratedPane
E
20

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.

Elmaelmajian answered 22/7, 2009 at 15:45 Comment(4)
func_closure is being replaced by __closure__ in 3.x and it's already in 2.6Ciri
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.Sherleysherline
Probably won't work, if the decorator uses functools.wrapsBamford
Came up with the same solution, kudos ^^ @EvgeniiPuchkaryov it seems to work with functools.wrapTripodic
V
14

You can now use the undecorated package:

>>> from undecorated import undecorated
>>> undecorated(spam)

It goes through the hassle of digging through all the layers of different decorators until it reaches the bottom function and doesn't require changing the original decorators. It works on both Python 2 and Python 3.

Venegas answered 15/2, 2016 at 20:38 Comment(2)
this is exactly what I've been looking for!Regarding
Epic! needed this aswell.Toothlike
J
14

It's good practice to decorate decorators with functools.wraps like so:

import functools

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

@with_connection
def spam(connection):
    # Do something

As of Python 3.2, this will automatically add a __wrapped__ attribute that lets you retrieve the original, undecorated function:

>>> spam.__wrapped__
<function spam at 0x7fe4e6dfc048>

However, instead of manually accessing the __wrapped__ attribute, it's better to use inspect.unwrap:

>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>
Jellaba answered 23/4, 2018 at 10:3 Comment(2)
Why is it better to use inspect.unwrap versus calling __wrapped__?Aleut
@Aleut For the same reason why it's better to use len instead of __len__, str instead of __str__, and so on.Jellaba
C
6

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.

Coaction answered 23/7, 2009 at 1:51 Comment(2)
Nice approach, so clever!Eonian
The whole point of using a decorator is reusing it simply. This doesn't scale if I have code all over the place that needs the decorator.Kanal
C
5

the original function is stored in spam.__closure__[0].cell_contents.
Decorator uses closure to bind original function with extra layer of functionality. The original function must be stored in a closure cell kept by one of the functions in the nested structure of decorator.
Example:

>>> def add(f):
...     def _decorator(*args, **kargs):
...             print('hello_world')
...             return f(*args, **kargs)
...     return _decorator
... 
>>> @add
... def f(msg):
...     print('f ==>', msg)
... 
>>> f('alice')
hello_world
f ==> alice
>>> f.__closure__[0].cell_contents
<function f at 0x7f5d205991e0>
>>> f.__closure__[0].cell_contents('alice')
f ==> alice

this is the core principle of undecorated, you could refer to the source code for more details.

Chirrup answered 24/5, 2018 at 11:39 Comment(0)
D
2

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.

Domiciliary answered 22/7, 2009 at 21:43 Comment(0)
N
1

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.

Neoclassicism answered 19/9, 2009 at 8:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.