Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0635e20

Browse files
bpo-45081: Fix __init__ method generation when inheriting from Protocol (GH-28121)
Co-authored-by: Ken Jin <[email protected]>
1 parent 767a17f commit 0635e20

File tree

3 files changed

+47
-13
lines changed

3 files changed

+47
-13
lines changed

Lib/test/test_dataclasses.py

+21-1
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

+24-12
Original file line numberDiff line numberDiff line change
@@ -1400,8 +1400,29 @@ def _is_callable_members_only(cls):
14001400
return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
14011401

14021402

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

14061427
def _caller(depth=1, default='__main__'):
14071428
try:
@@ -1541,15 +1562,6 @@ def _proto_hook(other):
15411562

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

15551567
# ... otherwise check consistency of bases, and prohibit instantiation.
@@ -1560,7 +1572,7 @@ def _proto_hook(other):
15601572
issubclass(base, Generic) and base._is_protocol):
15611573
raise TypeError('Protocols can only inherit from other'
15621574
' protocols, got %r' % base)
1563-
cls.__init__ = _no_init
1575+
cls.__init__ = _no_init_or_replace_init
15641576

15651577

15661578
class _AnnotatedAlias(_GenericAlias, _root=True):
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)