Skip to content

Commit

Permalink
[3.12] gh-112509: Fix keys being present in both required_keys and op…
Browse files Browse the repository at this point in the history
…tional_keys in TypedDict (GH-112512) (#112530)

(cherry picked from commit 4038869)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
  • Loading branch information
3 people committed Nov 29, 2023
1 parent 01b882b commit c678126
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 5 deletions.
40 changes: 40 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7509,6 +7509,46 @@ class Cat(Animal):
'voice': str,
})

def test_keys_inheritance_with_same_name(self):
class NotTotal(TypedDict, total=False):
a: int

class Total(NotTotal):
a: int

self.assertEqual(NotTotal.__required_keys__, frozenset())
self.assertEqual(NotTotal.__optional_keys__, frozenset(['a']))
self.assertEqual(Total.__required_keys__, frozenset(['a']))
self.assertEqual(Total.__optional_keys__, frozenset())

class Base(TypedDict):
a: NotRequired[int]
b: Required[int]

class Child(Base):
a: Required[int]
b: NotRequired[int]

self.assertEqual(Base.__required_keys__, frozenset(['b']))
self.assertEqual(Base.__optional_keys__, frozenset(['a']))
self.assertEqual(Child.__required_keys__, frozenset(['a']))
self.assertEqual(Child.__optional_keys__, frozenset(['b']))

def test_multiple_inheritance_with_same_key(self):
class Base1(TypedDict):
a: NotRequired[int]

class Base2(TypedDict):
a: Required[str]

class Child(Base1, Base2):
pass

# Last base wins
self.assertEqual(Child.__annotations__, {'a': Required[str]})
self.assertEqual(Child.__required_keys__, frozenset(['a']))
self.assertEqual(Child.__optional_keys__, frozenset())

def test_required_notrequired_keys(self):
self.assertEqual(NontotalMovie.__required_keys__,
frozenset({"title"}))
Expand Down
25 changes: 20 additions & 5 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2846,8 +2846,14 @@ def __new__(cls, name, bases, ns, total=True):

for base in bases:
annotations.update(base.__dict__.get('__annotations__', {}))
required_keys.update(base.__dict__.get('__required_keys__', ()))
optional_keys.update(base.__dict__.get('__optional_keys__', ()))

base_required = base.__dict__.get('__required_keys__', set())
required_keys |= base_required
optional_keys -= base_required

base_optional = base.__dict__.get('__optional_keys__', set())
required_keys -= base_optional
optional_keys |= base_optional

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

if annotation_origin is Required:
required_keys.add(annotation_key)
is_required = True
elif annotation_origin is NotRequired:
optional_keys.add(annotation_key)
elif total:
is_required = False
else:
is_required = total

if is_required:
required_keys.add(annotation_key)
optional_keys.discard(annotation_key)
else:
optional_keys.add(annotation_key)
required_keys.discard(annotation_key)

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

0 comments on commit c678126

Please sign in to comment.