From f15c64abf5fb66d16cf4723abac07f474f752f05 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 1 Apr 2023 00:33:03 +0100 Subject: [PATCH 1/7] gh-74690: typing: Cache results of `_get_protocol_attrs` and `_callable_member_only` --- Lib/typing.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index a88542cfbaecd5..69de3d9a44c622 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1935,9 +1935,9 @@ def _get_protocol_attrs(cls): return attrs -def _is_callable_members_only(cls, protocol_attrs): +def _is_callable_members_only(cls): # PEP 544 prohibits using issubclass() with protocols that have non-method members. - return all(callable(getattr(cls, attr, None)) for attr in protocol_attrs) + return all(callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__) def _no_init_or_replace_init(self, *args, **kwargs): @@ -2016,12 +2016,11 @@ def __instancecheck__(cls, instance): if not is_protocol_cls and issubclass(instance.__class__, cls): return True - protocol_attrs = _get_protocol_attrs(cls) + if not hasattr(cls, "__protocol_attrs__"): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + cls.__callable_proto_members_only__ = _is_callable_members_only(cls) - if ( - _is_callable_members_only(cls, protocol_attrs) - and issubclass(instance.__class__, cls) - ): + if cls.__callable_proto_members_only__ and issubclass(instance.__class__, cls): return True if is_protocol_cls: @@ -2029,7 +2028,7 @@ def __instancecheck__(cls, instance): # All *methods* can be blocked by setting them to None. (not callable(getattr(cls, attr, None)) or getattr(instance, attr) is not None) - for attr in protocol_attrs): + for attr in cls.__protocol_attrs__): return True return super().__instancecheck__(instance) @@ -2087,9 +2086,11 @@ 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 hasattr(cls, "__protocol_attrs__"): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + cls.__callable_proto_members_only__ = _is_callable_members_only(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" @@ -2099,7 +2100,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__: From f3ec3db9a43244602c6da89553fcba7051a48717 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 1 Apr 2023 11:17:52 +0100 Subject: [PATCH 2/7] Simplify and inline --- Lib/typing.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 69de3d9a44c622..347425553d68f0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1935,11 +1935,6 @@ def _get_protocol_attrs(cls): return attrs -def _is_callable_members_only(cls): - # PEP 544 prohibits using issubclass() with protocols that have non-method members. - return all(callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__) - - def _no_init_or_replace_init(self, *args, **kwargs): cls = type(self) @@ -2016,9 +2011,7 @@ def __instancecheck__(cls, instance): if not is_protocol_cls and issubclass(instance.__class__, cls): return True - if not hasattr(cls, "__protocol_attrs__"): - cls.__protocol_attrs__ = _get_protocol_attrs(cls) - cls.__callable_proto_members_only__ = _is_callable_members_only(cls) + cls.__lazy_protocol_init_subclass__() if cls.__callable_proto_members_only__ and issubclass(instance.__class__, cls): return True @@ -2067,6 +2060,16 @@ def meth(self) -> T: _is_protocol = True _is_runtime_protocol = False + @classmethod + def __lazy_protocol_init_subclass__(cls): + if not hasattr(cls, "__protocol_attrs__"): + 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 __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(*args, **kwargs) @@ -2086,9 +2089,7 @@ def _proto_hook(other): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if not hasattr(cls, "__protocol_attrs__"): - cls.__protocol_attrs__ = _get_protocol_attrs(cls) - cls.__callable_proto_members_only__ = _is_callable_members_only(cls) + cls.__lazy_protocol_init_subclass__() if not cls.__callable_proto_members_only__ : if _allow_reckless_class_checks(): From 8e3c6f71c986e0daf588faa437b39e7bf768e5ac Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 1 Apr 2023 14:23:15 +0100 Subject: [PATCH 3/7] Use a metaclass instance method, not a classmethod on the class --- Lib/typing.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 347425553d68f0..37609bdae4fb4c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1996,6 +1996,15 @@ def _allow_reckless_class_checks(depth=3): class _ProtocolMeta(ABCMeta): # This metaclass is really unfortunate and exists only because of # the lack of __instancehook__. + def __lazy_protocol_init_subclass__(cls): + if not hasattr(cls, "__protocol_attrs__"): + 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__. @@ -2060,16 +2069,6 @@ def meth(self) -> T: _is_protocol = True _is_runtime_protocol = False - @classmethod - def __lazy_protocol_init_subclass__(cls): - if not hasattr(cls, "__protocol_attrs__"): - 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 __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(*args, **kwargs) From a34f7005abc35f7da745e45aed3043d9c273837a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 1 Apr 2023 17:11:49 +0100 Subject: [PATCH 4/7] Move to class-creation time --- Lib/typing.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 37609bdae4fb4c..17dcb3f157db83 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({ @@ -1996,14 +1997,15 @@ def _allow_reckless_class_checks(depth=3): class _ProtocolMeta(ABCMeta): # This metaclass is really unfortunate and exists only because of # the lack of __instancehook__. - def __lazy_protocol_init_subclass__(cls): - if not hasattr(cls, "__protocol_attrs__"): - 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 __new__(metacls, name, bases, namespace, **kwargs): + cls = super().__new__(metacls, name, bases, namespace, **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__ + ) + return cls def __instancecheck__(cls, instance): # We need this method for situations where attributes are @@ -2020,8 +2022,6 @@ def __instancecheck__(cls, instance): if not is_protocol_cls and issubclass(instance.__class__, cls): return True - cls.__lazy_protocol_init_subclass__() - if cls.__callable_proto_members_only__ and issubclass(instance.__class__, cls): return True @@ -2088,8 +2088,6 @@ def _proto_hook(other): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - cls.__lazy_protocol_init_subclass__() - if not cls.__callable_proto_members_only__ : if _allow_reckless_class_checks(): return NotImplemented From 4d050749087ea86a7cacc9ce96b867057e655788 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 1 Apr 2023 17:46:17 +0100 Subject: [PATCH 5/7] Use `__init__`, not `__new__` --- Lib/typing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 17dcb3f157db83..178ab741409978 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1997,15 +1997,13 @@ def _allow_reckless_class_checks(depth=3): class _ProtocolMeta(ABCMeta): # This metaclass is really unfortunate and exists only because of # the lack of __instancehook__. - def __new__(metacls, name, bases, namespace, **kwargs): - cls = super().__new__(metacls, name, bases, namespace, **kwargs) + def __init__(cls, *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__ ) - return cls def __instancecheck__(cls, instance): # We need this method for situations where attributes are From d3f9ebee0fb88db3d87bac71180fa00ab853f2c4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 1 Apr 2023 18:23:35 +0100 Subject: [PATCH 6/7] Add missing `super().__init__()` call, to be safe --- Lib/typing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/typing.py b/Lib/typing.py index 178ab741409978..6a5d0ba3771cb0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1998,6 +1998,7 @@ 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. From 08af61593d85db78efa1abef197f8d8e510a7913 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Apr 2023 10:38:21 +0100 Subject: [PATCH 7/7] Update Lib/typing.py --- Lib/typing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/typing.py b/Lib/typing.py index 406cf3c6e8920e..b8420f619a1d05 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2028,6 +2028,7 @@ def __instancecheck__(cls, instance): ): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") + if super().__instancecheck__(instance): return True