Skip to content

Commit f6edb83

Browse files
miss-islingtonJelleZijlstraAlexWaygood
authored
[3.11] gh-112509: Fix keys being present in both required_keys and optional_keys in TypedDict (GH-112512) (#112531)
(cherry picked from commit 4038869) Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: Alex Waygood <[email protected]>
1 parent 0a0cf45 commit f6edb83

File tree

3 files changed

+63
-5
lines changed

3 files changed

+63
-5
lines changed

Lib/test/test_typing.py

+40
Original file line numberDiff line numberDiff line change
@@ -6805,6 +6805,46 @@ class Cat(Animal):
68056805
'voice': str,
68066806
})
68076807

6808+
def test_keys_inheritance_with_same_name(self):
6809+
class NotTotal(TypedDict, total=False):
6810+
a: int
6811+
6812+
class Total(NotTotal):
6813+
a: int
6814+
6815+
self.assertEqual(NotTotal.__required_keys__, frozenset())
6816+
self.assertEqual(NotTotal.__optional_keys__, frozenset(['a']))
6817+
self.assertEqual(Total.__required_keys__, frozenset(['a']))
6818+
self.assertEqual(Total.__optional_keys__, frozenset())
6819+
6820+
class Base(TypedDict):
6821+
a: NotRequired[int]
6822+
b: Required[int]
6823+
6824+
class Child(Base):
6825+
a: Required[int]
6826+
b: NotRequired[int]
6827+
6828+
self.assertEqual(Base.__required_keys__, frozenset(['b']))
6829+
self.assertEqual(Base.__optional_keys__, frozenset(['a']))
6830+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
6831+
self.assertEqual(Child.__optional_keys__, frozenset(['b']))
6832+
6833+
def test_multiple_inheritance_with_same_key(self):
6834+
class Base1(TypedDict):
6835+
a: NotRequired[int]
6836+
6837+
class Base2(TypedDict):
6838+
a: Required[str]
6839+
6840+
class Child(Base1, Base2):
6841+
pass
6842+
6843+
# Last base wins
6844+
self.assertEqual(Child.__annotations__, {'a': Required[str]})
6845+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
6846+
self.assertEqual(Child.__optional_keys__, frozenset())
6847+
68086848
def test_required_notrequired_keys(self):
68096849
self.assertEqual(NontotalMovie.__required_keys__,
68106850
frozenset({"title"}))

Lib/typing.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -2987,8 +2987,14 @@ def __new__(cls, name, bases, ns, total=True):
29872987

29882988
for base in bases:
29892989
annotations.update(base.__dict__.get('__annotations__', {}))
2990-
required_keys.update(base.__dict__.get('__required_keys__', ()))
2991-
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
2990+
2991+
base_required = base.__dict__.get('__required_keys__', set())
2992+
required_keys |= base_required
2993+
optional_keys -= base_required
2994+
2995+
base_optional = base.__dict__.get('__optional_keys__', set())
2996+
required_keys -= base_optional
2997+
optional_keys |= base_optional
29922998

29932999
annotations.update(own_annotations)
29943000
for annotation_key, annotation_type in own_annotations.items():
@@ -3000,14 +3006,23 @@ def __new__(cls, name, bases, ns, total=True):
30003006
annotation_origin = get_origin(annotation_type)
30013007

30023008
if annotation_origin is Required:
3003-
required_keys.add(annotation_key)
3009+
is_required = True
30043010
elif annotation_origin is NotRequired:
3005-
optional_keys.add(annotation_key)
3006-
elif total:
3011+
is_required = False
3012+
else:
3013+
is_required = total
3014+
3015+
if is_required:
30073016
required_keys.add(annotation_key)
3017+
optional_keys.discard(annotation_key)
30083018
else:
30093019
optional_keys.add(annotation_key)
3020+
required_keys.discard(annotation_key)
30103021

3022+
assert required_keys.isdisjoint(optional_keys), (
3023+
f"Required keys overlap with optional keys in {name}:"
3024+
f" {required_keys=}, {optional_keys=}"
3025+
)
30113026
tp_dict.__annotations__ = annotations
30123027
tp_dict.__required_keys__ = frozenset(required_keys)
30133028
tp_dict.__optional_keys__ = frozenset(optional_keys)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix edge cases that could cause a key to be present in both the
2+
``__required_keys__`` and ``__optional_keys__`` attributes of a
3+
:class:`typing.TypedDict`. Patch by Jelle Zijlstra.

0 commit comments

Comments
 (0)