Skip to content

Commit 7379a2a

Browse files
committed
Implement support for PEP 764 (inline typed dictionaries)
1 parent 7cfb2c0 commit 7379a2a

File tree

2 files changed

+109
-54
lines changed

2 files changed

+109
-54
lines changed

src/test_typing_extensions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5066,6 +5066,41 @@ def test_cannot_combine_closed_and_extra_items(self):
50665066
class TD(TypedDict, closed=True, extra_items=range):
50675067
x: str
50685068

5069+
def test_inlined_too_many_arguments(self):
5070+
with self.assertRaises(TypeError):
5071+
TypedDict[{"a": int}, "extra"]
5072+
5073+
def test_inlined_not_a_dict(self):
5074+
with self.assertRaises(TypeError):
5075+
TypedDict["not_a_dict"]
5076+
5077+
def test_inlined_empty(self):
5078+
TD = TypedDict[{}]
5079+
self.assertEqual(TD.__required_keys__, set())
5080+
5081+
def test_inlined(self):
5082+
TD = TypedDict[{
5083+
"a": int,
5084+
"b": Required[int],
5085+
"c": NotRequired[int],
5086+
"d": ReadOnly[int],
5087+
}]
5088+
self.assertIsSubclass(TD, dict)
5089+
self.assertIsSubclass(TD, typing.MutableMapping)
5090+
self.assertNotIsSubclass(TD, collections.abc.Sequence)
5091+
self.assertTrue(is_typeddict(TD))
5092+
self.assertEqual(TD.__name__, "<inlined TypedDict>")
5093+
self.assertEqual(TD.__module__, __name__)
5094+
self.assertEqual(TD.__bases__, (dict,))
5095+
self.assertEqual(TD.__total__, True)
5096+
self.assertEqual(TD.__required_keys__, {"a", "b", "d"})
5097+
self.assertEqual(TD.__optional_keys__, {"c"})
5098+
self.assertEqual(TD.__readonly_keys__, {"d"})
5099+
self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"})
5100+
5101+
inst = TD(a=1, b=2, d=3)
5102+
self.assertIs(type(inst), dict)
5103+
self.assertEqual(inst["a"], 1)
50695104

50705105
class AnnotatedTests(BaseTestCase):
50715106

src/typing_extensions.py

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,17 +1078,73 @@ def __subclasscheck__(cls, other):
10781078

10791079
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
10801080

1081+
1082+
class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True):
1083+
def __call__(
1084+
self,
1085+
typename,
1086+
fields=_marker,
1087+
/,
1088+
*,
1089+
total=True,
1090+
closed=None,
1091+
extra_items=NoExtraItems,
1092+
__typing_is_inline__=False,
1093+
**kwargs
1094+
):
1095+
if fields is _marker or fields is None:
1096+
if fields is _marker:
1097+
deprecated_thing = (
1098+
"Failing to pass a value for the 'fields' parameter"
1099+
)
1100+
else:
1101+
deprecated_thing = "Passing `None` as the 'fields' parameter"
1102+
1103+
example = f"`{typename} = TypedDict({typename!r}, {{}})`"
1104+
deprecation_msg = (
1105+
f"{deprecated_thing} is deprecated and will be disallowed in "
1106+
"Python 3.15. To create a TypedDict class with 0 fields "
1107+
"using the functional syntax, pass an empty dictionary, e.g. "
1108+
) + example + "."
1109+
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1110+
# Support a field called "closed"
1111+
if closed is not False and closed is not True and closed is not None:
1112+
kwargs["closed"] = closed
1113+
closed = None
1114+
# Or "extra_items"
1115+
if extra_items is not NoExtraItems:
1116+
kwargs["extra_items"] = extra_items
1117+
extra_items = NoExtraItems
1118+
fields = kwargs
1119+
elif kwargs:
1120+
raise TypeError("TypedDict takes either a dict or keyword arguments,"
1121+
" but not both")
1122+
if kwargs:
1123+
if sys.version_info >= (3, 13):
1124+
raise TypeError("TypedDict takes no keyword arguments")
1125+
warnings.warn(
1126+
"The kwargs-based syntax for TypedDict definitions is deprecated "
1127+
"in Python 3.11, will be removed in Python 3.13, and may not be "
1128+
"understood by third-party type checkers.",
1129+
DeprecationWarning,
1130+
stacklevel=2,
1131+
)
1132+
1133+
ns = {'__annotations__': dict(fields)}
1134+
module = _caller(depth=5 if __typing_is_inline__ else 2)
1135+
if module is not None:
1136+
# Setting correct module is necessary to make typed dict classes
1137+
# pickleable.
1138+
ns['__module__'] = module
1139+
1140+
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1141+
extra_items=extra_items)
1142+
td.__orig_bases__ = (TypedDict,)
1143+
return td
1144+
10811145
@_ensure_subclassable(lambda bases: (_TypedDict,))
1082-
def TypedDict(
1083-
typename,
1084-
fields=_marker,
1085-
/,
1086-
*,
1087-
total=True,
1088-
closed=None,
1089-
extra_items=NoExtraItems,
1090-
**kwargs
1091-
):
1146+
@_TypedDictSpecialForm
1147+
def TypedDict(self, args):
10921148
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
10931149
10941150
TypedDict creates a dictionary type such that a type checker will expect all
@@ -1135,52 +1191,16 @@ class Point2D(TypedDict):
11351191
11361192
See PEP 655 for more details on Required and NotRequired.
11371193
"""
1138-
if fields is _marker or fields is None:
1139-
if fields is _marker:
1140-
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
1141-
else:
1142-
deprecated_thing = "Passing `None` as the 'fields' parameter"
1143-
1144-
example = f"`{typename} = TypedDict({typename!r}, {{}})`"
1145-
deprecation_msg = (
1146-
f"{deprecated_thing} is deprecated and will be disallowed in "
1147-
"Python 3.15. To create a TypedDict class with 0 fields "
1148-
"using the functional syntax, pass an empty dictionary, e.g. "
1149-
) + example + "."
1150-
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1151-
# Support a field called "closed"
1152-
if closed is not False and closed is not True and closed is not None:
1153-
kwargs["closed"] = closed
1154-
closed = None
1155-
# Or "extra_items"
1156-
if extra_items is not NoExtraItems:
1157-
kwargs["extra_items"] = extra_items
1158-
extra_items = NoExtraItems
1159-
fields = kwargs
1160-
elif kwargs:
1161-
raise TypeError("TypedDict takes either a dict or keyword arguments,"
1162-
" but not both")
1163-
if kwargs:
1164-
if sys.version_info >= (3, 13):
1165-
raise TypeError("TypedDict takes no keyword arguments")
1166-
warnings.warn(
1167-
"The kwargs-based syntax for TypedDict definitions is deprecated "
1168-
"in Python 3.11, will be removed in Python 3.13, and may not be "
1169-
"understood by third-party type checkers.",
1170-
DeprecationWarning,
1171-
stacklevel=2,
1194+
# This runs when creating inline TypedDicts:
1195+
if not isinstance(args, tuple):
1196+
args = (args,)
1197+
if len(args) != 1 or not isinstance(args[0], dict):
1198+
raise TypeError(
1199+
"TypedDict[...] should be used with a single dict argument"
11721200
)
11731201

1174-
ns = {'__annotations__': dict(fields)}
1175-
module = _caller()
1176-
if module is not None:
1177-
# Setting correct module is necessary to make typed dict classes pickleable.
1178-
ns['__module__'] = module
1179-
1180-
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1181-
extra_items=extra_items)
1182-
td.__orig_bases__ = (TypedDict,)
1183-
return td
1202+
# Delegate to _TypedDictSpecialForm.__call__:
1203+
return self("<inlined TypedDict>", args[0], __typing_is_inline__=True)
11841204

11851205
_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
11861206

0 commit comments

Comments
 (0)