Skip to content
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
60 changes: 60 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
4 changes: 2 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading