1

A decorator @wrapper is using the wrapt library to access the wrapped function's class to get the class's name. Using it on Animal.method() and foo() works as expected.

Problem: However, the method Animal.classy decorated with @classmethod is giving type as its class name, and the method Animal.static decorated with @staticmethod is unable to retrieve its class.

Is it possible for @wrapper decorator function to obtain the class name Animal for Animal.classy() and Animal.static()?

Expected Output

foo
Animal.method
Animal.classy
Animal.static

Obtained Output

foo
Animal.method
type.classy
static

Code to reproduce

import wrapt
import time

@wrapt.decorator
def wrapper(wrapped, instance, args, kw):
    if instance is not None:
        print(f'\n{instance.__class__.__name__}.{wrapped.__name__}')
    else:
        print('\n' + wrapped.__name__)
    result = wrapped(*args, **kw)
    return result

@wrapper
def foo():
    time.sleep(0.1)

class Animal:
    @wrapper
    def method(self):
        time.sleep(0.1)

    @wrapper
    @classmethod
    def classy(cls):
        time.sleep(0.1)

    @wrapper
    @staticmethod
    def static():
        time.sleep(0.1)

if __name__ == '__main__':
    foo()                   # foo
    Animal().method()       # Animal.method
    Animal.classy()         # type.classy
    Animal.static()         # static
Nyxynyx
  • 61,411
  • 155
  • 482
  • 830
  • Why do you call classy and static on the class and not on the instance, as you do with method? Try doing `Animal().classy()` and `Animal().static()` – Pynchia Dec 18 '19 at 17:44
  • 1
    I'm not sure how you would detect the distinction you are looking for. `method` is getting an instance of `Animal` as an argument, but `classy` is just getting an instance of `type`. `static`, of course, isn't getting any argument. None of the three objects knows that they are used as class attributes of `Animal`, and so that information isn't available, in general, in the body of `wrapper`. – chepner Dec 18 '19 at 17:45
  • 1
    That is, `Animal.method` refers to a function object, but that function object doesn't know that it is referenced as an `Animal` class attribute. The same goes for the functions bound to `Animal.classy` and `Animal.static`. – chepner Dec 18 '19 at 17:47
  • 1
    @Pynchia Whether you use `Animal.classy()` or `Animal().classy()`, `classy` is still just getting a reference to `Animal` (an instance of `type`) as an argument. – chepner Dec 18 '19 at 17:49
  • @chepner I think `Animal.method` does know the class name. Since it knows the instance it belongs to, it can do `instance.__class__` to get the instance's class, and hence `instance.__class__.__name__` to get the class name. My example was able to show this. – Nyxynyx Dec 18 '19 at 18:05
  • You could add an explicit check to see if `instance.__class__` is `type` or not, but then you are assuming that you are only wrapping instance of classes whose metaclass is `type`. Maybe that's sufficient for your use case. – chepner Dec 18 '19 at 18:07

1 Answers1

4

The issue you're having is that the arg instance is the value for the first parameter of any method. So for a regular method this will be it's self value and for a class method it'll be the cls value. For a static method you have no first/instance parameter so it behaves as if it were a function and instance is None.

So let's walk through your code. You call foo, your wrapper checks if instance is set, it is not since functions don't get an instance parameter, so it prints out the name of the function.

Next you call method on an instance of Animal. Your wrapper checks if instance is set, which it is since methods get an instance parameter, so it prints out the name of the class for instance and the name of the method.

Next you call classy on the class Animal. Your wrapper checks if instance is set, which it is since class methods get an instance parameter, so it prints out the name of the class for instance and the name of the class method. However in this case instance is the class that the method is defined on so when you do instance.__class__ it retrieves the metaclass for your Animal class which is type. So what you actual want in this case is to add a check if isinstance(instance, type) and in that case you want to do print(f"{instance.__name__}.{wrapped.__name__}") since instance is your class in this case.

Finally you call the static method static on your class Animal. When you declare a method static it behaves as a normal function. So your wrapper checks if instance is set and finds that it is not, since functions don't get instance parameters, so moves on and just prints the name of the function. There's no standard way I know of to detect a static method.

So in summary to answer your exact questions. Is it possible to get the class name for classy? Yes. And is it possible to get the class name for static? Not that I'm aware of.

For further information on implementing a decorator that can be applied in different contexts, see:

In particular it provides the example:

import inspect

@wrapt.decorator
def universal(wrapped, instance, args, kwargs):
    if instance is None:
        if inspect.isclass(wrapped):
            # Decorator was applied to a class.
            return wrapped(*args, **kwargs)
        else:
            # Decorator was applied to a function or staticmethod.
            return wrapped(*args, **kwargs)
    else:
        if inspect.isclass(instance):
            # Decorator was applied to a classmethod.
            return wrapped(*args, **kwargs)
        else:
            # Decorator was applied to an instancemethod.
            return wrapped(*args, **kwargs)

That may help in understanding what steps would need to be taken in each case to calculate the name.

Hope that makes sense.

Graham Dumpleton
  • 57,726
  • 6
  • 119
  • 134
Zech
  • 118
  • 6