2

Is there any way to check inside function f1 in my example if calling a function (here decorated or not_decorated) has a specific decorator (in code @out)? Is such information passed to a function?

def out(fun):
    def inner(*args, **kwargs):
        fun(*args, **kwargs)
    return inner

@out
def decorated():
    f1()

def not_decorated():
    f1()

def f1():
    if is_decorated_by_out: # here I want to check it
        print('I am')
    else:
        print('I am not')

decorated()
not_decorated()

Expected output:

I am
I am not
Mihai Chelaru
  • 7,614
  • 14
  • 45
  • 51
Kuba
  • 123
  • 6
  • You could add an attribute to the function `out()` returns whose existence could be checked in `f1()`. – martineau Dec 13 '18 at 19:49
  • 1
    Having a function try to inspect anything at all about the code calling it is generally a last resort - you should try to find other ways to get the necessary information into your function, or reconsider whether you really need it. – user2357112 Dec 13 '18 at 19:53
  • I changed `in` to `inner`. `in` is a reserved word so calling or returning `in` is not going to work – DeepSpace Dec 13 '18 at 19:54
  • Your decorator could, in theory, set an attribute on the function that it is decorating. The main problem is that a function can't reference itself (so you can't check if `f1` has that attribute inside of `f1`), you will need another decorator just for that. See https://stackoverflow.com/questions/5063607/is-there-a-generic-way-for-a-function-to-reference-itself – DeepSpace Dec 13 '18 at 19:58
  • 1
    "Decorating" a function does not in and of itself leave any trace. `@out def decoratd()...` is just syntactic sugar for `def decorated(): ...; decorated = out(decorated)`. – chepner Dec 13 '18 at 19:58
  • In this specific case, however, note that `decorated.__name__ == "inner"`, while `not_decorated.__name__ == "not_decorated"`. (Not that `f1` knows, absent the kind of introspection mentioned by user2357112, who its caller is.) – chepner Dec 13 '18 at 20:00

3 Answers3

2

To be clear, this is egregious hackery, so I don't recommend it, but since you've ruled out additional parameters, and f1 will be the same whether wrapped or not, you've left hacks as your only option. The solution is to add a local variable to the wrapper function for the sole purpose of being found by means of stack inspection:

import inspect

def out(fun):
    def inner(*args, **kwargs):
        __wrapped_by__ = out
        fun(*args, **kwargs)
    return inner

def is_wrapped_by(func):
    try:
        return inspect.currentframe().f_back.f_back.f_back.f_locals.get('__wrapped_by__') is func
    except AttributeError:
        return False

@out
def decorated():
    f1()

def not_decorated():
    f1()

def f1():
    if is_wrapped_by(out):
        print('I am')
    else:
        print('I am not')

decorated()
not_decorated()

Try it online!

This assumes a specific degree of nesting (the manual back-tracking via f_back to account for is_wrapped_by itself, f1, decorated and finally to inner (from out). If you want to determine if out was involved anywhere in the call stack, make is_wrapped_by loop until the stack is exhausted:

def is_wrapped_by(func):
    frame = None
    try:
        # Skip is_wrapped_by and caller 
        frame = inspect.currentframe().f_back.f_back
        while True:
            if frame.f_locals.get('__wrapped_by__') is func:
                return True
            frame = frame.f_back
    except AttributeError:
        pass
    finally:
        # Leaving frame on the call stack can cause cycle involving locals
        # which delays cleanup until cycle collector runs;
        # explicitly break cycle to save yourself the headache
        del frame
    return False
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
0

If you are open to creating an additional parameter in f1 (you could also use a default parameter), you can use functools.wraps and check for the existence of the __wrapped__ attribute. To do so, pass the wrapper function to f:

import functools

def out(fun):
  @functools.wraps(fun)
  def inner(*args, **kwargs):
     fun(*args, **kwargs)
  return inner

@out
def decorated():
  f1(decorated)

def not_decorated():
  f1(not_decorated)

def f1(_func):
  if getattr(_func, '__wrapped__', False):
    print('I am')
  else:
    print('I am not')

decorated()
not_decorated()

Output:

I am 
I am not
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • Good solution, but I can't create a new parameter in `f()`. This is a big obstacle. – Kuba Dec 13 '18 at 20:23
0

Suppose you have a function decoration like this one

def double_arg(fun):
    def inner(x):
        return fun(x*2)
    return inner

however you can't access it (it's inside a 3rd party lib or something). In this case you can wrap it into another function that adds the name of the decoration to the resulting function

def keep_decoration(decoration):
    def f(g):
        h = decoration(g)
        h.decorated_by = decoration.__name__
        return h
    return f

and replace the old decoration by the wrapper.

double_arg = keep_decoration(double_arg)

You can even write a helper function that checks whether a function is decorated or not.

def is_decorated_by(f, decoration_name):
    try:
        return f.decorated_by == decoration_name
    except AttributeError:
        return False

Example of use...

@double_arg
def inc_v1(x):
    return x + 1

def inc_v2(x):
    return x + 1

print(inc_v1(5))
print(inc_v2(5))

print(is_decorated_by(inc_v1, 'double_arg'))
print(is_decorated_by(inc_v2, 'double_arg'))

Output

11
6
True
False
Gabriel
  • 1,922
  • 2
  • 19
  • 37