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

Skip to content

Commit 09bfecc

Browse files
committed
Implement support for PEP 764 (inlined typed dictionaries)
1 parent 8092c39 commit 09bfecc

File tree

2 files changed

+118
-65
lines changed

2 files changed

+118
-65
lines changed

src/test_typing_extensions.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5053,6 +5053,38 @@ 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(self):
5065+
TD = TypedDict[{
5066+
"a": int,
5067+
"b": Required[int],
5068+
"c": NotRequired[int],
5069+
"d": ReadOnly[int],
5070+
}]
5071+
self.assertIsSubclass(TD, dict)
5072+
self.assertIsSubclass(TD, typing.MutableMapping)
5073+
self.assertNotIsSubclass(TD, collections.abc.Sequence)
5074+
self.assertTrue(is_typeddict(TD))
5075+
self.assertEqual(TD.__name__, "<inlined TypedDict>")
5076+
self.assertEqual(TD.__module__, __name__)
5077+
self.assertEqual(TD.__bases__, (dict,))
5078+
self.assertEqual(TD.__total__, True)
5079+
self.assertEqual(TD.__required_keys__, {"a", "b", "d"})
5080+
self.assertEqual(TD.__optional_keys__, {"c"})
5081+
self.assertEqual(TD.__readonly_keys__, {"d"})
5082+
self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"})
5083+
5084+
inst = TD(a=1, b=2, d=3)
5085+
self.assertIs(type(inst), dict)
5086+
self.assertEqual(inst["a"], 1)
5087+
50565088

50575089
class AnnotatedTests(BaseTestCase):
50585090

@@ -6622,7 +6654,7 @@ def test_typing_extensions_defers_when_possible(self):
66226654
exclude |= {
66236655
'TypeAliasType'
66246656
}
6625-
if not typing_extensions._PEP_728_IMPLEMENTED:
6657+
if not typing_extensions._PEP_728_OR_764_IMPLEMENTED:
66266658
exclude |= {'TypedDict', 'is_typeddict'}
66276659
for item in typing_extensions.__all__:
66286660
if item not in exclude and hasattr(typing, item):

src/typing_extensions.py

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pyright: ignore[reportShadowedImports]
12
import abc
23
import builtins
34
import collections
@@ -935,11 +936,11 @@ def __reduce__(self):
935936
del SingletonMeta
936937

937938

938-
# Update this to something like >=3.13.0b1 if and when
939-
# PEP 728 is implemented in CPython
940-
_PEP_728_IMPLEMENTED = False
939+
# Update this to something like >=3.14 if and when
940+
# PEP 728/PEP 764 is implemented in CPython
941+
_PEP_728_OR_764_IMPLEMENTED = False
941942

942-
if _PEP_728_IMPLEMENTED:
943+
if _PEP_728_OR_764_IMPLEMENTED:
943944
# The standard library TypedDict in Python 3.8 does not store runtime information
944945
# about which (if any) keys are optional. See https://bugs.python.org/issue38834
945946
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
@@ -951,7 +952,7 @@ def __reduce__(self):
951952
# to enable better runtime introspection.
952953
# On 3.13 we deprecate some odd ways of creating TypedDicts.
953954
# Also on 3.13, PEP 705 adds the ReadOnly[] qualifier.
954-
# PEP 728 (still pending) makes more changes.
955+
# PEP 728 and PEP 764 (still pending) makes more changes.
955956
TypedDict = typing.TypedDict
956957
_TypedDictMeta = typing._TypedDictMeta
957958
is_typeddict = typing.is_typeddict
@@ -1095,7 +1096,11 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10951096
tp_dict.__extra_items__ = extra_items_type
10961097
return tp_dict
10971098

1098-
__call__ = dict # static method
1099+
def __call__(cls, /, *args, **kwargs):
1100+
if cls is TypedDict:
1101+
# Functional syntax, let `TypedDict.__new__` handle it:
1102+
return super().__call__(*args, **kwargs)
1103+
return dict(*args, **kwargs)
10991104

11001105
def __subclasscheck__(cls, other):
11011106
# Typed dicts are only for static structural subtyping.
@@ -1105,17 +1110,7 @@ def __subclasscheck__(cls, other):
11051110

11061111
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
11071112

1108-
@_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-
):
1113+
class TypedDict(metaclass=_TypedDictMeta):
11191114
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
11201115
11211116
TypedDict creates a dictionary type such that a type checker will expect all
@@ -1162,52 +1157,77 @@ class Point2D(TypedDict):
11621157
11631158
See PEP 655 for more details on Required and NotRequired.
11641159
"""
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"
11701160

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,
1199-
)
1161+
def __new__(
1162+
cls,
1163+
typename,
1164+
fields=_marker,
1165+
/,
1166+
*,
1167+
total=True,
1168+
closed=None,
1169+
extra_items=NoExtraItems,
1170+
**kwargs
1171+
):
1172+
if fields is _marker or fields is None:
1173+
if fields is _marker:
1174+
deprecated_thing = (
1175+
"Failing to pass a value for the 'fields' parameter"
1176+
)
1177+
else:
1178+
deprecated_thing = "Passing `None` as the 'fields' parameter"
1179+
1180+
example = f"`{typename} = TypedDict({typename!r}, {{}})`"
1181+
deprecation_msg = (
1182+
f"{deprecated_thing} is deprecated and will be disallowed in "
1183+
"Python 3.15. To create a TypedDict class with 0 fields "
1184+
"using the functional syntax, pass an empty dictionary, e.g. "
1185+
) + example + "."
1186+
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1187+
# Support a field called "closed"
1188+
if closed is not False and closed is not True and closed is not None:
1189+
kwargs["closed"] = closed
1190+
closed = None
1191+
# Or "extra_items"
1192+
if extra_items is not NoExtraItems:
1193+
kwargs["extra_items"] = extra_items
1194+
extra_items = NoExtraItems
1195+
fields = kwargs
1196+
elif kwargs:
1197+
raise TypeError("TypedDict takes either a dict or keyword arguments,"
1198+
" but not both")
1199+
if kwargs:
1200+
if sys.version_info >= (3, 13):
1201+
raise TypeError("TypedDict takes no keyword arguments")
1202+
warnings.warn(
1203+
"The kwargs-based syntax for TypedDict definitions is deprecated "
1204+
"in Python 3.11, will be removed in Python 3.13, and may not be "
1205+
"understood by third-party type checkers.",
1206+
DeprecationWarning,
1207+
stacklevel=2,
1208+
)
12001209

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
1210+
ns = {'__annotations__': dict(fields)}
1211+
module = _caller(depth=3)
1212+
if module is not None:
1213+
# Setting correct module is necessary to make typed dict classes
1214+
# pickleable.
1215+
ns['__module__'] = module
12061216

1207-
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1208-
extra_items=extra_items)
1209-
td.__orig_bases__ = (TypedDict,)
1210-
return td
1217+
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
1218+
extra_items=extra_items)
1219+
td.__orig_bases__ = (TypedDict,)
1220+
return td
1221+
1222+
def __class_getitem__(cls, args):
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"
1228+
)
1229+
1230+
return cls.__new__(cls, "<inlined TypedDict>", args[0])
12111231

12121232
if hasattr(typing, "_TypedDictMeta"):
12131233
_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
@@ -1225,10 +1245,11 @@ class Film(TypedDict):
12251245
is_typeddict(Film) # => True
12261246
is_typeddict(Union[list, str]) # => False
12271247
"""
1228-
# On 3.8, this would otherwise return True
1229-
if hasattr(typing, "TypedDict") and tp is typing.TypedDict:
1230-
return False
1231-
return isinstance(tp, _TYPEDDICT_TYPES)
1248+
return (
1249+
tp is not TypedDict
1250+
and tp is not typing.TypedDict
1251+
and isinstance(tp, _TYPEDDICT_TYPES)
1252+
)
12321253

12331254

12341255
if hasattr(typing, "assert_type"):

0 commit comments

Comments
 (0)
0