https://github.com/python/cpython/commit/71af090e24f4df7557e4ad77f26bf0219ef672be commit: 71af090e24f4df7557e4ad77f26bf0219ef672be branch: main author: Felix Scherz <felixwsch...@gmail.com> committer: JelleZijlstra <jelle.zijls...@gmail.com> date: 2025-04-16T08:20:35-07:00 summary:
gh-132493: lazy evaluation of annotations in `typing._proto_hook` (#132534) Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com> Co-authored-by: sobolevn <m...@sobolevn.me> Co-authored-by: Alex Waygood <alex.wayg...@gmail.com> files: A Misc/NEWS.d/next/Library/2025-04-15-08-39-14.gh-issue-132493.V0gLkU.rst M Lib/test/test_typing.py M Lib/typing.py diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 16c5a5204da102..5b05ebe8702015 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4554,6 +4554,33 @@ class Commentable(Protocol): ) self.assertIs(type(exc.__cause__), CustomError) + def test_isinstance_with_deferred_evaluation_of_annotations(self): + @runtime_checkable + class P(Protocol): + def meth(self): + ... + + class DeferredClass: + x: undefined + + class DeferredClassImplementingP: + x: undefined | int + + def __init__(self): + self.x = 0 + + def meth(self): + ... + + # override meth with a non-method attribute to make it part of __annotations__ instead of __dict__ + class SubProtocol(P, Protocol): + meth: undefined + + + self.assertIsSubclass(SubProtocol, P) + self.assertNotIsInstance(DeferredClass(), P) + self.assertIsInstance(DeferredClassImplementingP(), P) + def test_deferred_evaluation_of_annotations(self): class DeferredProto(Protocol): x: DoesNotExist diff --git a/Lib/typing.py b/Lib/typing.py index 36789624d2f57a..245592b5678957 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2020,10 +2020,13 @@ def _proto_hook(cls, other): break # ...or in annotations, if it is a sub-protocol. - annotations = getattr(base, '__annotations__', {}) - if (isinstance(annotations, collections.abc.Mapping) and - attr in annotations and - issubclass(other, Generic) and getattr(other, '_is_protocol', False)): + if ( + issubclass(other, Generic) + and getattr(other, "_is_protocol", False) + and attr in _lazy_annotationlib.get_annotations( + base, format=_lazy_annotationlib.Format.FORWARDREF + ) + ): break else: return NotImplemented diff --git a/Misc/NEWS.d/next/Library/2025-04-15-08-39-14.gh-issue-132493.V0gLkU.rst b/Misc/NEWS.d/next/Library/2025-04-15-08-39-14.gh-issue-132493.V0gLkU.rst new file mode 100644 index 00000000000000..a7f762703642a7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-15-08-39-14.gh-issue-132493.V0gLkU.rst @@ -0,0 +1,4 @@ +:class:`typing.Protocol` now uses :func:`annotationlib.get_annotations` when +checking whether or not an instance implements the protocol with +:func:`isinstance`. This enables support for ``isinstance`` checks against +classes with deferred annotations. _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com