Skip to content

Backport CPython PR 105976 #252

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 2 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Unreleased

- Fix bug where a `typing_extensions.Protocol` class that had one or more
non-callable members would raise `TypeError` when `issubclass()`
was called against it, even if it defined a custom `__subclasshook__`
method. The correct behaviour -- which has now been restored -- is not to
raise `TypeError` in these situations if a custom `__subclasshook__` method
is defined. Patch by Alex Waygood (backporting
https://github.com/python/cpython/pull/105976).

# Release 4.7.0rc1 (June 21, 2023)

- Add `typing_extensions.get_protocol_members` and
Expand Down
44 changes: 44 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2713,6 +2713,50 @@ def __subclasshook__(cls, other):
self.assertIsSubclass(OKClass, C)
self.assertNotIsSubclass(BadClass, C)

@skipIf(
sys.version_info[:4] == (3, 12, 0, 'beta') and sys.version_info[4] < 4,
"Early betas of Python 3.12 had a bug"
)
def test_custom_subclasshook_2(self):
@runtime_checkable
class HasX(Protocol):
# The presence of a non-callable member
# would mean issubclass() checks would fail with TypeError
# if it weren't for the custom `__subclasshook__` method
x = 1

@classmethod
def __subclasshook__(cls, other):
return hasattr(other, 'x')

class Empty: pass

class ImplementsHasX:
x = 1

self.assertIsInstance(ImplementsHasX(), HasX)
self.assertNotIsInstance(Empty(), HasX)
self.assertIsSubclass(ImplementsHasX, HasX)
self.assertNotIsSubclass(Empty, HasX)

# isinstance() and issubclass() checks against this still raise TypeError,
# despite the presence of the custom __subclasshook__ method,
# as it's not decorated with @runtime_checkable
class NotRuntimeCheckable(Protocol):
@classmethod
def __subclasshook__(cls, other):
return hasattr(other, 'x')

must_be_runtime_checkable = (
"Instance and class checks can only be used "
"with @runtime_checkable protocols"
)

with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
issubclass(object, NotRuntimeCheckable)
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
isinstance(object(), NotRuntimeCheckable)

@skip_if_py312b1
def test_issubclass_fails_correctly(self):
@runtime_checkable
Expand Down
27 changes: 11 additions & 16 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,14 +644,17 @@ def __init__(cls, *args, **kwargs):
def __subclasscheck__(cls, other):
if cls is Protocol:
return type.__subclasscheck__(cls, other)
if not isinstance(other, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')
if (
getattr(cls, '_is_protocol', False)
and not _allow_reckless_class_checks()
):
if not cls.__callable_proto_members_only__:
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
):
raise TypeError(
"Protocols with non-method members don't support issubclass()"
)
Expand Down Expand Up @@ -752,12 +755,8 @@ def __init_subclass__(cls, *args, **kwargs):
if '__subclasshook__' not in cls.__dict__:
cls.__subclasshook__ = _proto_hook

# We have nothing more to do for non-protocols...
if not cls._is_protocol:
return

# ... otherwise prohibit instantiation.
if cls.__init__ is Protocol.__init__:
# Prohibit instantiation for protocol classes
if cls._is_protocol and cls.__init__ is Protocol.__init__:
cls.__init__ = _no_init

else:
Expand Down Expand Up @@ -847,12 +846,8 @@ def __init_subclass__(cls, *args, **kwargs):
if '__subclasshook__' not in cls.__dict__:
cls.__subclasshook__ = _proto_hook

# We have nothing more to do for non-protocols.
if not cls._is_protocol:
return

# Prohibit instantiation
if cls.__init__ is Protocol.__init__:
# Prohibit instantiation for protocol classes
if cls._is_protocol and cls.__init__ is Protocol.__init__:
cls.__init__ = _no_init


Expand Down