New submission from Erik Welch <erik.n.we...@gmail.com>:

The following is new to Python 3.9, and I consider the implementation 
incomplete.  I have code that works for Python 3.8 and before, but not for 
Python 3.9:

"Class methods can now wrap other :term:`descriptors <descriptor>` such as 
:func:`property`."

https://github.com/python/cpython/pull/8405
https://bugs.python.org/issue19072

As implemented, `classmethod` does not correctly wrap descriptors that mimic 
classmethod.  Previously, __get__of wrapped objects wasn't invoked by 
classmethod, so it was safe to have an object with both __call__ and __get__ 
behave like a classmethod.  Now, classmethod calling __get__ first gives 
incorrect results.

Here is a minimal example:
```
from types import MethodType


class myclassmethod:
    def __init__(self, func):
        self.func = func

    def __call__(self, cls):
        return self.func(cls)

    def __get__(self, instance, owner=None):
        if owner is None:
            owner = type(instance)
        return MethodType(self, owner)


class A:
    @myclassmethod
    def f1(cls):
        return cls

    @classmethod
    @myclassmethod
    def f2(cls):
        return cls


assert A.f1() is A
assert A.f2() is A  # <-- fails in 3.9, works in 3.8 and before
```
This pattern would typically be used to do something extra in __call__.

For the sake of discussion, let's call the two arguments to __get__ "instance" 
and "owner".  Typically, "instance" is an instance of "owner", or, 
equivalently, "owner" is the type of "instance".  If "owner" is None, it is 
generally assumed to be the type of "instance".

In bpo19072 (and gh8405), classmethod was changed to call `obj.__get__(owner)` 
if the wrapped object "obj" has __get__.  Notice that only the "instance" 
argument is provided.  Moreover, the type `owner` is passed as the "instance" 
argument.  This means that the "owner" argument (which is None) will be assumed 
to be the type of the "instance" argument, which is the type of the `owner` 
type.  This is wrong.  The "owner" argument should be `owner`.

I believe it would be better for classmethod to call `obj.__get__(owner, 
owner)` if "obj" has __get__.

This is kind of difficult to explain.  I will make a PR with more informative 
tests shortly.  Here is the simple diff to make the above example pass:
```
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index bd24f67b97..74f9167566 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -739,7 +739,7 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
         type = (PyObject *)(Py_TYPE(obj));
     if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
         return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
-                                                      NULL);
+                                                      type);
     }
     return PyMethod_New(cm->cm_callable, type);
 }

```
Since I consider the new behavior to have introduced a regression, I think this 
change should be applied to both 3.9 and 3.10.

Cheers!

----------
components: Interpreter Core
messages: 378893
nosy: berker.peksag, eriknw, rhettinger, serhiy.storchaka
priority: normal
severity: normal
status: open
title: classmethod does not pass "type/owner" when invoking wrapped __get__
type: behavior
versions: Python 3.10, Python 3.9

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

Reply via email to