8000 Implement support for PEP 764 (inline typed dictionaries) · python/typing_extensions@2fe24f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2fe24f0

Browse files
committed
Implement support for PEP 764 (inline typed dictionaries)
1 parent 8092c39 commit 2fe24f0

File tree

2 files changed

+110
-54
lines changed

2 files changed

+110
-54
lines changed

src/test_typing_extensions.py

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

5056+
def test_inlined_too_many_arguments(self):
5057+
with self.assertRaises(TypeError):
5058+
TypedDict[{"a": int}, "extra"]
5059+
5060+
def test_inlined_not_a_dict(self):
5061+
with self.assertRaises(TypeError):
5062+
TypedDict["not_a_dict"]
5063+
5064+
def test_inlined_empty(self):
5065+
TD = TypedDict[{}]
5066+
self.assertEqual(TD.__required_keys__, set())
5067+
5068+
def test_inlined(self):
5069+
TD = TypedDict[{
5070+
"a": int,
5071+
"b": Required[int],
5072+
"c": NotRequired[int],
5073+
"d": ReadOnly[int],
5074+
}]
5075+
self.assertIsSubclass(TD, dict)
5076+
self.assertIsSubclass(TD, typing.MutableMapping)
5077+
self.assertNotIsSubclass(TD, collections.abc.Sequence)
5078+
self.assertTrue(is_typeddict(TD))
5079+
self.assertEqual(TD.__name__, "<inlined TypedDict>")
5080+
self.assertEqual(TD.__module__, __name__)
5081+
self.assertEqual(TD.__bases__, (dict,))
5082+
self.assertEqual(TD.__total__, True)
5083+
self.assertEqual(TD.__required_keys__, {"a", "b", "d"})
5084+
self.assertEqual(TD.__optional_keys__, {"c"})
5085+
self.assertEqual(TD.__readonly_keys__, {"d"})
5086+
self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"})
5087+
5088+
inst = TD(a=1, b=2, d=3)
5089+
self.assertIs(type(inst), dict)
5090+
self.assertEqual(inst["a"], 1)
50565091

50575092
class AnnotatedTests(BaseTestCase):
50585093

src/typing_extensions.py

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pyright: ignore
12
import abc
23
import builtins
34
import collections
@@ -1105,17 +1106,73 @@ def __subclasscheck__(cls, other):
11051106

11061107
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
11071108

1109+
1110+
class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True):
1111+
def __call__(
1112+
self,
1113+
typename,
1114+
fields=_marker,
1115+
/,
1116+
*,
1117+
total=True,
1118+
closed=None,
1119+
extra_items=NoExtraItems,
1120+
__typing_is_inline__=False,
1121+
**kwargs
1122+
):
1123+
if fields is _marker or fields is None:
1124+
if fields is _marker:
1125+
deprecated_thing = (
1126+
"Failing to pass a value for the 'fields' parameter"
1127+
)
1128+
else:
1129+
deprecated_thing = "Passing `None` as the 'fields' parameter"
1130+
1131+
example = f"`{typename} = TypedDict({typename!r}, {{}})`"
1132+
deprecation_msg = (
1133+
f"{deprecated_thing} is deprecated and will be disallowed in "
1134+
"Python 3.15. To create a TypedDict class with 0 fields "
1135+
"using the functional syntax, pass an empty dictionary, e.g. "
1136+
) + example + "."
1137+
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1138+
# Support a field called "closed"
1139+
if closed is not False and closed is not True and closed is not None:
1140+
kwargs["closed"] = closed
1141+
closed = None
1142+
# Or "extra_items"
1143+
if extra_items is not NoExtraItems:
1144+
kwargs["extra_items"] = extra_items
1145+
extra_items = NoExtraItems
1146+
fields = kwargs
1147+
elif kwargs:
1148+
raise TypeError("TypedDict takes either a dict or keyword arguments,"
1149+
" but not both")
1150+
if kwargs:
1151+
if sys.version_info >= (3, 13):
1152+
raise TypeError("TypedDict takes no keyword arguments")
1153+
warnings.warn(
1154+
"The kwargs-based syntax for TypedDict definitions is deprecated "
1155+
"in Python 3.11, will be removed in Python 3.13, and may not be "
1156+
"understood by third-party type checkers.",
1157+
DeprecationWarning,
1158+
stacklevel=2,
1159+
)
1160+
1161+
ns = {'__annotations__': dict(fields)}
1162+
module = _caller(depth=5 if __typing_is_inline__ else 2)
1163+
if module is not None:
1164+
# Setting correct module is necessary to make typed dict classes
1165+
# pickleable.
1166+
ns['__module__'] = module
1167+
1168+
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1169+
extra_items=extra_items)
1170+
td.__orig_bases__ = (TypedDict,)
1171+
return td
1172+
11081173
@_ensure_subclassable(lambda bases: (_TypedDict,))
1109-
def TypedDict(
1110-
typename,
1111-
fields=_marker,
1112-
/,
1113-
*,
1114-
total=True,
1115-
closed=None,
1116-
extra_items=NoExtraItems,
1117-
**kwargs
1118-
):
1174+
@_TypedDictSpecialForm
1175+
def TypedDict(self, args):
11191176
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
11201177
11211178
TypedDict creates a dictionary type such that a type checker will expect all
@@ -1162,52 +1219,16 @@ class Point2D(TypedDict):
11621219
11631220
See PEP 655 for more details on Required and NotRequired.
11641221
"""
1165-
if fields is _marker or fields is None:
1166-
if fields is _marker:
1167-
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
1168-
else:
1169-
deprecated_thing = "Passing `None` as the 'fields' parameter"
1170-
1171-
example = f"`{typename} = TypedDict({typename!r}, {{}})`"
1172-
deprecation_msg = (
1173-
f"{deprecated_thing} is deprecated and will be disallowed in "
1174-
"Python 3.15. To create a TypedDict class with 0 fields "
1175-
"using the functional syntax, pass an empty dictionary, e.g. "
1176-
) + example + "."
1177-
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1178-
# Support a field called "closed"
1179-
if closed is not False and closed is not True and closed is not None:
1180-
kwargs["closed"] = closed
1181-
closed = None
1182-
# Or "extra_items"
1183-
if extra_items is not NoExtraItems:
1184-
kwargs["extra_items"] = extra_items
1185-
extra_items = NoExtraItems
1186-
fields = kwargs
1187-
elif kwargs:
1188-
raise TypeError("TypedDict takes either a dict or keyword arguments,"
1189-
" but not both")
1190-
if kwargs:
1191-
if sys.version_info >= (3, 13):
1192-
raise TypeError("TypedDict takes no keyword arguments")
1193-
warnings.warn(
1194-
"The kwargs-based syntax for TypedDict definitions is deprecated "
1195-
"in Python 3.11, will be removed in Python 3.13, and may not be "
1196-
"understood by third-party type checkers.",
1197-
DeprecationWarning,
1198-
stacklevel=2,
1222+
# This runs when creating inline TypedDicts:
1223+
if not isinstance(args, tuple):
1224+
args = (args,)
1225+
if len(args) != 1 or not isinstance(args[0], dict):
1226+
raise TypeError(
1227+
"TypedDict[...] should be used with a single dict argument"
11991228
)
12001229

1201-
ns = {'__annotations__': dict(fields)}
1202-
module = _caller()
1203-
if module is not None:
1204-
# Setting correct module is necessary to make typed dict classes pickleable.
1205-
ns['__module__'] = module
1206-
1207-
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1208-
extra_items=extra_items)
1209-
td.__orig_bases__ = (TypedDict,)
1210-
return td
1230+
# Delegate to _TypedDictSpecialForm.__call__:
1231+
return self("<inlined TypedDict>", args[0], __typing_is_inline__=True)
12111232

12121233
if hasattr(typing, "_TypedDictMeta"):
12131234
_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)

0 commit comments

Comments
 (0)
0