How to detect is decorator has been applied to method or function?

You can use inspect.getargspec:

import inspect

def _is_method(func):
    spec = inspect.getargspec(func)
    return spec.args and spec.args[0] == 'self'

Example usage:

>>> def dummy_deco(f):
...     print('{} is method? {}'.format(f.__name__, _is_method(f)))
...     return f
... 
>>> @dummy_deco
... def add(a, b):
...     return a + b
... 
add is method? False
>>> class A:
...     @dummy_deco
...     def meth(self, a, b):
...         return a + b
... 
meth is method? True

NOTE This code depend on the name of the first argument. If the name is not self it will treat it as non-instance-method even though it is.


You can solve this problem using descriptor protocol. By returning non-data descriptor from decorator you get to implement __get__ where you can save the method's instance/class.

Another (simpler) way would be to detect instance/class late, in decorator-made wrapper which may have self or cls as first of *args. This improves "inspectability" of decorated function, as it's still a plain function and not a custom non-data-desctiptor/function-object.

Problem we have to solve is that we cannot hook into or before the method binding:

Note that the transformation from function object to (unbound or bound) method object happens each time the attribute is retrieved from the class or instance.

In other words: when our wrapper runs, its descriptor protocol, namely __get__ method-wrapper of function, has already bound function with class/instance and resulting method is already being executed. We're left with args/kwargs and no straightforwardly accessible class-related info in current stack frame.

Let's start with solving class/staticmethod special cases and implementing wrapper as simple printer:

def decorated(fun):
    desc = next((desc for desc in (staticmethod, classmethod)
                 if isinstance(fun, desc)), None)
    if desc:
        fun = fun.__func__

    @wraps(fun)
    def wrap(*args, **kwargs):
        cls, nonselfargs = _declassify(fun, args)
        clsname = cls.__name__ if cls else None
        print('class: %-10s func: %-15s args: %-10s kwargs: %-10s' %
              (clsname, fun.__name__, nonselfargs, kwargs))

    wrap.original = fun

    if desc:
        wrap = desc(wrap)
    return wrap

Here comes the tricky part - if this was a method/classmethod call, first of args must be instance/class respectively. If so, we can get the very method we execute from this arg. If so, wrapper which we implemented above will be inside as __func__. If so, original member will be in our wrapper. If it is identical to fun from closure, we're home and can slice instance/class safely from remaining args.

def _declassify(fun, args):
    if len(args):
        met = getattr(args[0], fun.__name__, None)
        if met:
            wrap = getattr(met, '__func__', None)
            if getattr(wrap, 'original', None) is fun:
                maybe_cls = args[0]
                cls = maybe_cls if isclass(maybe_cls) else maybe_cls.__class__
                return cls, args[1:]
    return None, args

Let's see if this works with different variants of functions/methods:

@decorated
def simplefun():
    pass

class Class(object):
    @decorated
    def __init__(self):
        pass

    @decorated
    def method(self, a, b):
        pass

    @decorated
    @staticmethod
    def staticmethod(a1, a2=None):
        pass

    @decorated
    @classmethod
    def classmethod(cls):
        pass

Let's see if this actually runs:

simplefun()
instance = Class()
instance.method(1, 2)
instance.staticmethod(a1=3)
instance.classmethod()
Class.staticmethod(a1=3)
Class.classmethod()

output:

$ python Example5.py 
class: None       func: simplefun       args: ()         kwargs: {}        
class: Class      func: __init__        args: ()         kwargs: {}        
class: Class      func: method          args: (1, 2)     kwargs: {}        
class: None       func: staticmethod    args: ()         kwargs: {'a1': 3} 
class: Class      func: classmethod     args: ()         kwargs: {}        
class: None       func: staticmethod    args: ()         kwargs: {'a1': 3} 
class: Class      func: classmethod     args: ()         kwargs: {}        

Thanks to this SO answer: Using the same decorator (with arguments) with functions and methods

I came to this solution witch works for me flawlessly:

def proofOfConcept():
    def wrapper(func):

        class MethodDecoratorAdapter(object):
            def __init__(self, func):
                self.func = func
                self.is_method = False

            def __get__(self, instance, owner):
                if not self.is_method:
                    self.is_method = True
                self.instance = instance

                return self

            def __call__(self, *args, **kwargs):
                # Decorator real logic goes here
                if self.is_method:
                    return self.func(self.instance, *args, **kwargs)
                else:
                    return self.func(*args, **kwargs)

        return wraps(func)(MethodDecoratorAdapter(func))

    return wrapper

NOTE This is not thread safe, to have a thread safe method one must return a callable object from __get__ that will have scope tied to instance

Tags:

Python