How to remove decorators from a function in python

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 missing a connection setup problem (or whatever the decorator does).

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

+42
python decorator
Jul 22 '09 at 15:29
source share
8 answers

In general, you cannot, because

 @with_connection def spam(connection): # Do something 

equivalently

 def spam(connection): # Do something spam = with_connection(spam) 

which means that the "original" spam may not even exist. A (not too pretty) hack will be as follows:

 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 
+29
Jul 22 '09 at 15:42
source share

Balpha's solution can be made more generalized 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 each of them will have a testable (not decorated) version hidden 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 
+26
Jul 22 '09 at 18:31
source share

There were few updates for this question. If you are using Python 3, you can use the __wrapped__ property, which returns a wrapped function.

Here is an example from the Python Cookbook, 3rd Edition

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

See the discussion for a more detailed use of this attribute.

+15
Oct 08 '15 at 19:48
source share

Here, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

  orig_spam = spam.func_closure[0].cell_contents 

Change For functions / methods decorated more than once and more complex decorators, you can try using the following code. He relies on the fact that the decorated __name__d functions are different from the original.

 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> 

This is not stupid proof. This will fail if the name of the function returned from the decorator is the same as the decorated one. The hasattr () check order is also heuristic; there are trim chains that return incorrect results anyway.

+14
Jul 22 '09 at 15:45
source share

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 it.

 def with_connection(f): .... def spam_f(connection): ... spam = with_connection(spam_f) 

.. which is the syntax of @decorator - you can obviously access the original spam_f usually

+6
Jul 23 '09 at 1:51
source share

Now you can use the undecorated package:

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

He goes through hassles in all layers of different decorators, until he reaches the bottom function and does not require changing the original decorators. Works on both python2 and python3.

+3
Feb 15 '16 at 20:38
source share

The usual approach to testing such functions is to create any dependencies, such as get_connection, that are custom. Then you can override it with the layout during testing. This is basically the same as dependency injection in the Java world, but much simpler due to the dynamic nature of Pythons.

The 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 might find a better object than a decorator to enable the factory function. The problem with having a decorator is that you have to remember to restore it to its old value in the teardown method.

+2
Jul 22 '09 at 21:43
source share

Add a do-nothing decorator:

 def do_nothing(f): return f 

After defining or importing with_connection, but before you move on to methods using it as a decorator, add:

 if TESTING: with_connection = do_nothing 

Then, if you set the global TEST to True, you replace the_connection with the do-nothing decorator.

+1
Sep 19 '09 at 8:23
source share



All Articles