Skip to content

Commit 98eb408

Browse files
bpo-45081: Fix __init__ method generation when inheriting from Protocol (GH-28121) (GH-28132)
Co-authored-by: Ken Jin <[email protected]> (cherry picked from commit 0635e20) Co-authored-by: Yurii Karabas <[email protected]>
1 parent ca27109 commit 98eb408

File tree

3 files changed

+47
-13
lines changed

3 files changed

+47
-13
lines changed

Lib/test/test_dataclasses.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import builtins
1010
import unittest
1111
from unittest.mock import Mock
12-
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional
12+
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol
1313
from typing import get_type_hints
1414
from collections import deque, OrderedDict, namedtuple
1515
from functools import total_ordering
@@ -2124,6 +2124,26 @@ def __init__(self, x):
21242124
self.x = 2 * x
21252125
self.assertEqual(C(5).x, 10)
21262126

2127+
def test_inherit_from_protocol(self):
2128+
# Dataclasses inheriting from protocol should preserve their own `__init__`.
2129+
# See bpo-45081.
2130+
2131+
class P(Protocol):
2132+
a: int
2133+
2134+
@dataclass
2135+
class C(P):
2136+
a: int
2137+
2138+
self.assertEqual(C(5).a, 5)
2139+
2140+
@dataclass
2141+
class D(P):
2142+
def __init__(self, a):
2143+
self.a = a * 2
2144+
2145+
self.assertEqual(D(5).a, 10)
2146+
21272147

21282148
class TestRepr(unittest.TestCase):
21292149
def test_repr(self):

Lib/typing.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,8 +1079,29 @@ def _is_callable_members_only(cls):
10791079
return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
10801080

10811081

1082-
def _no_init(self, *args, **kwargs):
1083-
raise TypeError('Protocols cannot be instantiated')
1082+
def _no_init_or_replace_init(self, *args, **kwargs):
1083+
cls = type(self)
1084+
1085+
if cls._is_protocol:
1086+
raise TypeError('Protocols cannot be instantiated')
1087+
1088+
# Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1089+
# The first instantiation of the subclass will call `_no_init_or_replace_init` which
1090+
# searches for a proper new `__init__` in the MRO. The new `__init__`
1091+
# replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1092+
# instantiation of the protocol subclass will thus use the new
1093+
# `__init__` and no longer call `_no_init_or_replace_init`.
1094+
for base in cls.__mro__:
1095+
init = base.__dict__.get('__init__', _no_init_or_replace_init)
1096+
if init is not _no_init_or_replace_init:
1097+
cls.__init__ = init
1098+
break
1099+
else:
1100+
# should not happen
1101+
cls.__init__ = object.__init__
1102+
1103+
cls.__init__(self, *args, **kwargs)
1104+
10841105

10851106

10861107
def _allow_reckless_class_cheks():
@@ -1209,15 +1230,6 @@ def _proto_hook(other):
12091230

12101231
# We have nothing more to do for non-protocols...
12111232
if not cls._is_protocol:
1212-
if cls.__init__ == _no_init:
1213-
for base in cls.__mro__:
1214-
init = base.__dict__.get('__init__', _no_init)
1215-
if init != _no_init:
1216-
cls.__init__ = init
1217-
break
1218-
else:
1219-
# should not happen
1220-
cls.__init__ = object.__init__
12211233
return
12221234

12231235
# ... otherwise check consistency of bases, and prohibit instantiation.
@@ -1228,7 +1240,7 @@ def _proto_hook(other):
12281240
issubclass(base, Generic) and base._is_protocol):
12291241
raise TypeError('Protocols can only inherit from other'
12301242
' protocols, got %r' % base)
1231-
cls.__init__ = _no_init
1243+
cls.__init__ = _no_init_or_replace_init
12321244

12331245

12341246
class _AnnotatedAlias(_GenericAlias, _root=True):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix issue when dataclasses that inherit from ``typing.Protocol`` subclasses
2+
have wrong ``__init__``. Patch provided by Yurii Karabas.

0 commit comments

Comments
 (0)