Larry Hastings <la...@hastings.org> added the comment:

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.

----------
resolution:  -> third party
stage: test needed -> resolved
status: open -> closed

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

Reply via email to