Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9499b0f

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

File tree

Expand file treeCollapse file tree

3 files changed

+79
-32
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+79
-32
lines changed

‎Lib/test/test_typing.py

Copy file name to clipboardExpand all lines: Lib/test/test_typing.py
+40Lines 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

Copy file name to clipboardExpand all lines: Lib/typing.py
+33-32Lines 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
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

+6Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.