How to strip decorators from a function in Python

28,656

Solution 1

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

Solution 2

There's been a bit of an update for this question. If you're using Python 3, 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

Solution 3

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

Solution 4

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.

Solution 5

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.

Share:
28,656

Related videos on Youtube

Herge
Author by

Herge

Updated on September 20, 2021

Comments

  • Herge
    Herge almost 3 years

    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?

  • Ehab Developer
    Ehab Developer almost 15 years
    If you going to modify the code to call _original you might as well comment off the decorator.
  • conspicillatus
    conspicillatus almost 15 years
    func_closure is being replaced by __closure__ in 3.x and it's already in 2.6
  • Herge
    Herge almost 15 years
    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.
  • Sparr
    Sparr over 8 years
    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?
  • Harshdeep
    Harshdeep about 8 years
    @jcdyer 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 ?
  • jcdyer
    jcdyer about 8 years
    @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)
  • jcdyer
    jcdyer about 8 years
    @Harshdeep 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)
  • Harshdeep
    Harshdeep about 8 years
    @jcdyer 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 decorated
  • Evgen
    Evgen about 7 years
    Probably won't work, if the decorator uses functools.wraps
  • funk
    funk over 6 years
    Python3 for the win!
  • Romuald Brunet
    Romuald Brunet over 6 years
    Came up with the same solution, kudos ^^ @EvgeniiPuchkaryov it seems to work with functools.wrap
  • Aran-Fey
    Aran-Fey about 6 years
    This is untrue. The decorated function only has a __wrapped__ attribute if you decorate it with functools.wraps. Also, the link is dead.
  • Alex Volkov
    Alex Volkov about 6 years
    I fixed link to the book and expanded the answer for the cases when implementing own decorator.
  • Erik Kalkoken
    Erik Kalkoken over 4 years
    This worked for me. Additional info: If your function has multiple decorators you can chain multiple .__wrapped__ to get to the original function.
  • Tom N Tech
    Tom N Tech about 3 years
    This is out of date for Python 3. See the answer below: stackoverflow.com/a/33024739/13969