Skip to content

Commit 4ce8826

Browse files
committed
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.
1 parent 7a845ce commit 4ce8826

3 files changed

Lines changed: 66 additions & 2 deletions

File tree

Lib/test/test_typing.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4823,6 +4823,66 @@ class DeferredProto(Protocol):
48234823
{'x': 'DoesNotExist'}
48244824
)
48254825

4826+
def test_get_protocol_attrs_reraises_unrelated_errors(self):
4827+
class BrokenAnnotationsMeta(type):
4828+
def __getattribute__(cls, name):
4829+
if name == '__annotations__':
4830+
raise RuntimeError('boom')
4831+
return super().__getattribute__(name)
4832+
4833+
class Base(metaclass=BrokenAnnotationsMeta):
4834+
pass
4835+
4836+
with self.assertRaises(RuntimeError):
4837+
typing._get_protocol_attrs(Base)
4838+
4839+
def test_proto_hook_reraises_unrelated_errors(self):
4840+
@runtime_checkable
4841+
class P(Protocol):
4842+
def meth(self): ...
4843+
4844+
class Other(Protocol):
4845+
pass
4846+
4847+
orig_getattribute = type(Other).__getattribute__
4848+
4849+
def broken_getattribute(cls, name):
4850+
if cls is Other and name == '__annotations__':
4851+
raise RuntimeError('boom')
4852+
return orig_getattribute(cls, name)
4853+
4854+
with patch.object(type(Other), '__getattribute__', broken_getattribute):
4855+
with self.assertRaises(RuntimeError):
4856+
issubclass(Other, P)
4857+
4858+
def test_get_protocol_attrs_falls_back_on_attribute_error(self):
4859+
class BrokenAnnotationsMeta(type):
4860+
def __getattribute__(cls, name):
4861+
if name == '__annotations__':
4862+
raise AttributeError('simulated missing annotations')
4863+
return super().__getattribute__(name)
4864+
4865+
class Base(metaclass=BrokenAnnotationsMeta):
4866+
x: int
4867+
4868+
self.assertEqual(typing._get_protocol_attrs(Base), {'x'})
4869+
4870+
def test_proto_hook_falls_back_on_attribute_error(self):
4871+
class BrokenAnnotationsMeta(typing._ProtocolMeta):
4872+
def __getattribute__(cls, name):
4873+
if name == '__annotations__':
4874+
raise AttributeError('simulated missing annotations')
4875+
return super().__getattribute__(name)
4876+
4877+
@runtime_checkable
4878+
class P(Protocol):
4879+
def meth(self): ...
4880+
4881+
class SubProtocol(P, Protocol, metaclass=BrokenAnnotationsMeta):
4882+
meth: int # override with annotation to route through _proto_hook's __annotations__ check
4883+
4884+
self.assertIsSubclass(SubProtocol, P)
4885+
48264886

48274887
class GenericTests(BaseTestCase):
48284888

Lib/typing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,7 +1891,7 @@ def _get_protocol_attrs(cls):
18911891
continue
18921892
try:
18931893
annotations = base.__annotations__
1894-
except Exception:
1894+
except (AttributeError, NameError):
18951895
# Only go through annotationlib to handle deferred annotations if we need to
18961896
annotations = annotationlib.get_annotations(
18971897
base, format=annotationlib.Format.FORWARDREF
@@ -2141,7 +2141,7 @@ def _proto_hook(cls, other):
21412141
# cases it should be unnecessary.
21422142
try:
21432143
annos = base.__annotations__
2144-
except Exception:
2144+
except (AttributeError, NameError):
21452145
annos = annotationlib.get_annotations(
21462146
base, format=annotationlib.Format.FORWARDREF
21472147
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Narrow the ``except Exception`` in ``typing._get_protocol_attrs`` and
2+
``typing._proto_hook`` to ``except (AttributeError, NameError)``, so that
3+
unrelated errors raised while accessing ``__annotations__`` are no longer
4+
silently swallowed.

0 commit comments

Comments
 (0)