Skip to content

gh-105144: abc: Suppress errors raised by unrelated other subclasses #105159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,20 @@ def __subclasscheck__(cls, subclass):
return True
# Check if it's a subclass of a registered class (recursive)
for rcls in cls._abc_registry:
if issubclass(subclass, rcls):
cls._abc_cache.add(subclass)
return True
try:
if issubclass(subclass, rcls):
cls._abc_cache.add(subclass)
return True
except Exception:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the C code I actually suppress all exceptions, not just Exception. Not sure what the right thing to do is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say suppressing all exceptions would definitely be bad -- that would mean that we'd be suppressing e.g. KeyboardInterrupt, and SystemExit.

Honestly, I feel pretty uncomfortable about suppressing all subclasses of Exception -- could we just do TypeError? If I had a typo in a custom __subclasscheck__ method, I'm not sure I'd want e.g. the resulting AttributeError to be silenced because of the fact that ABCMeta was being used somewhere

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd still see the error if you use your broken class directly. What this PR changes is that you'll no longer see the error if you use issubclass() checks on a separate class that inherits from the same ABC.

pass
# Check if it's a subclass of a subclass (recursive)
for scls in cls.__subclasses__():
if issubclass(subclass, scls):
cls._abc_cache.add(subclass)
return True
try:
if issubclass(subclass, scls):
cls._abc_cache.add(subclass)
return True
except Exception:
pass
# No dice; update negative cache
cls._abc_negative_cache.add(subclass)
return False
38 changes: 36 additions & 2 deletions Lib/test/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,17 +435,22 @@ class C:
None,
lambda x: [],
lambda: 42,
lambda: [42],
]

for i, func in enumerate(bogus_subclasses):
class S(metaclass=abc_ABCMeta):
__subclasses__ = func

with self.subTest(i=i):
with self.subTest(i=i, func=func):
with self.assertRaises(TypeError):
issubclass(int, S)

# If __subclasses__ contains non-classes, we suppress the error instead.
class S(metaclass=abc_ABCMeta):
__subclasses__ = lambda: [42]

self.assertIs(issubclass(int, S), False)

# Also check that issubclass() propagates exceptions raised by
# __subclasses__.
exc_msg = "exception from __subclasses__"
Expand All @@ -459,6 +464,35 @@ class S(metaclass=abc_ABCMeta):
with self.assertRaisesRegex(Exception, exc_msg):
issubclass(int, S)

def test_subclass_with_broken_subclasscheck(self):
class A(metaclass=abc_ABCMeta):
pass

class Unrelated: pass

self.assertIs(issubclass(Unrelated, A), False)

class BrokenMeta(abc_ABCMeta):
is_broken = True
def __subclasscheck__(cls, subclass):
if not BrokenMeta.is_broken:
return super().__subclasscheck__(subclass)
raise Exception("broken")

class Broken(A, metaclass=BrokenMeta):
pass

self.assertIs(issubclass(Unrelated, A), False)

class RegisteredBroken(metaclass=BrokenMeta):
pass

BrokenMeta.is_broken = False
A.register(RegisteredBroken)
BrokenMeta.is_broken = True

self.assertIs(issubclass(Unrelated, A), False)

def test_subclasshook(self):
class A(metaclass=abc.ABCMeta):
@classmethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Subclass checks on :class:`abc.ABC` subclasses no longer propagate errors
raised by subclass checks on unrelated base classes of the ABC. Patch by
Jelle Zijlstra.
6 changes: 3 additions & 3 deletions Modules/_abc.c
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,8 @@ _abc__abc_subclasscheck_impl(PyObject *module, PyObject *self,
goto end;
}
if (r < 0) {
goto end;
// Ignore subclasses that throw on issubclass().
PyErr_Clear();
}
}

Expand Down Expand Up @@ -856,8 +857,7 @@ subclasscheck_check_registry(_abc_data *impl, PyObject *subclass,
int r = PyObject_IsSubclass(subclass, rkey);
Py_DECREF(rkey);
if (r < 0) {
ret = -1;
break;
PyErr_Clear();
}
if (r > 0) {
if (_add_to_weak_set(&impl->_abc_cache, subclass) < 0) {
Expand Down