From 3246688918a428738b61c4adb5fbc6525eae96f9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Apr 2023 12:19:03 +0100 Subject: [PATCH] gh-74690: typing: Call `_get_protocol_attrs` and `_callable_members_only` at protocol class creation time, not during `isinstance()` checks (#103160) --- Lib/typing.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index f50e9b0a93b061..b8420f619a1d05 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1905,7 +1905,8 @@ class _TypingEllipsis: _TYPING_INTERNALS = frozenset({ '__parameters__', '__orig_bases__', '__orig_class__', - '_is_protocol', '_is_runtime_protocol' + '_is_protocol', '_is_runtime_protocol', '__protocol_attrs__', + '__callable_proto_members_only__', }) _SPECIAL_NAMES = frozenset({ @@ -1935,11 +1936,6 @@ def _get_protocol_attrs(cls): return attrs -def _is_callable_members_only(cls, protocol_attrs): - # PEP 544 prohibits using issubclass() with protocols that have non-method members. - return all(callable(getattr(cls, attr, None)) for attr in protocol_attrs) - - def _no_init_or_replace_init(self, *args, **kwargs): cls = type(self) @@ -2012,6 +2008,15 @@ def _lazy_load_getattr_static(): class _ProtocolMeta(ABCMeta): # This metaclass is really unfortunate and exists only because of # the lack of __instancehook__. + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + cls.__callable_proto_members_only__ = all( + callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ + ) + def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. @@ -2029,7 +2034,7 @@ def __instancecheck__(cls, instance): if is_protocol_cls: getattr_static = _lazy_load_getattr_static() - for attr in _get_protocol_attrs(cls): + for attr in cls.__protocol_attrs__: try: val = getattr_static(instance, attr) except AttributeError: @@ -2095,9 +2100,7 @@ def _proto_hook(other): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - protocol_attrs = _get_protocol_attrs(cls) - - if not _is_callable_members_only(cls, protocol_attrs): + if not cls.__callable_proto_members_only__ : if _allow_reckless_class_checks(): return NotImplemented raise TypeError("Protocols with non-method members" @@ -2107,7 +2110,7 @@ def _proto_hook(other): raise TypeError('issubclass() arg 1 must be a class') # Second, perform the actual structural compatibility check. - for attr in protocol_attrs: + for attr in cls.__protocol_attrs__: for base in other.__mro__: # Check if the members appears in the class dictionary... if attr in base.__dict__: