Skip to content

Commit 79e9f5a

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

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
@@ -10,7 +10,7 @@
1010
import builtins
1111
import unittest
1212
from unittest.mock import Mock
13-
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional
13+
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol
1414
from typing import get_type_hints
1515
from collections import deque, OrderedDict, namedtuple
1616
from functools import total_ordering
@@ -2150,6 +2150,26 @@ def __init__(self, x):
21502150
self.x = 2 * x
21512151
self.assertEqual(C(5).x, 10)
21522152

2153+
def test_inherit_from_protocol(self):
2154+
# Dataclasses inheriting from protocol should preserve their own `__init__`.
2155+
# See bpo-45081.
2156+
2157+
class P(Protocol):
2158+
a: int
2159+
2160+
@dataclass
2161+
class C(P):
2162+
a: int
2163+
2164+
self.assertEqual(C(5).a, 5)
2165+
2166+
@dataclass
2167+
class D(P):
2168+
def __init__(self, a):
2169+
self.a = a * 2
2170+
2171+
self.assertEqual(D(5).a, 10)
2172+
21532173

21542174
class TestRepr(unittest.TestCase):
21552175
def test_repr(self):

Lib/typing.py

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

14001400

1401-
def _no_init(self, *args, **kwargs):
1402-
raise TypeError('Protocols cannot be instantiated')
1401+
def _no_init_or_replace_init(self, *args, **kwargs):
1402+
cls = type(self)
1403+
1404+
if cls._is_protocol:
1405+
raise TypeError('Protocols cannot be instantiated')
1406+
1407+
# Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1408+
# The first instantiation of the subclass will call `_no_init_or_replace_init` which
1409+
# searches for a proper new `__init__` in the MRO. The new `__init__`
1410+
# replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1411+
# instantiation of the protocol subclass will thus use the new
1412+
# `__init__` and no longer call `_no_init_or_replace_init`.
1413+
for base in cls.__mro__:
1414+
init = base.__dict__.get('__init__', _no_init_or_replace_init)
1415+
if init is not _no_init_or_replace_init:
1416+
cls.__init__ = init
1417+
break
1418+
else:
1419+
# should not happen
1420+
cls.__init__ = object.__init__
1421+
1422+
cls.__init__(self, *args, **kwargs)
1423+
14031424

14041425
def _caller(depth=1, default='__main__'):
14051426
try:
@@ -1542,15 +1563,6 @@ def _proto_hook(other):
15421563

15431564
# We have nothing more to do for non-protocols...
15441565
if not cls._is_protocol:
1545-
if cls.__init__ == _no_init:
1546-
for base in cls.__mro__:
1547-
init = base.__dict__.get('__init__', _no_init)
1548-
if init != _no_init:
1549-
cls.__init__ = init
1550-
break
1551-
else:
1552-
# should not happen
1553-
cls.__init__ = object.__init__
15541566
return
15551567

15561568
# ... otherwise check consistency of bases, and prohibit instantiation.
@@ -1561,7 +1573,7 @@ def _proto_hook(other):
15611573
issubclass(base, Generic) and base._is_protocol):
15621574
raise TypeError('Protocols can only inherit from other'
15631575
' protocols, got %r' % base)
1564-
cls.__init__ = _no_init
1576+
cls.__init__ = _no_init_or_replace_init
15651577

15661578

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