Closed jamadden closed 3 years ago
The oldest version of the C code present in this repo behaves this way ("contains this bug") as well. That version of the code is pretty loose about throwing away any exception raised from getattr
(i.e. not checking PyErr_ExceptionMatches(AttributeError)
). In fact, there's no where in the original code that checks for AttributeError
. Examples:
Over time, many of those have been tightened up, and newly written C code is checking for AttributeError
.
(Found while working on
collective/contentratings
, which has exactly this anti-pattern).Consider this example code. It creates a class that implements an interface, and that class calls
providedBy(self)
in both__getattr__
(for unknown attributes) and__setattr__
(for setting attributes). It then instantiates a subclass of that class (which itself does not implement any interfaces):If we run it with the C optimizations, everything seems fine (at first blush):
If we use the Python implementation, however, it blows up:
The
providedBy
function first asks for the__providedBy__
attribute of the object. For classes that implement an interface, this is a non-data descriptor. The__get__
method of this descriptor in turn asks for the instance's__provides__
attribute to see if the instance has any extra interfaces; if not, it returns what the class implements.The Python version of this descriptor uses
getattr(inst, '__provides__', None)
to check for__provides__
. In Python 3, this allows exceptions other thanAttributeError
to be raised:https://github.com/zopefoundation/zope.interface/blob/4a686fc8d87d398045dc44c1b6a97a2940121800/src/zope/interface/declarations.py#L1258-L1260
The C code, on the other hand, disregards all exceptions that might happen checking for
__provides__
:https://github.com/zopefoundation/zope.interface/blob/4a686fc8d87d398045dc44c1b6a97a2940121800/src/zope/interface/_zope_interface_coptimizations.c#L528-L531
Watching closely, we can see that the recursion is still happening with the C code, it just ultimately "bottoms out" and returns
NULL
, which is then ignored…and all the stack unwound, exceptions discarded, etc.Most places in the C code that do
PyObject_GetAttr
follow aNULL
result up withPyErr_ExceptionMatches(AttributeError)
before deciding to ignore the error. This matches whatgetattr(…,…,None)
does.I argue that's what should happen here. It's bad to allow deep recursion in the event of errors which are then completely hidden. Indeed, with
sys.recursionlimit
set high enough, this code even crashes in the bowels of C, which is not good.It was a bit tricky to simplify this from the failing test case in
contentratings
. The key is that there must be a subclass of a class that implements something (the super class has both__providedBy__
and__provides__
; the subclass has neither). If you don't use a subclass, both C and Python implementations work fine (because the class automatically has__provides__
); if the subclass also implements something, they work fine (same reason). The base class, but not the subclass, must also implement an interface; if it doesn't, both C and Python crash (because there is no__providedBy__
descriptor). And actually, the__setattr__
isn't even necessary: callingprovidedBy()
in__getattr__
is enough to trigger this.The fix for
contentratings
, by the way, was to check in__getattr__
to see if we're being asked for__provides__
before going through with callingprovidedBy()
.