Skip to content

Commit

Permalink
Backport CPython PR 105976 (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored Jun 23, 2023
1 parent e703629 commit e65b036
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 16 deletions.
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

0 comments on commit e65b036

Please sign in to comment.