From 35ba873f215320ca7d69d19ba101ce6092c4d538 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 4 Dec 2023 16:47:07 +0000 Subject: [PATCH 1/3] gh-74690: Avoid a costly type check where possible in `_ProtocolMeta.__subclasscheck__` --- Lib/test/test_typing.py | 19 ++++++++++++++++--- Lib/typing.py | 11 ++++++++--- ...3-12-04-16-45-11.gh-issue-74690.pQYP5U.rst | 2 ++ 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-12-04-16-45-11.gh-issue-74690.pQYP5U.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3572df7737f652..2d10c39840ddf3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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') diff --git a/Lib/typing.py b/Lib/typing.py index aa64ed93f76fbf..b19a3845d9dc27 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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... @@ -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)) @@ -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" diff --git a/Misc/NEWS.d/next/Library/2023-12-04-16-45-11.gh-issue-74690.pQYP5U.rst b/Misc/NEWS.d/next/Library/2023-12-04-16-45-11.gh-issue-74690.pQYP5U.rst new file mode 100644 index 00000000000000..8102f02e941c29 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-12-04-16-45-11.gh-issue-74690.pQYP5U.rst @@ -0,0 +1,2 @@ +Speedup :func:`issubclass` checks against simple :func:`runtime-checkable +protocols ` by around 6%. Patch by Alex Waygood. From 3c7eb199fa51bbccf677b3dbe4001b423887918d Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 4 Dec 2023 18:55:25 +0000 Subject: [PATCH 2/3] Add a docstring to help future maintainers --- Lib/typing.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index b19a3845d9dc27..00958598312f22 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1790,7 +1790,17 @@ def _pickle_pskwargs(pskwargs): _abc_subclasscheck = ABCMeta.__subclasscheck__ -def _type_check_subclasscheck_second_arg(arg): +def _type_check_issubclass_arg_1(arg): + """Raise TypeError if `arg` is not an instance of `type` + in `issubclass(arg, )`. + + In most cases, this is verified by type.__subclasscheck__. + As such, we don't perform this check unless we absolutely have to. + + For various error paths, however, + we want to ensure that *this* error message is shown to the user + where relevant, rather than a typing.py-specific error message. + """ if not isinstance(arg, type): # Same error message as for issubclass(1, int). raise TypeError('issubclass() arg 1 must be a class') @@ -1839,7 +1849,7 @@ def __subclasscheck__(cls, other): not cls.__callable_proto_members_only__ and cls.__dict__.get("__subclasshook__") is _proto_hook ): - _type_check_subclasscheck_second_arg(other) + _type_check_issubclass_arg_1(other) non_method_attrs = sorted( attr for attr in cls.__protocol_attrs__ if not callable(getattr(cls, attr, None)) @@ -1849,7 +1859,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) + _type_check_issubclass_arg_1(other) raise TypeError( "Instance and class checks can only be used with " "@runtime_checkable protocols" From 39756114cf8b14ac8a3040af5cce6bbfdb3bf83a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 4 Dec 2023 19:09:30 +0000 Subject: [PATCH 3/3] improve docstring --- Lib/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 00958598312f22..61b88a560e9dc5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1795,7 +1795,8 @@ def _type_check_issubclass_arg_1(arg): in `issubclass(arg, )`. In most cases, this is verified by type.__subclasscheck__. - As such, we don't perform this check unless we absolutely have to. + Checking it again unnecessarily would slow down issubclass() checks, + so, we don't perform this check unless we absolutely have to. For various error paths, however, we want to ensure that *this* error message is shown to the user