Skip to content

Commit 2ed20d3

Browse files
authored
gh-74690: Avoid a costly type check where possible in _ProtocolMeta.__subclasscheck__ (#112717)
1 parent 1e4680c commit 2ed20d3

File tree

3 files changed

+37
-6
lines changed

3 files changed

+37
-6
lines changed

Lib/test/test_typing.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -3533,13 +3533,26 @@ def __subclasshook__(cls, other):
35333533

35343534
def test_issubclass_fails_correctly(self):
35353535
@runtime_checkable
3536-
class P(Protocol):
3536+
class NonCallableMembers(Protocol):
35373537
x = 1
35383538

3539+
class NotRuntimeCheckable(Protocol):
3540+
def callable_member(self) -> int: ...
3541+
3542+
@runtime_checkable
3543+
class RuntimeCheckable(Protocol):
3544+
def callable_member(self) -> int: ...
3545+
35393546
class C: pass
35403547

3541-
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
3542-
issubclass(C(), P)
3548+
# These three all exercise different code paths,
3549+
# but should result in the same error message:
3550+
for protocol in NonCallableMembers, NotRuntimeCheckable, RuntimeCheckable:
3551+
with self.subTest(proto_name=protocol.__name__):
3552+
with self.assertRaisesRegex(
3553+
TypeError, r"issubclass\(\) arg 1 must be a class"
3554+
):
3555+
issubclass(C(), protocol)
35433556

35443557
def test_defining_generic_protocols(self):
35453558
T = TypeVar('T')

Lib/typing.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,23 @@ def _pickle_pskwargs(pskwargs):
17901790
_abc_subclasscheck = ABCMeta.__subclasscheck__
17911791

17921792

1793+
def _type_check_issubclass_arg_1(arg):
1794+
"""Raise TypeError if `arg` is not an instance of `type`
1795+
in `issubclass(arg, <protocol>)`.
1796+
1797+
In most cases, this is verified by type.__subclasscheck__.
1798+
Checking it again unnecessarily would slow down issubclass() checks,
1799+
so, we don't perform this check unless we absolutely have to.
1800+
1801+
For various error paths, however,
1802+
we want to ensure that *this* error message is shown to the user
1803+
where relevant, rather than a typing.py-specific error message.
1804+
"""
1805+
if not isinstance(arg, type):
1806+
# Same error message as for issubclass(1, int).
1807+
raise TypeError('issubclass() arg 1 must be a class')
1808+
1809+
17931810
class _ProtocolMeta(ABCMeta):
17941811
# This metaclass is somewhat unfortunate,
17951812
# but is necessary for several reasons...
@@ -1829,13 +1846,11 @@ def __subclasscheck__(cls, other):
18291846
getattr(cls, '_is_protocol', False)
18301847
and not _allow_reckless_class_checks()
18311848
):
1832-
if not isinstance(other, type):
1833-
# Same error message as for issubclass(1, int).
1834-
raise TypeError('issubclass() arg 1 must be a class')
18351849
if (
18361850
not cls.__callable_proto_members_only__
18371851
and cls.__dict__.get("__subclasshook__") is _proto_hook
18381852
):
1853+
_type_check_issubclass_arg_1(other)
18391854
non_method_attrs = sorted(
18401855
attr for attr in cls.__protocol_attrs__
18411856
if not callable(getattr(cls, attr, None))
@@ -1845,6 +1860,7 @@ def __subclasscheck__(cls, other):
18451860
f" Non-method members: {str(non_method_attrs)[1:-1]}."
18461861
)
18471862
if not getattr(cls, '_is_runtime_protocol', False):
1863+
_type_check_issubclass_arg_1(other)
18481864
raise TypeError(
18491865
"Instance and class checks can only be used with "
18501866
"@runtime_checkable protocols"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speedup :func:`issubclass` checks against simple :func:`runtime-checkable
2+
protocols <typing.runtime_checkable>` by around 6%. Patch by Alex Waygood.

0 commit comments

Comments
 (0)