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

Skip to content

Commit 28f08ac

Browse files
authored
Implement support for PEP 764 (inline typed dictionaries) (#580)
1 parent 7ab72d7 commit 28f08ac

File tree

3 files changed

+157
-59
lines changed

3 files changed

+157
-59
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Unreleased
22

33
- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos).
4+
- Add support for inline typed dictionaries ([PEP 764](https://peps.python.org/pep-0764/)).
5+
Patch by [Victorien Plot](https://github.com/Viicos).
46

57
# Release 4.13.2 (April 10, 2025)
68

src/test_typing_extensions.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5066,6 +5066,63 @@ 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_typed_dict_signature(self):
5070+
self.assertListEqual(
5071+
list(inspect.signature(TypedDict).parameters),
5072+
['typename', 'fields', 'total', 'closed', 'extra_items', 'kwargs']
5073+
)
5074+
5075+
def test_inline_too_many_arguments(self):
5076+
with self.assertRaises(TypeError):
5077+
TypedDict[{"a": int}, "extra"]
5078+
5079+
def test_inline_not_a_dict(self):
5080+
with self.assertRaises(TypeError):
5081+
TypedDict["not_a_dict"]
5082+
5083+
# a tuple of elements isn't allowed, even if the first element is a dict:
5084+
with self.assertRaises(TypeError):
5085+
TypedDict[({"key": int},)]
5086+
5087+
def test_inline_empty(self):
5088+
TD = TypedDict[{}]
5089+
self.assertIs(TD.__total__, True)
5090+
self.assertIs(TD.__closed__, True)
5091+
self.assertEqual(TD.__extra_items__, NoExtraItems)
5092+
self.assertEqual(TD.__required_keys__, set())
5093+
self.assertEqual(TD.__optional_keys__, set())
5094+
self.assertEqual(TD.__readonly_keys__, set())
5095+
self.assertEqual(TD.__mutable_keys__, set())
5096+
5097+
def test_inline(self):
5098+
TD = TypedDict[{
5099+
"a": int,
5100+
"b": Required[int],
5101+
"c": NotRequired[int],
5102+
"d": ReadOnly[int],
5103+
}]
5104+
self.assertIsSubclass(TD, dict)
5105+
self.assertIsSubclass(TD, typing.MutableMapping)
5106+
self.assertNotIsSubclass(TD, collections.abc.Sequence)
5107+
self.assertTrue(is_typeddict(TD))
5108+
self.assertEqual(TD.__name__, "<inline TypedDict>")
5109+
self.assertEqua 6D40 l(
5110+
TD.__annotations__,
5111+
{"a": int, "b": Required[int], "c": NotRequired[int], "d": ReadOnly[int]},
5112+
)
5113+
self.assertEqual(TD.__module__, __name__)
5114+
self.assertEqual(TD.__bases__, (dict,))
5115+
self.assertIs(TD.__total__, True)
5116+
self.assertIs(TD.__closed__, True)
5117+
self.assertEqual(TD.__extra_items__, NoExtraItems)
5118+
self.assertEqual(TD.__required_keys__, {"a", "b", "d"})
5119+
self.assertEqual(TD.__optional_keys__, {"c"})
5120+
self.assertEqual(TD.__readonly_keys__, {"d"})
5121+
self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"})
5122+
5123+
inst = TD(a=1, b=2, d=3)
5124+
self.assertIs(type(inst), dict)
5125+
self.assertEqual(inst["a"], 1)
50695126

50705127
class AnnotatedTests(BaseTestCase):
50715128

src/typing_extensions.py

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -846,13 +846,6 @@ def __round__(self, ndigits: int = 0) -> T_co:
846846
pass
847847

848848

849-
def _ensure_subclassable(mro_entries):
850-
def inner(obj):
851-
obj.__mro_entries__ = mro_entries
852-
return obj
853-
return inner
854-
855-
856849
_NEEDS_SINGLETONMETA = (
857850
not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems")
858851
)
@@ -1078,17 +1071,94 @@ def __subclasscheck__(cls, other):
10781071

10791072
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
10801073

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

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
1214+
return _create_typeddict(
1215+
"<inline TypedDict>",
1216+
args,
1217+
typing_is_inline=True,
1218+
total=True,
1219+
closed=True,
1220+
extra_items=NoExtraItems,
1221+
)
11841222

11851223
_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
11861224

@@ -3194,7 +3232,6 @@ def _namedtuple_mro_entries(bases):
31943232
assert NamedTuple in bases
31953233
return (_NamedTuple,)
31963234

3197-
@_ensure_subclassable(_namedtuple_mro_entries)
31983235
def NamedTuple(typename, fields=_marker, /, **kwargs):
31993236
"""Typed version of namedtuple.
32003237
@@ -3260,6 +3297,8 @@ class Employee(NamedTuple):
32603297
nt.__orig_bases__ = (NamedTuple,)
32613298
return nt
32623299

3300+
NamedTuple.__mro_entries__ = _namedtuple_mro_entries
3301+
32633302

32643303
if hasattr(collections.abc, "Buffer"):
32653304
Buffer = collections.abc.Buffer

0 commit comments

Comments
 (0)
0