Skip to content

Commit d25bd14

Browse files
authored
fix: dump NamedTuple to dict instead of list (#1037)
1 parent cffaf6d commit d25bd14

File tree

2 files changed

+26
-21
lines changed

2 files changed

+26
-21
lines changed

python/cocoindex/convert.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,16 @@ def dump_engine_object(v: Any) -> Any:
625625
secs = int(total_secs)
626626
nanos = int((total_secs - secs) * 1e9)
627627
return {"secs": secs, "nanos": nanos}
628+
elif is_namedtuple_type(type(v)):
629+
# Handle NamedTuple objects specifically to use dict format
630+
field_names = list(getattr(type(v), "_fields", ()))
631+
result = {}
632+
for name in field_names:
633+
val = getattr(v, name)
634+
result[name] = dump_engine_object(val) # Include all values, including None
635+
if hasattr(v, "kind") and "kind" not in result:
636+
result["kind"] = v.kind
637+
return result
628638
elif hasattr(v, "__dict__"): # for dataclass-like objects
629639
s = {}
630640
for k, val in v.__dict__.items():
@@ -712,32 +722,27 @@ def load_engine_object(expected_type: Any, v: Any) -> Any:
712722
if isinstance(variant, AnalyzedStructType):
713723
struct_type = variant.struct_type
714724
if dataclasses.is_dataclass(struct_type):
725+
if not isinstance(v, Mapping):
726+
raise ValueError(f"Expected dict for dataclass, got {type(v)}")
715727
# Drop auxiliary discriminator "kind" if present
716-
src = dict(v) if isinstance(v, Mapping) else v
717-
if isinstance(src, Mapping):
718-
init_kwargs: dict[str, Any] = {}
719-
field_types = {f.name: f.type for f in dataclasses.fields(struct_type)}
720-
for name, f_type in field_types.items():
721-
if name in src:
722-
init_kwargs[name] = load_engine_object(f_type, src[name])
723-
# Construct with defaults for missing fields
724-
return struct_type(**init_kwargs)
728+
dc_init_kwargs: dict[str, Any] = {}
729+
field_types = {f.name: f.type for f in dataclasses.fields(struct_type)}
730+
for name, f_type in field_types.items():
731+
if name in v:
732+
dc_init_kwargs[name] = load_engine_object(f_type, v[name])
733+
return struct_type(**dc_init_kwargs)
725734
elif is_namedtuple_type(struct_type):
726-
# NamedTuple is dumped as list/tuple of items
735+
if not isinstance(v, Mapping):
736+
raise ValueError(f"Expected dict for NamedTuple, got {type(v)}")
737+
# Dict format (from dump/load functions)
727738
annotations = getattr(struct_type, "__annotations__", {})
728739
field_names = list(getattr(struct_type, "_fields", ()))
729-
values: list[Any] = []
740+
nt_init_kwargs: dict[str, Any] = {}
730741
for name in field_names:
731742
f_type = annotations.get(name, Any)
732-
# Assume v is a sequence aligned with fields
733-
if isinstance(v, (list, tuple)):
734-
idx = field_names.index(name)
735-
values.append(load_engine_object(f_type, v[idx]))
736-
elif isinstance(v, Mapping):
737-
values.append(load_engine_object(f_type, v.get(name)))
738-
else:
739-
values.append(v)
740-
return struct_type(*values)
743+
if name in v:
744+
nt_init_kwargs[name] = load_engine_object(f_type, v[name])
745+
return struct_type(**nt_init_kwargs)
741746
return v
742747

743748
# Union with discriminator support via "kind"

python/cocoindex/tests/test_load_convert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_typed_dict_roundtrip_via_dump_load() -> None:
112112
def test_namedtuple_roundtrip_via_dump_load() -> None:
113113
p = LocalPoint(1, 2)
114114
dumped = dump_engine_object(p)
115-
assert dumped == [1, 2]
115+
assert dumped == {"x": 1, "y": 2}
116116
loaded = load_engine_object(LocalPoint, dumped)
117117
assert isinstance(loaded, LocalPoint)
118118
assert loaded == p

0 commit comments

Comments
 (0)