Skip to content

Commit c678126

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

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
@@ -7509,6 +7509,46 @@ class Cat(Animal):
75097509
'voice': str,
75107510
})
75117511

7512+
def test_keys_inheritance_with_same_name(self):
7513+
class NotTotal(TypedDict, total=False):
7514+
a: int
7515+
7516+
class Total(NotTotal):
7517+
a: int
7518+
7519+
self.assertEqual(NotTotal.__required_keys__, frozenset())
7520+
self.assertEqual(NotTotal.__optional_keys__, frozenset(['a']))
7521+
self.assertEqual(Total.__required_keys__, frozenset(['a']))
7522+
self.assertEqual(Total.__optional_keys__, frozenset())
7523+
7524+
class Base(TypedDict):
7525+
a: NotRequired[int]
7526+
b: Required[int]
7527+
7528+
class Child(Base):
7529+
a: Required[int]
7530+
b: NotRequired[int]
7531+
7532+
self.assertEqual(Base.__required_keys__, frozenset(['b']))
7533+
self.assertEqual(Base.__optional_keys__, frozenset(['a']))
7534+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
7535+
self.assertEqual(Child.__optional_keys__, frozenset(['b']))
7536+
7537+
def test_multiple_inheritance_with_same_key(self):
7538+
class Base1(TypedDict):
7539+
a: NotRequired[int]
7540+
7541+
class Base2(TypedDict):
7542+
a: Required[str]
7543+
7544+
class Child(Base1, Base2):
7545+
pass
7546+
7547+
# Last base wins
7548+
self.assertEqual(Child.__annotations__, {'a': Required[str]})
7549+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
7550+
self.assertEqual(Child.__optional_keys__, frozenset())
7551+
75127552
def test_required_notrequired_keys(self):
75137553
self.assertEqual(NontotalMovie.__required_keys__,
75147554
frozenset({"title"}))

Lib/typing.py

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

28472847
for base in bases:
28482848
annotations.update(base.__dict__.get('__annotations__', {}))
2849-
required_keys.update(base.__dict__.get('__required_keys__', ()))
2850-
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
2849+
2850+
base_required = base.__dict__.get('__required_keys__', set())
2851+
required_keys |= base_required
2852+
optional_keys -= base_required
2853+
2854+
base_optional = base.__dict__.get('__optional_keys__', set())
2855+
required_keys -= base_optional
2856+
optional_keys |= base_optional
28512857

28522858
annotations.update(own_annotations)
28532859
for annotation_key, annotation_type in own_annotations.items():
@@ -2859,14 +2865,23 @@ def __new__(cls, name, bases, ns, total=True):
28592865
annotation_origin = get_origin(annotation_type)
28602866

28612867
if annotation_origin is Required:
2862-
required_keys.add(annotation_key)
2868+
is_required = True
28632869
elif annotation_origin is NotRequired:
2864-
optional_keys.add(annotation_key)
2865-
elif total:
2870+
is_required = False
2871+
else:
2872+
is_required = total
2873+
2874+
if is_required:
28662875
required_keys.add(annotation_key)
2876+
optional_keys.discard(annotation_key)
28672877
else:
28682878
optional_keys.add(annotation_key)
2879+
required_keys.discard(annotation_key)
28692880

2881+
assert required_keys.isdisjoint(optional_keys), (
2882+
f"Required keys overlap with optional keys in {name}:"
2883+
f" {required_keys=}, {optional_keys=}"
2884+
)
28702885
tp_dict.__annotations__ = annotations
28712886
tp_dict.__required_keys__ = frozenset(required_keys)
28722887
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)