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