gh-74690: typing: Call _get_protocol_attrs and _callable_members_only at protocol class creation time, not during isinstance() checks (#103160)

This commit is contained in:
Alex Waygood 2023-04-05 12:19:03 +01:00 committed by GitHub
parent c396b6ddf3
commit 3246688918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1905,7 +1905,8 @@ class _TypingEllipsis:
_TYPING_INTERNALS = frozenset({ _TYPING_INTERNALS = frozenset({
'__parameters__', '__orig_bases__', '__orig_class__', '__parameters__', '__orig_bases__', '__orig_class__',
'_is_protocol', '_is_runtime_protocol' '_is_protocol', '_is_runtime_protocol', '__protocol_attrs__',
'__callable_proto_members_only__',
}) })
_SPECIAL_NAMES = frozenset({ _SPECIAL_NAMES = frozenset({
@ -1935,11 +1936,6 @@ def _get_protocol_attrs(cls):
return attrs 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): def _no_init_or_replace_init(self, *args, **kwargs):
cls = type(self) cls = type(self)
@ -2012,6 +2008,15 @@ _cleanups.append(_lazy_load_getattr_static.cache_clear)
class _ProtocolMeta(ABCMeta): class _ProtocolMeta(ABCMeta):
# This metaclass is really unfortunate and exists only because of # This metaclass is really unfortunate and exists only because of
# the lack of __instancehook__. # 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): def __instancecheck__(cls, instance):
# We need this method for situations where attributes are # We need this method for situations where attributes are
# assigned in __init__. # assigned in __init__.
@ -2029,7 +2034,7 @@ class _ProtocolMeta(ABCMeta):
if is_protocol_cls: if is_protocol_cls:
getattr_static = _lazy_load_getattr_static() getattr_static = _lazy_load_getattr_static()
for attr in _get_protocol_attrs(cls): for attr in cls.__protocol_attrs__:
try: try:
val = getattr_static(instance, attr) val = getattr_static(instance, attr)
except AttributeError: except AttributeError:
@ -2095,9 +2100,7 @@ class Protocol(Generic, metaclass=_ProtocolMeta):
raise TypeError("Instance and class checks can only be used with" raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols") " @runtime_checkable protocols")
protocol_attrs = _get_protocol_attrs(cls) if not cls.__callable_proto_members_only__ :
if not _is_callable_members_only(cls, protocol_attrs):
if _allow_reckless_class_checks(): if _allow_reckless_class_checks():
return NotImplemented return NotImplemented
raise TypeError("Protocols with non-method members" raise TypeError("Protocols with non-method members"
@ -2107,7 +2110,7 @@ class Protocol(Generic, metaclass=_ProtocolMeta):
raise TypeError('issubclass() arg 1 must be a class') raise TypeError('issubclass() arg 1 must be a class')
# Second, perform the actual structural compatibility check. # Second, perform the actual structural compatibility check.
for attr in protocol_attrs: for attr in cls.__protocol_attrs__:
for base in other.__mro__: for base in other.__mro__:
# Check if the members appears in the class dictionary... # Check if the members appears in the class dictionary...
if attr in base.__dict__: if attr in base.__dict__: