Message414826
This isn't a CPython bug. It's a change in CPython behavior that wrapt needs to accommodate. In particular, it isn't 'causing a regression for C subclasses of heap types when the parent class has an "__annotations__" descriptor', nor do child classes have any difficulty inheriting the descriptor of their parent classes. That's unsurprising; after all, if I had broken child classes inheriting the descriptors of their parent classes, a lot more would have broken than just wrapt.
The problem is in WraptObjectProxy_setattro(). (Which--just to drive my point home--*is* getting called when you set __annotations__ on one of wrapt's various proxy objects.) WraptObjectProxy_setattro() proxies setattr calls for wrapped objects to the original object--if "o" is a wrapt proxy object wrapping "fn", and you run "o.__annotations__ = x", it should actually execute "fn.__annotations__ = x" under the covers.
Except WraptObjectProxy_setattro() executes *this* code first, starting at line 1531 in my copy of _wrapped.c:
if (PyObject_HasAttr((PyObject *)Py_TYPE(self), name))
return PyObject_GenericSetAttr((PyObject *)self, name, value);
If the *type* has the attribute, then it doesn't proxy the setattr to the wrapped object. Instead it does a "generic setattr" on the object itself.
PyObject_HasAttr works by attempting a getattr on the type. If that getattr call succeeds, PyObject_HasAttr returns true. The type here is FunctionWrapper (WraptFunctionWrapper_Type). Since we're now looking it up on this type object, we use the type of the type object, which is "type", to access the attribute. And getting the "__annotations__" attribute from an object of type "type" means calling type_get_annotations(), a new descriptor which ensures that the annotations dict always exists, which means the HasAttr call succeeds and returns true. In short, this change to the semantics of the "__annotations__" attribute means wrapt no longer proxies the setattr to the underlying wrapped object when setting the "__annotations__" attribute on *any* of its objects.
In my opinion, wrapt needs to accommodate this new behavior. In my testing I changed the above code to this:
if (!annotations_str) {
annotations_str = PyUnicode_InternFromString("__annotations__");
}
if (PyObject_RichCompareBool(name, annotations_str, Py_NE)
&& PyObject_HasAttr((PyObject *)Py_TYPE(self), name))
return PyObject_GenericSetAttr((PyObject *)self, name, value);
I also declared
static PyObject *annotations_str = NULL;
at the top of the function. With that change in place, the tests now passed.
My hunch is, this approach is more or less what wrapt should do. It *might* be undersophisticated; it's possible that there are classes out there playing their own weird descriptor tricks with the "__annotations__" attribute. Perhaps the fix needs to be on a case-by-case basis, based on the type of the wrapped object. Anyway this is obviously up to Graham, which is for the best anyway--he has far more experience than I do with this sort of object proxying wizardry. |
|
Date |
User |
Action |
Args |
2022-03-10 04:58:11 | larry | set | recipients:
+ larry, christian.heimes, grahamd, petr.viktorin, corona10, pablogsal |
2022-03-10 04:58:11 | larry | set | messageid: <1646888291.48.0.970508358077.issue45319@roundup.psfhosted.org> |
2022-03-10 04:58:11 | larry | link | issue45319 messages |
2022-03-10 04:58:11 | larry | create | |
|