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!
4
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'))
1