gh-74690: typing: Call `_get_protocol_attrs` and `_callable_members_only` at protocol class creation time, not during `isinstance()` checks by AlexWaygood · Pull Request #103160 · python/cpython · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The reason will be displayed to describe this comment to others. Learn more.
The only reason I'm adding this method here rather than doing this work in Protocol.__init_subclass__ is that it seems slightly more backwards-compatible. With this PR, _ProtcolMeta.__instancecheck__ assumes that all classes with _ProtocolMeta as their metaclass will have a __protocol_attrs__ attribute. Since _ProtocolMeta is an undocumented implementation detail, it should only be Protocol and Protocol subclasses using _ProtocolMeta as their metaclass, and if we could count on that, then it would be safe to do this work in Protocol.__init_subclass__. But it's possible users might have been reaching into the internals of typing.py and creating other classes that use _ProtocolMeta as their metaclass, and I don't want to risk breaking their code unnecessarily.
if not is_protocol_cls and issubclass(instance.__class__, cls):
return True
protocol_attrs = _get_protocol_attrs(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:
if all(hasattr(instance, attr) and
# 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)
Expand DownExpand Up
@@ -2087,9 +2086,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"
Expand All
@@ -2099,7 +2096,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__:
Expand Down
Toggle all file notesToggle all file annotations
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason I'm adding this method here rather than doing this work in
Protocol.__init_subclass__
is that it seems slightly more backwards-compatible. With this PR,_ProtcolMeta.__instancecheck__
assumes that all classes with_ProtocolMeta
as their metaclass will have a__protocol_attrs__
attribute. Since_ProtocolMeta
is an undocumented implementation detail, it should only beProtocol
andProtocol
subclasses using_ProtocolMeta
as their metaclass, and if we could count on that, then it would be safe to do this work inProtocol.__init_subclass__
. But it's possible users might have been reaching into the internals oftyping.py
and creating other classes that use_ProtocolMeta
as their metaclass, and I don't want to risk breaking their code unnecessarily.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed that there's at least a few uses of
_ProtocolMeta
out in the wild:https://github.com/antonagestam/phantom-types/blob/68ee857af4452a90a48669a8dbc7d1d7110b31d0/src/phantom/sized.py#L71-L82
https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/core.py#L18-L22
https://github.com/InterStella0/stella_bot/blob/bf5f5632bcd88670df90be67b888c282c6e83d99/utils/useful.py#L337-L343