Skip to content

Commit 7d6ee29

Browse files
[3.12] gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom __subclasshook__ methods (GH-105976) (#106032)
gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom `__subclasshook__` methods (GH-105976) (cherry picked from commit 9499b0f) Co-authored-by: Alex Waygood <[email protected]>
1 parent 1ffcd49 commit 7d6ee29

File tree

3 files changed

+79
-32
lines changed

3 files changed

+79
-32
lines changed

Lib/test/test_typing.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3465,6 +3465,46 @@ def __subclasshook__(cls, other):
34653465
self.assertIsSubclass(OKClass, C)
34663466
self.assertNotIsSubclass(BadClass, C)
34673467

3468+
def test_custom_subclasshook_2(self):
3469+
@runtime_checkable
3470+
class HasX(Protocol):
3471+
# The presence of a non-callable member
3472+
# would mean issubclass() checks would fail with TypeError
3473+
# if it weren't for the custom `__subclasshook__` method
3474+
x = 1
3475+
3476+
@classmethod
3477+
def __subclasshook__(cls, other):
3478+
return hasattr(other, 'x')
3479+
3480+
class Empty: pass
3481+
3482+
class ImplementsHasX:
3483+
x = 1
3484+
3485+
self.assertIsInstance(ImplementsHasX(), HasX)
3486+
self.assertNotIsInstance(Empty(), HasX)
3487+
self.assertIsSubclass(ImplementsHasX, HasX)
3488+
self.assertNotIsSubclass(Empty, HasX)
3489+
3490+
# isinstance() and issubclass() checks against this still raise TypeError,
3491+
# despite the presence of the custom __subclasshook__ method,
3492+
# as it's not decorated with @runtime_checkable
3493+
class NotRuntimeCheckable(Protocol):
3494+
@classmethod
3495+
def __subclasshook__(cls, other):
3496+
return hasattr(other, 'x')
3497+
3498+
must_be_runtime_checkable = (
3499+
"Instance and class checks can only be used "
3500+
"with @runtime_checkable protocols"
3501+
)
3502+
3503+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
3504+
issubclass(object, NotRuntimeCheckable)
3505+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
3506+
isinstance(object(), NotRuntimeCheckable)
3507+
34683508
def test_issubclass_fails_correctly(self):
34693509
@runtime_checkable
34703510
class P(Protocol):

Lib/typing.py

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,14 +1822,17 @@ def __init__(cls, *args, **kwargs):
18221822
def __subclasscheck__(cls, other):
18231823
if cls is Protocol:
18241824
return type.__subclasscheck__(cls, other)
1825-
if not isinstance(other, type):
1826-
# Same error message as for issubclass(1, int).
1827-
raise TypeError('issubclass() arg 1 must be a class')
18281825
if (
18291826
getattr(cls, '_is_protocol', False)
18301827
and not _allow_reckless_class_checks()
18311828
):
1832-
if not cls.__callable_proto_members_only__:
1829+
if not isinstance(other, type):
1830+
# Same error message as for issubclass(1, int).
1831+
raise TypeError('issubclass() arg 1 must be a class')
1832+
if (
1833+
not cls.__callable_proto_members_only__
1834+
and cls.__dict__.get("__subclasshook__") is _proto_hook
1835+
):
18331836
raise TypeError(
18341837
"Protocols with non-method members don't support issubclass()"
18351838
)
@@ -1873,6 +1876,30 @@ def __instancecheck__(cls, instance):
18731876
return False
18741877

18751878

1879+
@classmethod
1880+
def _proto_hook(cls, other):
1881+
if not cls.__dict__.get('_is_protocol', False):
1882+
return NotImplemented
1883+
1884+
for attr in cls.__protocol_attrs__:
1885+
for base in other.__mro__:
1886+
# Check if the members appears in the class dictionary...
1887+
if attr in base.__dict__:
1888+
if base.__dict__[attr] is None:
1889+
return NotImplemented
1890+
break
1891+
1892+
# ...or in annotations, if it is a sub-protocol.
1893+
annotations = getattr(base, '__annotations__', {})
1894+
if (isinstance(annotations, collections.abc.Mapping) and
1895+
attr in annotations and
1896+
issubclass(other, Generic) and getattr(other, '_is_protocol', False)):
1897+
break
1898+
else:
1899+
return NotImplemented
1900+
return True
1901+
1902+
18761903
class Protocol(Generic, metaclass=_ProtocolMeta):
18771904
"""Base class for protocol classes.
18781905
@@ -1918,37 +1945,11 @@ def __init_subclass__(cls, *args, **kwargs):
19181945
cls._is_protocol = any(b is Protocol for b in cls.__bases__)
19191946

19201947
# Set (or override) the protocol subclass hook.
1921-
def _proto_hook(other):
1922-
if not cls.__dict__.get('_is_protocol', False):
1923-
return NotImplemented
1924-
1925-
for attr in cls.__protocol_attrs__:
1926-
for base in other.__mro__:
1927-
# Check if the members appears in the class dictionary...
1928-
if attr in base.__dict__:
1929-
if base.__dict__[attr] is None:
1930-
return NotImplemented
1931-
break
1932-
1933-
# ...or in annotations, if it is a sub-protocol.
1934-
annotations = getattr(base, '__annotations__', {})
1935-
if (isinstance(annotations, collections.abc.Mapping) and
1936-
attr in annotations and
1937-
issubclass(other, Generic) and getattr(other, '_is_protocol', False)):
1938-
break
1939-
else:
1940-
return NotImplemented
1941-
return True
1942-
19431948
if '__subclasshook__' not in cls.__dict__:
19441949
cls.__subclasshook__ = _proto_hook
19451950

1946-
# We have nothing more to do for non-protocols...
1947-
if not cls._is_protocol:
1948-
return
1949-
1950-
# ... otherwise prohibit instantiation.
1951-
if cls.__init__ is Protocol.__init__:
1951+
# Prohibit instantiation for protocol classes
1952+
if cls._is_protocol and cls.__init__ is Protocol.__init__:
19521953
cls.__init__ = _no_init_or_replace_init
19531954

19541955

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix bug where a :class:`typing.Protocol` class that had one or more
2+
non-callable members would raise :exc:`TypeError` when :func:`issubclass`
3+
was called against it, even if it defined a custom ``__subclasshook__``
4+
method. The behaviour in Python 3.11 and lower -- which has now been
5+
restored -- was not to raise :exc:`TypeError` in these situations if a
6+
custom ``__subclasshook__`` method was defined. Patch by Alex Waygood.

0 commit comments

Comments
 (0)