]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Better runtime TypedDict (GH-17214)
authorZac Hatfield-Dodds <Zac-HD@users.noreply.github.com>
Sun, 24 Nov 2019 10:48:48 +0000 (21:48 +1100)
committerIvan Levkivskyi <levkivskyi@gmail.com>
Sun, 24 Nov 2019 10:48:48 +0000 (10:48 +0000)
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.

Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2019-11-18-17-08-23.bpo-38834.abcdef.rst [new file with mode: 0644]

index ccd617c1fdff0651abbc56f01f81e8312bc1363b..5b4916f9c3260b7a054dfa84bdbd7d7e61d384fc 100644 (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):
 
index 5523ee01e1f99fda868df6004b300bb9db7456a5..7de3e346eaa79a4c069d6ea7443d24a8df7ca366 100644 (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})
diff --git a/Misc/NEWS.d/next/Library/2019-11-18-17-08-23.bpo-38834.abcdef.rst b/Misc/NEWS.d/next/Library/2019-11-18-17-08-23.bpo-38834.abcdef.rst
new file mode 100644 (file)
index 0000000..af108b1
--- /dev/null
@@ -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.