Skip to content

Commit

Permalink
pythongh-74690: Avoid a costly type check where possible in `_Protoco…
Browse files Browse the repository at this point in the history
…lMeta.__subclasscheck__`
  • Loading branch information
AlexWaygood committed Dec 4, 2023
1 parent e08b70f commit 35ba873
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 6 deletions.
19 changes: 16 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3533,13 +3533,26 @@ def __subclasshook__(cls, other):

def test_issubclass_fails_correctly(self):
@runtime_checkable
class P(Protocol):
class NonCallableMembers(Protocol):
x = 1

class NotRuntimeCheckable(Protocol):
def callable_member(self) -> int: ...

@runtime_checkable
class RuntimeCheckable(Protocol):
def callable_member(self) -> int: ...

class C: pass

with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
issubclass(C(), P)
# These three all exercise different code paths,
# but should result in the same error message:
for protocol in NonCallableMembers, NotRuntimeCheckable, RuntimeCheckable:
with self.subTest(proto_name=protocol.__name__):
with self.assertRaisesRegex(
TypeError, r"issubclass\(\) arg 1 must be a class"
):
issubclass(C(), protocol)

def test_defining_generic_protocols(self):
T = TypeVar('T')
Expand Down
11 changes: 8 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,12 @@ def _pickle_pskwargs(pskwargs):
_abc_subclasscheck = ABCMeta.__subclasscheck__


def _type_check_subclasscheck_second_arg(arg):
if not isinstance(arg, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')


class _ProtocolMeta(ABCMeta):
# This metaclass is somewhat unfortunate,
# but is necessary for several reasons...
Expand Down Expand Up @@ -1829,13 +1835,11 @@ def __subclasscheck__(cls, other):
getattr(cls, '_is_protocol', False)
and not _allow_reckless_class_checks()
):
if not isinstance(other, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')
if (
not cls.__callable_proto_members_only__
and cls.__dict__.get("__subclasshook__") is _proto_hook
):
_type_check_subclasscheck_second_arg(other)
non_method_attrs = sorted(
attr for attr in cls.__protocol_attrs__
if not callable(getattr(cls, attr, None))
Expand All @@ -1845,6 +1849,7 @@ def __subclasscheck__(cls, other):
f" Non-method members: {str(non_method_attrs)[1:-1]}."
)
if not getattr(cls, '_is_runtime_protocol', False):
_type_check_subclasscheck_second_arg(other)
raise TypeError(
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Speedup :func:`issubclass` checks against simple :func:`runtime-checkable
protocols <typing.runtime_checkable>` by around 6%. Patch by Alex Waygood.

0 comments on commit 35ba873

Please sign in to comment.