Skip to content

Commit 10e87e5

Browse files
authored
bpo-39627: Fix TypedDict totality check for inherited keys (#18503)
(Adapted from python/typing#700)
1 parent fbeba8f commit 10e87e5

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

Lib/test/test_typing.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3809,6 +3809,38 @@ class Point2Dor3D(Point2D, total=False):
38093809
assert Point2Dor3D.__required_keys__ == frozenset(['x', 'y'])
38103810
assert Point2Dor3D.__optional_keys__ == frozenset(['z'])
38113811

3812+
def test_keys_inheritance(self):
3813+
class BaseAnimal(TypedDict):
3814+
name: str
3815+
3816+
class Animal(BaseAnimal, total=False):
3817+
voice: str
3818+
tail: bool
3819+
3820+
class Cat(Animal):
3821+
fur_color: str
3822+
3823+
assert BaseAnimal.__required_keys__ == frozenset(['name'])
3824+
assert BaseAnimal.__optional_keys__ == frozenset([])
3825+
assert BaseAnimal.__annotations__ == {'name': str}
3826+
3827+
assert Animal.__required_keys__ == frozenset(['name'])
3828+
assert Animal.__optional_keys__ == frozenset(['tail', 'voice'])
3829+
assert Animal.__annotations__ == {
3830+
'name': str,
3831+
'tail': bool,
3832+
'voice': str,
3833+
}
3834+
3835+
assert Cat.__required_keys__ == frozenset(['name', 'fur_color'])
3836+
assert Cat.__optional_keys__ == frozenset(['tail', 'voice'])
3837+
assert Cat.__annotations__ == {
3838+
'fur_color': str,
3839+
'name': str,
3840+
'tail': bool,
3841+
'voice': str,
3842+
}
3843+
38123844

38133845
class IOTests(BaseTestCase):
38143846

Lib/typing.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,23 +1828,30 @@ def __new__(cls, name, bases, ns, total=True):
18281828
ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
18291829
tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns)
18301830

1831-
anns = ns.get('__annotations__', {})
1831+
annotations = {}
1832+
own_annotations = ns.get('__annotations__', {})
1833+
own_annotation_keys = set(own_annotations.keys())
18321834
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
1833-
anns = {n: _type_check(tp, msg) for n, tp in anns.items()}
1834-
required = set(anns if total else ())
1835-
optional = set(() if total else anns)
1835+
own_annotations = {
1836+
n: _type_check(tp, msg) for n, tp in own_annotations.items()
1837+
}
1838+
required_keys = set()
1839+
optional_keys = set()
18361840

18371841
for base in bases:
1838-
base_anns = base.__dict__.get('__annotations__', {})
1839-
anns.update(base_anns)
1840-
if getattr(base, '__total__', True):
1841-
required.update(base_anns)
1842-
else:
1843-
optional.update(base_anns)
1842+
annotations.update(base.__dict__.get('__annotations__', {}))
1843+
required_keys.update(base.__dict__.get('__required_keys__', ()))
1844+
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
1845+
1846+
annotations.update(own_annotations)
1847+
if total:
1848+
required_keys.update(own_annotation_keys)
1849+
else:
1850+
optional_keys.update(own_annotation_keys)
18441851

1845-
tp_dict.__annotations__ = anns
1846-
tp_dict.__required_keys__ = frozenset(required)
1847-
tp_dict.__optional_keys__ = frozenset(optional)
1852+
tp_dict.__annotations__ = annotations
1853+
tp_dict.__required_keys__ = frozenset(required_keys)
1854+
tp_dict.__optional_keys__ = frozenset(optional_keys)
18481855
if not hasattr(tp_dict, '__total__'):
18491856
tp_dict.__total__ = total
18501857
return tp_dict
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed TypedDict totality check for inherited keys.

0 commit comments

Comments
 (0)