Better runtime TypedDict (GH-17214)

This patch enables downstream projects inspecting a TypedDict subclass at runtime to tell which keys are optional.

This is essential for generating test data with Hypothesis or validating inputs with typeguard or pydantic.
This commit is contained in:
Zac Hatfield-Dodds 2019-11-24 21:48:48 +11:00 committed by Ivan Levkivskyi
parent 041d8b48a2
commit 665ad3dfa9
3 changed files with 25 additions and 3 deletions

View File

@ -3741,6 +3741,13 @@ class TypedDictTests(BaseTestCase):
self.assertEqual(Options(log_level=2), {'log_level': 2})
self.assertEqual(Options.__total__, False)
def test_optional_keys(self):
class Point2Dor3D(Point2D, total=False):
z: int
assert Point2Dor3D.__required_keys__ == frozenset(['x', 'y'])
assert Point2Dor3D.__optional_keys__ == frozenset(['z'])
class IOTests(BaseTestCase):

View File

@ -1715,9 +1715,20 @@ class _TypedDictMeta(type):
anns = ns.get('__annotations__', {})
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
anns = {n: _type_check(tp, msg) for n, tp in anns.items()}
required = set(anns if total else ())
optional = set(() if total else anns)
for base in bases:
anns.update(base.__dict__.get('__annotations__', {}))
base_anns = base.__dict__.get('__annotations__', {})
anns.update(base_anns)
if getattr(base, '__total__', True):
required.update(base_anns)
else:
optional.update(base_anns)
tp_dict.__annotations__ = anns
tp_dict.__required_keys__ = frozenset(required)
tp_dict.__optional_keys__ = frozenset(optional)
if not hasattr(tp_dict, '__total__'):
tp_dict.__total__ = total
return tp_dict
@ -1744,8 +1755,9 @@ class TypedDict(dict, metaclass=_TypedDictMeta):
assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
The type info can be accessed via Point2D.__annotations__. TypedDict
supports two additional equivalent forms::
The type info can be accessed via the Point2D.__annotations__ dict, and
the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
TypedDict supports two additional equivalent forms::
Point2D = TypedDict('Point2D', x=int, y=int, label=str)
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})

View File

@ -0,0 +1,3 @@
:class:`typing.TypedDict` subclasses now track which keys are optional using
the ``__required_keys__`` and ``__optional_keys__`` attributes, to enable
runtime validation by downstream projects. Patch by Zac Hatfield-Dodds.