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: {}