5

I am trying to build a control structure in a class method that takes a function as input and has different behaviors if a function is decorated or not. Any ideas on how you would go about building a function is_decorated that behaves as follows:

def dec(fun):
    # do decoration


def func(data):
    # do stuff


@dec
def func2(data):
    # do other stuff


def is_decorated(func):
    # return True if func has decorator, otherwise False


is_decorated(func)  # False
is_decorated(func2) # True
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    Decorators are just syntactic sugar for wrapper functions. – Barmar Jul 29 '21 at 23:47
  • @Barmar Okay, then a potential rephrase of 'how could you go about writing a ```is_decorated``` function that detects if a function has a wrapper function?' –  Jul 29 '21 at 23:49
  • This seems like an XY problem. Why do you need to treat decorated functions differently? – Barmar Jul 29 '21 at 23:52
  • 1
    You could check if `func.__closure__` is not None. A wrapped function is a closure. – Barmar Jul 29 '21 at 23:54
  • 1
    @Barmar my decorator causes side effects that I want to manage –  Jul 29 '21 at 23:56
  • 1
    So you just want to detect *your* decorator, not all decorators in general? – Barmar Jul 29 '21 at 23:57
  • @Barmar Yes, but there may be more than one –  Jul 29 '21 at 23:58

3 Answers3

2

Yes, it's relatively easy because functions can have arbitrary attributes added to them, so the decorator function can add one when it does its thing:

def dec(fun):
    def wrapped(*args, **kwargs):
        pass

    wrapped.i_am_wrapped = True
    return wrapped

def func(data):
    ... # do stuff

@dec
def func2(data):
    ... # do other stuff

def is_decorated(func):
    return getattr(func, 'i_am_wrapped', False)


print(is_decorated(func))   # -> False
print(is_decorated(func2))  # -> True
martineau
  • 119,623
  • 25
  • 170
  • 301
  • This just also wraps manually the function with an additional property. You have to do this in all of your wrappers. My problem that this feels a tinkering instead of using any in-built potential of the language. If there is any of course. But eventually this is what `functools.wraps` provide isn't it? It just wraps this logic further...lol. – MattSom Oct 26 '22 at 13:58
1

There are several ways to identify if a function is wrapped/decorated.

TLDR Solution

def is_decorated(func):
    return hasattr(func, '__wrapped__') or func.__name__ not in globals()

Scenario A

If we assume the decorator uses the helper decorator functools.wraps, then it is pretty straight forward, since our decorated function is going to have the attribute __wrapped__ after.

from functools import wraps


def decorator(function):
    @wraps(function)
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator
def foo(x, y): ...


def is_decorated(function):
    return hasattr(function, '__wrapped__')

Scenario B

In case we don't use wraps during the decorator's definition, then it's a bit different.

def decorator(function):
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator
def foo(x, y): ...


def bar(x, y): ...


print(bar.__name__)  # prints 'bar'
print(foo.__name__)  # prints '_wrapper' instead of 'foo'


def is_decorated(function):
    """
    globals() returns a dictionary which includes
    defined functions, classes and many other stuff.
    Assuming we haven't defined the inner/wrapper name
    as an outer function elsewhere, then it will not be
    in globals()
    """
    return function.__name__ not in globals()

Conclusion

There are other - more sophisticated - ways to check this, however readability is as important as completeness is, and this solution is as simple it can be. As mentioned in the previous code block, the only "hole" of this solution is the following situation:

from functools import wraps

def is_decorated(func):
    return hasattr(func, '__wrapped__') or func.__name__ not in globals()


def decorator_wraps(function):
    @wraps(function)
    def _wrapper(*a, **kw): ...
    return _wrapper


def decorator_no_wraps(function):
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator_wraps
def foo(x, y): ...


@decorator_no_wraps
def bar(x, y): ...


def baz(x, y): ...


for function in (foo, bar, baz):
    print(is_decorated(function))  # True, True, False


def _wrapper(): ...


for function in (foo, bar, baz):
    print(is_decorated(function))  # True, False, False
Will Be
  • 89
  • 1
  • 7
-1

In general, you can't. There's nothing special about a decorator that leaves a trace. Per the excellent Primer on Python Decorators tutorial, "Decorators provide a simple syntax for calling higher-order functions."

A quick illustrative example from the tutorial:

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice
from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")

You see how wrapper_do_twice inside do_twice is literally just calling the passed function twice? It in no way modifies the function, so it's impossible to tell (without inspection, which, don't do that) that the function was decorated.

However, if you are writing your own decorator, or otherwise have knowledge that you can exploit that the decorator you are using is modifying the decorated function (in contrast to the example above, which does not modify the function), then you can just check for whatever marker(s) are left directly. See this question for an example.

thisisrandy
  • 2,660
  • 2
  • 12
  • 25