This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Author larry
Recipients christian.heimes, corona10, grahamd, larry, pablogsal, petr.viktorin
Date 2022-03-10.04:58:11
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1646888291.48.0.970508358077.issue45319@roundup.psfhosted.org>
In-reply-to
Content
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.
History
Date User Action Args
2022-03-10 04:58:11larrysetrecipients: + larry, christian.heimes, grahamd, petr.viktorin, corona10, pablogsal
2022-03-10 04:58:11larrysetmessageid: <1646888291.48.0.970508358077.issue45319@roundup.psfhosted.org>
2022-03-10 04:58:11larrylinkissue45319 messages
2022-03-10 04:58:11larrycreate