From 4ce88262bce6f4602cfdee9a6971ff3612aaa5a3 Mon Sep 17 00:00:00 2001 From: lovit Date: Mon, 17 Aug 2026 12:52:07 +0900 Subject: [PATCH] gh-155925: Narrow except Exception to (AttributeError, NameError) in typing Protocol helpers _get_protocol_attrs and _proto_hook caught bare Exception around __annotations__ access, silently swallowing unrelated errors along with the legitimate AttributeError/NameError cases. Narrow the catch to (AttributeError, NameError) so genuine bugs propagate instead of being hidden. --- Lib/test/test_typing.py | 60 +++++++++++++++++++ Lib/typing.py | 4 +- ...-08-17-12-49-55.gh-issue-155925.MObpzz.rst | 4 ++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-17-12-49-55.gh-issue-155925.MObpzz.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f35f864dce21e86..6c11f33e50741af 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4823,6 +4823,66 @@ class DeferredProto(Protocol): {'x': 'DoesNotExist'} ) + def test_get_protocol_attrs_reraises_unrelated_errors(self): + class BrokenAnnotationsMeta(type): + def __getattribute__(cls, name): + if name == '__annotations__': + raise RuntimeError('boom') + return super().__getattribute__(name) + + class Base(metaclass=BrokenAnnotationsMeta): + pass + + with self.assertRaises(RuntimeError): + typing._get_protocol_attrs(Base) + + def test_proto_hook_reraises_unrelated_errors(self): + @runtime_checkable + class P(Protocol): + def meth(self): ... + + class Other(Protocol): + pass + + orig_getattribute = type(Other).__getattribute__ + + def broken_getattribute(cls, name): + if cls is Other and name == '__annotations__': + raise RuntimeError('boom') + return orig_getattribute(cls, name) + + with patch.object(type(Other), '__getattribute__', broken_getattribute): + with self.assertRaises(RuntimeError): + issubclass(Other, P) + + def test_get_protocol_attrs_falls_back_on_attribute_error(self): + class BrokenAnnotationsMeta(type): + def __getattribute__(cls, name): + if name == '__annotations__': + raise AttributeError('simulated missing annotations') + return super().__getattribute__(name) + + class Base(metaclass=BrokenAnnotationsMeta): + x: int + + self.assertEqual(typing._get_protocol_attrs(Base), {'x'}) + + def test_proto_hook_falls_back_on_attribute_error(self): + class BrokenAnnotationsMeta(typing._ProtocolMeta): + def __getattribute__(cls, name): + if name == '__annotations__': + raise AttributeError('simulated missing annotations') + return super().__getattribute__(name) + + @runtime_checkable + class P(Protocol): + def meth(self): ... + + class SubProtocol(P, Protocol, metaclass=BrokenAnnotationsMeta): + meth: int # override with annotation to route through _proto_hook's __annotations__ check + + self.assertIsSubclass(SubProtocol, P) + class GenericTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 65e1d1ea6be5844..76beba48568cb42 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1891,7 +1891,7 @@ def _get_protocol_attrs(cls): continue try: annotations = base.__annotations__ - except Exception: + except (AttributeError, NameError): # Only go through annotationlib to handle deferred annotations if we need to annotations = annotationlib.get_annotations( base, format=annotationlib.Format.FORWARDREF @@ -2141,7 +2141,7 @@ def _proto_hook(cls, other): # cases it should be unnecessary. try: annos = base.__annotations__ - except Exception: + except (AttributeError, NameError): annos = annotationlib.get_annotations( base, format=annotationlib.Format.FORWARDREF ) diff --git a/Misc/NEWS.d/next/Library/2026-08-17-12-49-55.gh-issue-155925.MObpzz.rst b/Misc/NEWS.d/next/Library/2026-08-17-12-49-55.gh-issue-155925.MObpzz.rst new file mode 100644 index 000000000000000..99bac02e97d6733 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-12-49-55.gh-issue-155925.MObpzz.rst @@ -0,0 +1,4 @@ +Narrow the ``except Exception`` in ``typing._get_protocol_attrs`` and +``typing._proto_hook`` to ``except (AttributeError, NameError)``, so that +unrelated errors raised while accessing ``__annotations__`` are no longer +silently swallowed.