summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorZac Hatfield-Dodds <Zac-HD@users.noreply.github.com>2019-11-24 10:48:48 (GMT)
committerIvan Levkivskyi <levkivskyi@gmail.com>2019-11-24 10:48:48 (GMT)
commit665ad3dfa9993b9a4000b097ddead4e292590e8c (patch)
tree886741ee4ffc36e769575e51b5e576d1f8e22b02
parent041d8b48a2e59fa642b2c5124d78086baf74e339 (diff)
downloadcpython-665ad3dfa9993b9a4000b097ddead4e292590e8c.zip
cpython-665ad3dfa9993b9a4000b097ddead4e292590e8c.tar.gz
cpython-665ad3dfa9993b9a4000b097ddead4e292590e8c.tar.bz2
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.
-rw-r--r--Lib/test/test_typing.py7
-rw-r--r--Lib/typing.py18
-rw-r--r--Misc/NEWS.d/next/Library/2019-11-18-17-08-23.bpo-38834.abcdef.rst3
3 files changed, 25 insertions, 3 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index ccd617c..5b4916f 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -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):
diff --git a/Lib/typing.py b/Lib/typing.py
index 5523ee0..7de3e34 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -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
index 0000000..af108b1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-11-18-17-08-23.bpo-38834.abcdef.rst
@@ -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.