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 7d6ee29

Browse filesBrowse 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 <Alex.Waygood@Gmail.com>
1 parent 1ffcd49 commit 7d6ee29
Copy full SHA for 7d6ee29

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
@@ -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

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
@@ -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

+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.