Skip to content

Commit 82d0984

Browse files
committed
Support TypedDict field as Dict[str, Any]
1 parent ca7b924 commit 82d0984

File tree

3 files changed

+50
-1
lines changed

3 files changed

+50
-1
lines changed

changelog.d/237.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `TypedDict` subclass support to fields. These are treated the same as `Dict[str, Any]`.

src/desert/_make.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,18 @@ def field_for_schema(
305305
field = field_for_schema(newtype_supertype, default=default)
306306

307307
# enumerations
308-
if type(typ) is enum.EnumMeta:
308+
elif type(typ) is enum.EnumMeta:
309309
import marshmallow_enum
310310

311311
field = marshmallow_enum.EnumField(typ, metadata=metadata)
312312

313+
# TypedDict
314+
elif _is_typeddict(typ):
315+
field = marshmallow.fields.Dict(
316+
keys=marshmallow.fields.String,
317+
values=marshmallow.fields.Raw,
318+
)
319+
313320
# Nested dataclasses
314321
forward_reference = getattr(typ, "__forward_arg__", None)
315322

@@ -370,6 +377,18 @@ def _get_field_default(
370377
raise TypeError(field)
371378

372379

380+
def _is_typeddict(typ: t.Any) -> bool:
381+
# typing_inspect misses some case.
382+
# python>=3.10: use t.is_typeddict
383+
if hasattr(t, "is_typeddict"):
384+
return t.is_typeddict(typ)
385+
# python>=3.8; <3.10: Reimplement t.is_typeddict
386+
if hasattr(t, "_TypedDictMeta"):
387+
return isinstance(typ, t._TypedDictMeta) # type: ignore[attr-defined]
388+
# Fallback to typing_inspect
389+
return typing_inspect.typed_dict_keys(typ) is not None
390+
391+
373392
@attr.frozen
374393
class _DesertSentinel:
375394
pass

tests/test_make.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ def dataclass_param(request: _pytest.fixtures.SubRequest) -> DataclassModule:
4545
return module
4646

4747

48+
@pytest.fixture
49+
def typeddict() -> None:
50+
try:
51+
from typing import TypedDict
52+
except ImportError:
53+
raise pytest.skip("No TypedDict support")
54+
55+
4856
class AssertLoadDumpProtocol(typing_extensions.Protocol):
4957
def __call__(
5058
self, schema: marshmallow.Schema, loaded: t.Any, dumped: t.Dict[t.Any, t.Any]
@@ -437,6 +445,27 @@ class A:
437445
assert_dump_load(schema=schema, loaded=loaded, dumped=dumped)
438446

439447

448+
def test_typeddict(
449+
module: DataclassModule,
450+
assert_dump_load: AssertLoadDumpProtocol,
451+
typeddict: None,
452+
) -> None:
453+
"""Test dataclasses with basic TypedDict support"""
454+
455+
class B(t.TypedDict):
456+
x: int
457+
458+
@module.dataclass
459+
class A:
460+
x: B
461+
462+
schema = desert.schema_class(A)()
463+
dumped = {"x": {"x": 1}}
464+
loaded = A(x={"x": 1}) # type: ignore[call-arg]
465+
466+
assert_dump_load(schema=schema, loaded=loaded, dumped=dumped)
467+
468+
440469
@pytest.mark.xfail(
441470
strict=True,
442471
reason=(

0 commit comments

Comments
 (0)