Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 5, 2023
31 changes: 14 additions & 17 deletions 31 Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -2001,6 +1997,14 @@ 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 __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__
)
Copy link
Member Author

@AlexWaygood AlexWaygood Apr 1, 2023

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 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def __instancecheck__(cls, instance):
# We need this method for situations where attributes are
# assigned in __init__.
Expand All @@ -2016,20 +2020,15 @@ def __instancecheck__(cls, instance):
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 Down Expand 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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.