pythonpython-decorators

Calling a wrapped static method using self instead of class name passes self as arg


This question is related to Calling a static method with self vs. class name but I'm trying to understand the behavior when you wrap a static method so I can fix my wrapper.

For example:

import functools

def wrap(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        print(f"{f.__name__} called with args: {args}, kwargs: {kwargs}")
        return f(*args, **kwargs)
    return wrapped

class Test:
    @staticmethod
    def static():
        print("hello")

    def method(self):
        self.static()

Test.static = wrap(Test.static)

Test().method()

will produce:

static called with args: (<__main__.Test object at 0x1050b3fd0>,), kwargs: {}
Traceback (most recent call last):
  File "/Users/aiguofer/decorator_example.py", line 20, in <module>
    Test().meth()
  File "/Users/aiguofer/decorator_example.py", line 16, in meth
    self.static()
  File "/Users/aiguofer/decorator_example.py", line 7, in wrapped
    return f(*args, **kwargs)
TypeError: Test.static() takes 0 positional arguments but 1 was given

However, if I change self.static() -> Test.static(), we get the expected output:

static called with args: (), kwargs: {}
hello

My use case is that I need to wrap some methods from an external library, including a staticmethod on a class. Within that class, they call the static method from an instance method using self.<method_name>, which is causing the above issue in my wrapper. I thought I might be able to deal with this issue with a isinstance(f, staticmethod) but that seems to return False.

I'd love to understand what is happening as well as potential solutions to this problem!


Solution

  • Method access in Python works by using the descriptor protocol to customize attribute access. When you access a staticmethod, it uses the descriptor protocol to make the attribute access return the underlying function. That's why isinstance(f, staticmethod) reported False, in the versions of your code where you tried that.

    Then when you try to assign Test.static = wrap(Test.static), wrap returns an ordinary function object. When you access one of those on an instance, they use the descriptor protocol to return a method object, with the first argument bound to the instance. You need to create a staticmethod object, to get staticmethod descriptor handling.

    You can bypass the descriptor protocol with inspect.getattr_static:

    import inspect
    import types
    
    def wrap_thing(thing):
        if isinstance(thing, types.FunctionType):
            return wrap(thing)
        elif isinstance(thing, staticmethod):
            return staticmethod(wrap(thing.__func__))
        elif isinstance(thing, classmethod):
            return classmethod(wrap(thing.__func__))
        elif isinstance(thing, property):
            fget, fset, fdel = [None if attr is None else wrap(attr)
                                for attr in [thing.fget, thing.fset, thing.fdel]]
            return property(fget, fset, fdel, thing.__doc__)
        else:
            raise TypeError(f'unhandled type: {type(thing)}')
    
    Test.static = wrap_thing(inspect.getattr_static(Test, 'static'))