8000 gh-105974: Revert unintentional behaviour change for protocols with n… · python/cpython@9499b0f · GitHub
[go: up one dir, main page]

Skip to content

Commit 9499b0f

Browse files
authored
gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom __subclasshook__ methods (#105976)
1 parent 968435d commit 9499b0f

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
@@ -3477,6 +3477,46 @@ def __subclasshook__(cls, other):
34773477
self.assertIsSubclass(OKClass, C)
34783478
self.assertNotIsSubclass(BadClass, C)
34793479

3480+
def test_custom_subclasshook_2(self):
3481+
@runtime_checkable
3482+
class HasX(Protocol):
3483+
# The presence of a non-callable member
3484+
# would mean issubclass() checks would fail with TypeError
3485+
# if it weren't for the custom `__subclasshook__` method
3486+
x = 1
3487+
3488+
@classmethod
3489+
def __subclasshook__(cls, other):
3490+
return hasattr(other, 'x')
3491+
3492+
class Empty: pass
3493+
3494+
class ImplementsHasX:
3495+
x = 1
3496+
3497+
self.assertIsInstance(ImplementsHasX(), HasX)
3498+
self.assertNotIsInstance(Empty(), HasX)
3499+
self.assertIsSubclass(ImplementsHasX, HasX)
3500+
self.assertNotIsSubclass(Empty, HasX)
3501+
3502+
# isinstance() and issubclass() checks against this still raise TypeError,
3503+
# despite the presence of the custom __subclasshook__ method,
3504+
# as it's not decorated with @runtime_checkable
3505+
class NotRuntimeCheckable(Protocol):
3506+
@classmethod
3507+
def __subclasshook__(cls, other):
3508+
return hasattr(other, 'x')
3509+
3510+
must_be_runtime_checkable = (
3511+
"Instance and class checks can only be used "
3512+
"with @runtime_checkable protocols"
3513+
)
3514+
3515+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
3516+
issubclass(object, NotRuntimeCheckable)
3517+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
3518+
isinstance(object(), NotRuntimeCheckable)
3519+
34803520
def test_issubclass_fails_correctly(self):
34813521
@runtime_checkable
34823522
class P(Protocol):

Lib/typing.py

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,14 +1818,17 @@ def __init__(cls, *args, **kwargs):
18181818
def __subclasscheck__(cls, other):
18191819
if cls is Protocol:
18201820
return type.__subclasscheck__(cls, other)
1821-
if not isinstance(other, type):
1822-
# Same error message as for issubclass(1, int).
1823-
raise TypeError('issubclass() arg 1 must be a class')
18241821
if (
18251822
getattr(cls, '_is_protocol', False)
18261823
and not _allow_reckless_class_checks()
18271824
):
1828-
if not cls.__callable_proto_members_only__:
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')
1828+
if (
1829+
not cls.__callable_proto_members_only__
1830+
and cls.__dict__.get("__subclasshook__") is _proto_hook
1831+
):
18291832
raise TypeError(
18301833
"Protocols with non-method members don't support issubclass()"
18311834
)
@@ -1869,6 +1872,30 @@ def __instancecheck__(cls, instance):
18691872
return False
18701873

18711874

1875+
@classmethod
1876+
def _proto_hook(cls, other):
1877+
if not cls.__dict__.get('_is_protocol', False):
1878+
return NotImplemented
1879+
1880+
for attr in cls.__protocol_attrs__:
1881+
for base in other.__mro__:
1882+
# Check if the members appears in the class dictionary...
1883+
if attr in base.__dict__:
1884+
if base.__dict__[attr] is None:
1885+
return NotImplemented
1886+
break
1887+
1888+
# ...or in annotations, if it is a sub-protocol.
1889+
annotations = getattr(base, '__annotations__', {})
1890+
if (isinstance(annotations, collections.abc.Mapping) and
1891+
attr in annotations and
1892+
issubclass(other, Generic) and getattr(other, '_is_protocol', False)):
1893+
break
1894+
else:
1895+
return NotImplemented
1896+
return True
< 6D40 /code>
1897+
1898+
18721899
class Protocol(Generic, metaclass=_ProtocolMeta):
18731900
"""Base class for protocol classes.
18741901
@@ -1914,37 +1941,11 @@ def __init_subclass__(cls, *args, **kwargs):
19141941
cls._is_protocol = any(b is Protocol for b in cls.__bases__)
19151942

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

1942-
# We have nothing more to do for non-protocols...
1943-
if not cls._is_protocol:
1944-
return
1945-
1946-
# ... otherwise prohibit instantiation.
1947-
if cls.__init__ is Protocol.__init__:
1947+
# Prohibit instantiation for protocol classes
1948+
if cls._is_protocol and cls.__init__ is Protocol.__init__:
19481949
cls.__init__ = _no_init_or_replace_init
19491950

19501951

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)
0