New submission from Graham Dumpleton:

The classmethod decorator when applied to a function of a class, does not 
honour the descriptor binding protocol for whatever it wraps. This means it 
will fail when applied around a function which has a decorator already applied 
to it and where that decorator expects that the descriptor binding protocol is 
executed in order to properly bind the function to the class.

A decorator may want to do this where it is implemented so as to be able to 
determine automatically the context it is used in. That is, one magic decorator 
that can work around functions, instance methods, class methods and classes, 
thereby avoiding the need to have multiple distinct decorator implementations 
for the different use case.

So in the following example code:

class BoundWrapper(object):
    def __init__(self, wrapped):
        self.__wrapped__ = wrapped
    def __call__(self, *args, **kwargs):
        print('BoundWrapper.__call__()', args, kwargs)
        print('__wrapped__.__self__', self.__wrapped__.__self__)
        return self.__wrapped__(*args, **kwargs)

class Wrapper(object):
    def __init__(self, wrapped):
        self.__wrapped__ = wrapped
    def __get__(self, instance, owner):
        bound_function = self.__wrapped__.__get__(instance, owner)
        return BoundWrapper(bound_function)

def decorator(wrapped):
    return Wrapper(wrapped)

class Class(object):
    @decorator
    def function_im(self):
        print('Class.function_im()', self)

    @decorator
    @classmethod
    def function_cm_inner(cls):
        print('Class.function_cm_inner()', cls)

    @classmethod
    @decorator
    def function_cm_outer(cls):
        print('Class.function_cm_outer()', cls)

c = Class()

c.function_im()
print()
Class.function_cm_inner()
print()
Class.function_cm_outer()

A failure is encountered of:

$ python3.3 cmgettest.py
BoundWrapper.__call__() () {}
__wrapped__.__self__ <__main__.Class object at 0x1029fc150>
Class.function_im() <__main__.Class object at 0x1029fc150>

BoundWrapper.__call__() () {}
__wrapped__.__self__ <class '__main__.Class'>
Class.function_cm_inner() <class '__main__.Class'>

Traceback (most recent call last):
  File "cmgettest.py", line 40, in <module>
    Class.function_cm_outer()
TypeError: 'Wrapper' object is not callable

IOW, everything is fine when the decorator is applied around the classmethod, 
but when it is placed inside of the classmethod, a failure occurs because the 
decorator object is not callable.

One could argue that the error is easily avoided by adding a __call__() method 
to the Wrapper class, but that defeats the purpose of what is trying to be 
achieved in using this pattern. That is that one can within the bound wrapper 
after binding occurs, determine from the __self__ of the bound function, the 
fact that it was a class method. This can be inferred from the fact that 
__self__ is a class type.

If the classmethod decorator tp_descr_get implementation is changed so as to 
properly apply the descriptor binding protocol to the wrapped object, then what 
is being described is possible.

Having it honour the descriptor binding protocol also seems to make application 
of the Python object model more consistent.

A patch is attached which does exactly this.

The result for the above test after the patch is applied is:

BoundWrapper.__call__() () {}
__wrapped__.__self__ <__main__.Class object at 0x10ad237d0>
Class.function_im() <__main__.Class object at 0x10ad237d0>

BoundWrapper.__call__() () {}
__wrapped__.__self__ <class '__main__.Class'>
Class.function_cm_inner() <class '__main__.Class'>

BoundWrapper.__call__() () {}
__wrapped__.__self__ <class '__main__.Class'>
Class.function_cm_outer() <class '__main__.Class'>

That is, the decorator whether it is inside or outside now sees things in the 
same way.

If one also tests for calling of the classmethod via the instance:

print()
c.function_cm_inner()
print()
c.function_cm_outer()

Everything again also works out how want it:

BoundWrapper.__call__() () {}
__wrapped__.__self__ <class '__main__.Class'>
Class.function_cm_inner() <class '__main__.Class'>

BoundWrapper.__call__() () {}
__wrapped__.__self__ <class '__main__.Class'>
Class.function_cm_outer() <class '__main__.Class'>

FWIW, the shortcoming of classmethod not applying the descriptor binding 
protocol to the wrapped object, was found in writing a new object proxy and 
decorator library called 'wrapt'. This issue in the classmethod implementation 
is the one thing that has prevented wrapt having a system of writing decorators 
that can magically work out the context it is used in all the time. Would be 
nice to see it fixed. :-)

The wrapt library can be found at:

https://github.com/GrahamDumpleton/wrapt
http://wrapt.readthedocs.org

The limitation in the classmethod implementation is noted in the wrapt 
documentation at:

http://wrapt.readthedocs.org/en/v1.1.2/issues.html#classmethod-get

----------
components: Interpreter Core
files: funcobject.c.diff
keywords: patch
messages: 198274
nosy: grahamd
priority: normal
severity: normal
status: open
title: classmethod doesn't honour descriptor protocol of wrapped callable
type: enhancement
versions: Python 3.3
Added file: http://bugs.python.org/file31842/funcobject.c.diff

_______________________________________
Python tracker <rep...@bugs.python.org>
<http://bugs.python.org/issue19072>
_______________________________________
_______________________________________________
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com

Reply via email to