Skip to content

Commit 533b09c

Browse files
committed
Implement NewTypedDict and strip TypedDict quals in Attrs/Members
- Add NewTypedDict type and evaluator that creates TypedDicts from Member arguments, mirroring NewProtocol - Strip NotRequired/ReadOnly/Required from type annotations in get_annotated_type_hints and move them to quals (Required is stripped silently since it's the default)
1 parent d8f21ca commit 533b09c

3 files changed

Lines changed: 62 additions & 12 deletions

File tree

tests/test_ts_utility.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ class TodoTD(TypedDict):
7070
# PartialTD<T>
7171
# Like Partial, but for TypedDicts: wraps all fields in NotRequired
7272
# rather than making them T | None.
73-
type PartialTD[T] = typing.NewProtocol[
73+
type PartialTD[T] = typing.NewTypedDict[
7474
*[
75-
typing.Member[p.name, NotRequired[p.type], p.quals]
75+
typing.Member[p.name, p.type, p.quals | Literal["NotRequired"]]
7676
for p in typing.Iter[typing.Attrs[T]]
7777
]
7878
]
@@ -112,10 +112,10 @@ class Partial[tests.test_ts_utility.Todo]:
112112

113113
def test_partial_td():
114114
tgt = eval_typing(PartialTD[TodoTD])
115-
fmt = format_helper.format_class(tgt)
116-
assert fmt == textwrap.dedent("""\
117-
class PartialTD[tests.test_ts_utility.TodoTD]:
118-
title: typing.NotRequired[str]
119-
description: typing.NotRequired[str]
120-
completed: typing.NotRequired[bool]
121-
""")
115+
assert tgt.__required_keys__ == frozenset()
116+
assert tgt.__optional_keys__ == {"title", "description", "completed"}
117+
assert tgt.__annotations__ == {
118+
"title": NotRequired[str],
119+
"description": NotRequired[str],
120+
"completed": NotRequired[bool],
121+
}

typemap/type_eval/_eval_operators.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
Member,
4242
Members,
4343
NewProtocol,
44+
NewTypedDict,
4445
Overloaded,
4546
Param,
4647
RaiseError,
@@ -116,11 +117,20 @@ def get_annotated_type_hints(cls, *, ctx, attrs_only=False, **kwargs):
116117
for k, ty in annos.items():
117118
quals = set()
118119

119-
# Strip ClassVar/Final from ty and add them to quals
120+
# Strip ClassVar/Final/NotRequired/ReadOnly/Required from ty
121+
# and add them to quals
120122
while True:
121-
for form in [typing.ClassVar, typing.Final]:
123+
for form in [
124+
typing.ClassVar,
125+
typing.Final,
126+
typing.NotRequired,
127+
typing.ReadOnly,
128+
typing.Required,
129+
]:
122130
if _typing_inspect.is_special_form(ty, form):
123-
quals.add(form.__name__)
131+
# Required is the default; strip but don't add a qual
132+
if form is not typing.Required:
133+
quals.add(form.__name__)
124134
ty = (
125135
typing.get_args(ty)[0]
126136
if typing.get_args(ty)
@@ -1282,3 +1292,39 @@ def _eval_NewProtocol(*etyps: Member, ctx):
12821292
cls.__init__ = dct['__init__']
12831293

12841294
return cls
1295+
1296+
1297+
def _add_td_quals(typ, quals):
1298+
for qual in (typing.NotRequired, typing.ReadOnly):
1299+
if type_eval.issubtype(typing.Literal[qual.__name__], quals):
1300+
typ = qual[typ]
1301+
return typ
1302+
1303+
1304+
@type_eval.register_evaluator(NewTypedDict)
1305+
@_lift_evaluated
1306+
def _eval_NewTypedDict(*etyps: Member, ctx):
1307+
annos = {}
1308+
1309+
members = [typing.get_args(prop) for prop in etyps]
1310+
for tname, typ, quals, _init, _ in members:
1311+
name = _eval_literal(tname, ctx)
1312+
typ = _eval_types(typ, ctx)
1313+
tquals = _eval_types(quals, ctx)
1314+
annos[name] = _add_td_quals(typ, tquals)
1315+
1316+
td_name = "NewTypedDict"
1317+
module_name = __name__
1318+
1319+
ctx = type_eval._get_current_context()
1320+
if ctx.current_generic_alias:
1321+
if isinstance(ctx.current_generic_alias, types.GenericAlias):
1322+
td_name = str(ctx.current_generic_alias)
1323+
else:
1324+
td_name = f"{ctx.current_generic_alias.__name__}[...]"
1325+
module_name = ctx.current_generic_alias.__module__
1326+
1327+
cls = typing.TypedDict(td_name, annos) # type: ignore[misc]
1328+
cls.__module__ = module_name
1329+
1330+
return cls

typemap/typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ class NewProtocol[*T]:
292292
pass
293293

294294

295+
class NewTypedDict[*T]:
296+
pass
297+
298+
295299
class UpdateClass[*Ms]:
296300
pass
297301

0 commit comments

Comments
 (0)