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-132493: Avoid eager import of annotationlib in typing (again) #132596

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 10 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions 27 Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3132,6 +3132,21 @@ def x(self): ...
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, BadPG)

def test_lazy_evaluation_with_subprotocols(self):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
@runtime_checkable
class Base(Protocol):
x: int

@runtime_checkable
class Child(Base, Protocol):
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
y: str

class Capybara:
x = 43

self.assertIsInstance(Capybara(), Base)
self.assertNotIsInstance(Capybara(), Child)

def test_implicit_issubclass_between_two_protocols(self):
@runtime_checkable
class CallableMembersProto(Protocol):
Expand Down Expand Up @@ -3826,6 +3841,7 @@ def meth(self): pass
'_is_protocol', '_is_runtime_protocol', '__parameters__',
'__init__', '__annotations__', '__subclasshook__', '__annotate__',
'__annotations_cache__', '__annotate_func__',
'__protocol_attrs_cache__',
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
}
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
self.assertLessEqual(
Expand Down Expand Up @@ -4542,10 +4558,15 @@ class classproperty:
def __get__(self, instance, type):
raise CustomError

@runtime_checkable
class Commentable(Protocol):
evil = classproperty()

class Normal:
evil = None

with self.assertRaises(TypeError) as cm:
@runtime_checkable
class Commentable(Protocol):
evil = classproperty()
isinstance(Normal(), Commentable)

exc = cm.exception
self.assertEqual(
Expand Down
61 changes: 38 additions & 23 deletions 61 Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,8 @@ class _TypingEllipsis:
'__parameters__', '__orig_bases__', '__orig_class__',
'_is_protocol', '_is_runtime_protocol', '__protocol_attrs__',
'__non_callable_proto_members__', '__type_params__',
'__protocol_attrs_cache__', '__non_callable_proto_members_cache__',
'__final__',
})

_SPECIAL_NAMES = frozenset({
Expand Down Expand Up @@ -1923,9 +1925,11 @@ class _ProtocolMeta(ABCMeta):
# This metaclass is somewhat unfortunate,
# but is necessary for several reasons...
def __new__(mcls, name, bases, namespace, /, **kwargs):
is_protocol = False
if name == "Protocol" and bases == (Generic,):
pass
elif Protocol in bases:
is_protocol = True
for base in bases:
if not (
base in {object, Generic}
Expand All @@ -1939,12 +1943,10 @@ def __new__(mcls, name, bases, namespace, /, **kwargs):
f"Protocols can only inherit from other protocols, "
f"got {base!r}"
)
return super().__new__(mcls, name, bases, namespace, **kwargs)

def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
if getattr(cls, "_is_protocol", False):
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if is_protocol:
cls.__protocol_attrs_cache__ = None
return cls

def __subclasscheck__(cls, other):
if cls is Protocol:
Expand Down Expand Up @@ -1997,14 +1999,42 @@ def __instancecheck__(cls, instance):
val = getattr_static(instance, attr)
except AttributeError:
break
# this attribute is set by @runtime_checkable:
if val is None and attr not in cls.__non_callable_proto_members__:
break
else:
return True

return False

@property
def __protocol_attrs__(cls):
protocol_attrs = cls.__protocol_attrs_cache__
if protocol_attrs is None:
protocol_attrs = _get_protocol_attrs(cls)
cls.__protocol_attrs_cache__ = protocol_attrs
return protocol_attrs

@property
def __non_callable_proto_members__(cls):
# PEP 544 prohibits using issubclass()
# with protocols that have non-method members.
non_callable_members = cls.__non_callable_proto_members_cache__ # set by @runtime_checkable
if non_callable_members is None:
non_callable_members = set()
for attr in cls.__protocol_attrs__:
try:
is_callable = callable(getattr(cls, attr, None))
except Exception as e:
raise TypeError(
f"Failed to determine whether protocol member {attr!r} "
"is a method member"
) from e
else:
if not is_callable:
non_callable_members.add(attr)
cls.__non_callable_proto_members_cache__ = non_callable_members
return non_callable_members


@classmethod
def _proto_hook(cls, other):
Expand Down Expand Up @@ -2220,22 +2250,7 @@ def close(self): ...
raise TypeError('@runtime_checkable can be only applied to protocol classes,'
' got %r' % cls)
cls._is_runtime_protocol = True
# PEP 544 prohibits using issubclass()
# with protocols that have non-method members.
# See gh-113320 for why we compute this attribute here,
# rather than in `_ProtocolMeta.__init__`
cls.__non_callable_proto_members__ = set()
for attr in cls.__protocol_attrs__:
try:
is_callable = callable(getattr(cls, attr, None))
except Exception as e:
raise TypeError(
f"Failed to determine whether protocol member {attr!r} "
"is a method member"
) from e
else:
if not is_callable:
cls.__non_callable_proto_members__.add(attr)
cls.__non_callable_proto_members_cache__ = None
return cls


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up the creation of :class:`typing.Protocol` subclasses.
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.