How to strip decorators from a function in Python
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.
Related videos on Youtube
Herge
Updated on September 20, 2021Comments
-
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 almost 15 yearsIf you going to modify the code to call
_original
you might as well comment off the decorator. -
conspicillatus almost 15 years
func_closure
is being replaced by__closure__
in 3.x and it's already in 2.6 -
Herge almost 15 yearsI 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
untilcell_contents is None
. I was hoping for a more elegant solution. -
Sparr over 8 yearsIs 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 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 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 thaty = foo(y)
is syntactically equivalent to@foo\ndef y():
. If you tried your suggestion, you end up withinclude_original(decorator_i_dont_control(function))
, when what you want isinclude_original(decorator_i_dont_control)(function)
-
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 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 about 7 yearsProbably won't work, if the decorator uses functools.wraps
-
funk over 6 yearsPython3 for the win!
-
Romuald Brunet over 6 yearsCame up with the same solution, kudos ^^ @EvgeniiPuchkaryov it seems to work with functools.wrap
-
Aran-Fey about 6 yearsThis is untrue. The decorated function only has a
__wrapped__
attribute if you decorate it withfunctools.wraps
. Also, the link is dead. -
Alex Volkov about 6 yearsI fixed link to the book and expanded the answer for the cases when implementing own decorator.
-
Erik Kalkoken over 4 yearsThis 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 about 3 yearsThis is out of date for Python 3. See the answer below: stackoverflow.com/a/33024739/13969