Search code examples
pythondependenciesdecoratorexecutionpython-decorators

Can python decorators be executed or skipped depending on an earlier decorator result?


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.


Solution

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