Skip to content

Commit 4038869

Browse files
gh-112509: Fix keys being present in both required_keys and optional_keys in TypedDict (#112512)
Co-authored-by: Alex Waygood <[email protected]>
1 parent e0449b9 commit 4038869

File tree

3 files changed

+63
-5
lines changed

3 files changed

+63
-5
lines changed

Lib/test/test_typing.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7769,6 +7769,46 @@ class Cat(Animal):
77697769
'voice': str,
77707770
})
77717771

7772+
def test_keys_inheritance_with_same_name(self):
7773+
class NotTotal(TypedDict, total=False):
7774+
a: int
7775+
7776+
class Total(NotTotal):
7777+
a: int
7778+
7779+
self.assertEqual(NotTotal.__required_keys__, frozenset())
7780+
self.assertEqual(NotTotal.__optional_keys__, frozenset(['a']))
7781+
self.assertEqual(Total.__required_keys__, frozenset(['a']))
7782+
self.assertEqual(Total.__optional_keys__, frozenset())
7783+
7784+
class Base(TypedDict):
7785+
a: NotRequired[int]
7786+
b: Required[int]
7787+
7788+
class Child(Base):
7789+
a: Required[int]
7790+
b: NotRequired[int]
7791+
7792+
self.assertEqual(Base.__required_keys__, frozenset(['b']))
7793+
self.assertEqual(Base.__optional_keys__, frozenset(['a']))
7794+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
7795+
self.assertEqual(Child.__optional_keys__, frozenset(['b']))
7796+
7797+
def test_multiple_inheritance_with_same_key(self):
7798+
class Base1(TypedDict):
7799+
a: NotRequired[int]
7800+
7801+
class Base2(TypedDict):
7802+
a: Required[str]
7803+
7804+
class Child(Base1, Base2):
7805+
pass
7806+
7807+
# Last base wins
7808+
self.assertEqual(Child.__annotations__, {'a': Required[str]})
7809+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
7810+
self.assertEqual(Child.__optional_keys__, frozenset())
7811+
77727812
def test_required_notrequired_keys(self):
77737813
self.assertEqual(NontotalMovie.__required_keys__,
77747814
frozenset({"title"}))

Lib/typing.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2884,8 +2884,14 @@ def __new__(cls, name, bases, ns, total=True):
28842884

28852885
for base in bases:
28862886
annotations.update(base.__dict__.get('__annotations__', {}))
2887-
required_keys.update(base.__dict__.get('__required_keys__', ()))
2888-
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
2887+
2888+
base_required = base.__dict__.get('__required_keys__', set())
2889+
required_keys |= base_required
2890+
optional_keys -= base_required
2891+
2892+
base_optional = base.__dict__.get('__optional_keys__', set())
2893+
required_keys -= base_optional
2894+
optional_keys |= base_optional
28892895

28902896
annotations.update(own_annotations)
28912897
for annotation_key, annotation_type in own_annotations.items():
@@ -2897,14 +2903,23 @@ def __new__(cls, name, bases, ns, total=True):
28972903
annotation_origin = get_origin(annotation_type)
28982904

28992905
if annotation_origin is Required:
2900-
required_keys.add(annotation_key)
2906+
is_required = True
29012907
elif annotation_origin is NotRequired:
2902-
optional_keys.add(annotation_key)
2903-
elif total:
2908+
is_required = False
2909+
else:
2910+
is_required = total
2911+
2912+
if is_required:
29042913
required_keys.add(annotation_key)
2914+
optional_keys.discard(annotation_key)
29052915
else:
29062916
optional_keys.add(annotation_key)
2917+
required_keys.discard(annotation_key)
29072918

2919+
assert required_keys.isdisjoint(optional_keys), (
2920+
f"Required keys overlap with optional keys in {name}:"
2921+
f" {required_keys=}, {optional_keys=}"
2922+
)
29082923
tp_dict.__annotations__ = annotations
29092924
tp_dict.__required_keys__ = frozenset(required_keys)
29102925
tp_dict.__optional_keys__ = frozenset(optional_keys)
Lines changed: 3 additions & 0 deletions
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)