Reputation: 575
I learnt from this stack_overflow_entry that in Python decorators are applied in the order they appear in the source.
So how will the following code snippet should behave?
@unittest.skip("Something no longer supported")
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
def test_this():
....
The first decorator (noted below) asks test runner to completely skip test_this()
@unittest.skip("Something no longer supported")
While the second decorator asks test runner to skip running test_this()
conditionally.
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
So does it mean that test_this
won't be run at all unless we put conditional skip decorator first?
Also, is there any way in Python to define dependent execution of decorators? e.g.
@skipIf("Something goes wrong")
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
@log
@send_email
def test_this():
....
The idea is to enable execution of @log
and @send_email
if @skipIf("Something goes wrong")
is true
.
Apologies if I am missing something very obvious.
Upvotes: 2
Views: 1747
Reputation: 366003
I think you may be missing a key point: a decorator is just a function that gets passed a function and returns a function.
So, these are identical:
@log
def test_this():
pass
def test_this():
pass
test_this = log(test_this)
And likewise:
@skip("blah")
def test_this():
pass
def test_this():
pass
test_this = skip("blah")(test_this)
Once you understand that, all of your questions become pretty simple.
First, yes, skip(…)
is being used to decorate skipIf(…)(test)
, so if it skips the thing it decorates, test
will never get called.
And the way to define the order in which decorators get called is to write them in the order you want them called.
If you want to do that dynamically, you'd do so by applying the decorators dynamically in the first place. For example:
for deco in sorted_list_of_decorators:
test = deco(test)
Also, is there any way in Python to define dependent execution of decorators?
No, they all get executed. More relevant to what you're asking, each decorator gets applied to the decorated function, not to the decorator.
But you can always just pass a decorator to a conditional decorator:
def decorate_if(cond, deco):
return deco if cond else lambda f: f
Then:
@skipIf("Something goes wrong")
@decorate_if(something_feature_enabled, log)
@decorate_if(something_feature_enabled, send_email)
def test_this():
pass
Simple, right?
Now, the log
and send_email
decorators will be applied if something_feature_enabled
is truthy; otherwise a decorator that doesn't decorate the function in any way and just returns it unchanged will get applied.
But what if you can't pass the decorator, because the function is already decorated? Well, if you define each decorator to expose the function it's wrapped, you can always unwrap it. If you always use functools.wraps
(which you generally should if you have no reason to do otherwise—and which you can easily emulate in this way even when you have such a reason), the wrapped function is always available as __wrapped__
. So, you can write a decorator that conditionally removes the outermost level of decoration easily:
def undecorate_if(cond):
def decorate(f):
return f.__unwrapped__ if cond else f
return decorate
And again, if you're trying to do this dynamically, you're probably going to be decorating dynamically. So, an easier solution is to just skip the decorator(s) you don't want by removing them from the decos
iterable before they get applied.
Upvotes: 2