Search code examples
pythondecoratorpython-decoratorsclass-methodmonkeypatching

How to wrap (monkey patch) @classmethod


I want to monkey patch one single classmethod, keeping old functionality. Consider my code to get the idea. Here is my code (pretty synthetic example).

#!/usr/bin/env python

class A:

  @classmethod
  def foo(kls, param):
    print 'A.foo called, param is ' + param

  def bar(self, param):
    print 'A.bar called, param is ' + param


a = A()
a.foo('param_foo')
a.bar('param_bar')

# Patching things

def bar_wrapper(wrapped_func):
  def _w(*args, **kwargs):
    print '<bar_wrap>'
    wrapped_func(*args, **kwargs)
    print '</bar_wrap>'
  return _w

def foo_wrapper(wrapped_func):
  # Something missing here?
  def _w(*args, **kwargs):
    print '<foo_wrap>'
    wrapped_func(*args, **kwargs)
    print '</foo_wrap>'
  return _w

# Everything is pretty ok
A.bar = bar_wrapper(A.bar)
a.bar('is_is_wrapped?')

# Failed to wrap @classmethod
A.foo = foo_wrapper(A.foo)
A.foo('another_wrap_test')

This is what I expect to output:

A.foo called, param is param_foo
A.bar called, param is param_bar
<bar_wrap>
A.bar called, param is is_is_wrapped?
</bar_wrap>
<foo_wrap>
A.foo called, param is another_wrap_test
</foo_wrap>

And this is what I get:

A.foo called, param is param_foo
A.bar called, param is param_bar
<bar_wrap>
A.bar called, param is is_is_wrapped?
</bar_wrap>
Traceback (most recent call last):
  File "./pytest.py", line 39, in <module>
    A.foo('another_wrap_test')
TypeError: unbound method _w() must be called with A instance as first argument (got str instance instead)

Seems like one parameter (class-param) was lost during wrapping. Or I just don't the idea of decorating functions?

Thanks in advance.


Solution

  • When you access a method on a class, it is wrapped at that moment; methods act as descriptors here.

    You may want to unwrap the method again, returning a wrapped wrapper:

    def foo_wrapper(wrapped_func):
        wrapped_func = wrapped_func.__func__
        def _w(*args, **kwargs):
            print '<foo_wrap>'
            wrapped_func(*args, **kwargs)
            print '</foo_wrap>'
        return classmethod(_w)
    

    Now the returned decorator is itself a class method, and wrapping works:

    >>> class A:
    ...     @classmethod
    ...     def foo(kls, param):
    ...         print 'A.foo called, param is ' + param
    ... 
    >>> def foo_wrapper(wrapped_func):
    ...     wrapped_func = wrapped_func.__func__
    ...     def _w(*args, **kwargs):
    ...         print '<foo_wrap>'
    ...         wrapped_func(*args, **kwargs)
    ...         print '</foo_wrap>'
    ...     return classmethod(_w)
    ... 
    >>> A.foo = foo_wrapper(A.foo)
    >>> A.foo('bar')
    <foo_wrap>
    A.foo called, param is bar
    </foo_wrap>