16

The goal is that I wan't to have one decorator that will work with both, function and instance methods, and I would like to retrieve within wrapping function the self object when decorator has been applied on method or the function object itself when applied to function.

Here's what I have found almost working, this is only the func I'm using to detect on what decorator has been applied:

def _is_method(func):
    for stack_frame in inspect.stack():
        # if the code_context of the stack frame starts with 'class' this
        # function is defined within a class and so a method.
        if inspect.getframeinfo(stack_frame[0]).code_context[0].strip().startswith('class'):
            return True
    return False

This does work for me, with one small exception, It throws exceptions when I run tests in parallel in multiple processes.

canni
  • 5,737
  • 9
  • 46
  • 68

6 Answers6

10

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: {}        
aurzenligl
  • 181
  • 1
  • 7
  • It's an interesting solution, but there is a problem. What if the first argument of a static function is a class instance which has a function with the same name? `class C: def staticmethod(self): pass` `Class.staticmethod(C()) # AttributeError: 'function' object has no attribute 'original'` – bartolo-otrit Nov 16 '18 at 07:50
  • 2
    I completely missed this case! I guess identity check `wrap.original is fun` wrongly assumes that if something is the first arg, has attribute as original fun `__name__` and has `__func__` attr, it's definitely the local `wrap` function with `original` attribute. It doesn't have to be - let's check for presence of `original` as well: `getattr(wrap, 'original', None) is fun`. – aurzenligl Nov 25 '18 at 23:17
  • 1
    One more answer which fails if to apply the same decorator twice. The outer decorator will work fine. But the inner decorator will not recognize if it decorated function or class's method. – Nairum Apr 24 '21 at 14:45
5

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.

falsetru
  • 357,413
  • 63
  • 732
  • 636
  • 2
    Thanks, looks like this could solve my issue, but I'm thinking like using `inspect` at all is not "elegant" approach, maybe there is some other way of doing this? – canni Oct 11 '13 at 12:26
  • 3
    just want to note, arg[0] is not always "self", especially when you use nested classes and have to avoid name shadowing causing other issues. I tend to also use "inst" or "this" – Tcll Jun 25 '19 at 15:02
5

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

Community
  • 1
  • 1
canni
  • 5,737
  • 9
  • 46
  • 68
1

Solution for python3:

import inspect

def _is_method(func):
    spec = inspect.signature(func)
    if len(spec.parameters) > 0:
        if list(spec.parameters.keys())[0] == 'self':
            return True
    return False
0
import functools
import inspect


def mydec():
    def decorator(func):
        @functools.wraps(func)
        def pickled_func(*args, **kwargs):
            is_method = False
            if len(args) > 0:
                method = getattr(args[0], func.__name__, False)
                if method:
                    wrapped = getattr(method, "__wrapped__", False)
                    if wrapped and wrapped == func:
                        print("Used mydec to a method")
                        is_method = True

            if is_method is False:
                print("Used mydec not to a method.")
            result = func(*args, **kwargs)
            return result
    return decorator

Check whether the __wrapped__ variable is the same function as decorated.

fx-kirin
  • 1,906
  • 1
  • 20
  • 33
0

My solution after taking a lot of inspiration from the answer of @canni. And from @aurzenligl to deal with static and classmethods.

Edit: I also added support for decorating properties

I have this class that can be reused for creating multiple decorators:

class MethodCheck:
    def __init__(self, func) -> None:
        self.func = func

    def __get__(self, instance, owner):
        if isinstance(self.func, staticmethod):
            return lambda *args, **kwargs: self.on_staticmethod(self.func.__func__, *args, **kwargs)
        elif isinstance(self.func, classmethod):
            return lambda *args, **kwargs: self.on_classmethod(self.func.__func__, owner, *args, **kwargs)
        elif isinstance(self.func, property):
            return self.on_property(self.func, instance)
        return lambda *args, **kwargs: self.on_method(self.func, instance, *args, **kwargs)

    def __call__(self, *args, **kwargs):
        return self.on_function(self.func, *args, **kwargs)

    def on_method(self, func, instance, *args, **kwargs):
        return func(instance, *args, **kwargs)

    def on_property(self, func:property, instance, *args, **kwargs):
        return func.fget(instance, *args, **kwargs)

    def on_classmethod(self, func, cls, *args, **kwargs):
        return func(cls, *args, **kwargs)

    def on_staticmethod(self, func, *args, **kwargs):
        return func(*args, **kwargs)

    def on_function(self, func, *args, **kwargs):
        return func(*args, **kwargs)

The implementation of a decorator using the above class:

from functools import wraps

class MethodCheckTester(MethodCheck):
    def on_method(self, *args, **kwargs):
        print(f"Executing Method: ", end='')
        return super().on_method(*args, **kwargs)

    def on_property(self, *args, **kwargs):
        print(f"Accessing Property: ", end='')
        return super().on_property(*args, **kwargs)

    def on_classmethod(self, *args, **kwargs):
        print(f"Executing Class Method: ", end='')
        return super().on_classmethod(*args, **kwargs)

    def on_staticmethod(self, *args, **kwargs):
        print(f"Executing Static Method: ", end='')
        return super().on_staticmethod(*args, **kwargs)

    def on_function(self, *args, **kwargs):
        print(f"Executing Function: ", end='')
        return super().on_function(*args, **kwargs)


def decorator(func):
    return wraps(func)(MethodCheckTester(func))

Decorated test functions/methods:

class MyClass:
    @decorator
    def f1(self, arg1):
        print(arg1)

    @decorator
    @classmethod
    def f2(cls, arg1):
        print(arg1)

    @decorator
    @staticmethod
    def f3(arg1):
        print(arg1)

    @decorator
    @property
    def f5(self):
        return "f5"

@decorator
def f4(arg1):
    print(arg1)

obj = MyClass()
obj.f1("f1")
obj.f2("f2")
obj.f3("f3")
f4("f4")
print(obj.f5)

This will print:

Executing Method: f1
Executing Class Method: f2
Executing Static Method: f3
Executing Function: f4
Accessing Property: f5
Niek
  • 1
  • 1