This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 4a3f097a [FIX] Preserve namedtuple fields when unpacking dataclasses
(#867)
4a3f097a is described below
commit 4a3f097a2bb25fe89da1900b671c056de8fc909b
Author: LingYi <[email protected]>
AuthorDate: Sun Oct 25 02:46:07 2026 +0800
[FIX] Preserve namedtuple fields when unpacking dataclasses (#867)
`unpack_dataclass_to_tuple` rebuilds tuples from a single iterable.
Named tuples take one positional argument per field: multi-field values
raise `TypeError`, while single-field values silently receive the
generator instead of their original value.
Handle named tuples separately, preserving their type and recursively
unpacking their fields without copying leaf objects. The regression
tests cover empty, single-field and multi-field values, both namedtuple
APIs, and nested values through the cached dataclass unpacker. Follow-up
to #563.
Validation on Windows / Python 3.12:
- All five new cases fail with the unchanged main implementation; 17
focused tests pass after the fix (`test_unpack_dataclass.py` and
`test_kwargs_wrapper.py`).
- The exact worktree utility sources were tested with the
`apache-tvm-ffi==0.1.15.post0` binary wheel as the package runtime. I
did not rebuild the native extension or run the full C++/Python/Rust
suites.
- Ruff lint/format, focused ty checks, ASF headers, file types and `git
diff --check` pass.
---
python/tvm_ffi/utils/unpack_dataclass.py | 5 ++++-
tests/python/utils/test_unpack_dataclass.py | 33 ++++++++++++++++++++++++++++-
2 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/python/tvm_ffi/utils/unpack_dataclass.py
b/python/tvm_ffi/utils/unpack_dataclass.py
index b85ffc6d..60405863 100644
--- a/python/tvm_ffi/utils/unpack_dataclass.py
+++ b/python/tvm_ffi/utils/unpack_dataclass.py
@@ -205,7 +205,7 @@ def unpack_dataclass_to_tuple(x: Any) -> Any:
"""Fast recursively unpack a dataclass value to tuple representation.
- Dataclass instances are unpacked to tuples of their field values.
- - Lists and tuples are recursed element-wise, returning a tuple.
+ - Lists and tuples are recursed element-wise, preserving their type.
- Dicts are recursed on values, returning a new dict.
- All other values are returned as-is (leaf passthrough).
@@ -250,6 +250,9 @@ def unpack_dataclass_to_tuple(x: Any) -> Any:
cache[cls] = fn
return fn(x)
if isinstance(x, (list, tuple)):
+ if isinstance(x, tuple) and hasattr(x, "_fields"):
+ # Named tuples take one positional argument per field.
+ return type(x)(*(unpack_dataclass_to_tuple(e) for e in x))
return type(x)(unpack_dataclass_to_tuple(e) for e in x)
if isinstance(x, dict):
return {k: unpack_dataclass_to_tuple(v) for k, v in x.items()}
diff --git a/tests/python/utils/test_unpack_dataclass.py
b/tests/python/utils/test_unpack_dataclass.py
index 3a557eab..7cb873ab 100644
--- a/tests/python/utils/test_unpack_dataclass.py
+++ b/tests/python/utils/test_unpack_dataclass.py
@@ -18,7 +18,8 @@ from __future__ import annotations
import dataclasses
import sys
-from typing import Any
+from collections import namedtuple
+from typing import Any, NamedTuple
import pytest
from tvm_ffi.utils.unpack_dataclass import (
@@ -37,6 +38,11 @@ class _ExtractConfig:
y: int
+class _UnpackNamedTuple(NamedTuple):
+ cfg: _ExtractConfig
+ leaf: Any
+
+
@dataclasses.dataclass
class _ExtractNested:
value: int
@@ -223,6 +229,31 @@ def test_validate_dataclass_to_tuple_schema() -> None:
_validate_dataclass_to_tuple_schema({"cfg": {"x": None, "y!": None}})
[email protected]("values", [(), (1,), (1, 2)])
+def test_unpack_namedtuple_arity(values: tuple[int, ...]) -> None:
+ record_type = namedtuple("Record", [f"field_{i}" for i in
range(len(values))])
+ record = record_type(*values)
+
+ result = unpack_dataclass_to_tuple(record)
+ assert type(result) is record_type
+ assert result == record
+
+
[email protected]("record_type", [namedtuple("Record", "cfg leaf"),
_UnpackNamedTuple])
+def test_unpack_nested_namedtuple(record_type: Any) -> None:
+ leaf = object()
+ record = record_type(_ExtractConfig(1, 2), leaf)
+ value = _ExtractWithAny(data={"records": [record]}, scale=3)
+
+ # Exercise both the initial and cached dataclass unpacker.
+ for _ in range(2):
+ result = unpack_dataclass_to_tuple(value)
+ unpacked = result[0]["records"][0]
+ assert result == ({"records": [record_type((1, 2), leaf)]}, 3)
+ assert type(unpacked) is record_type
+ assert unpacked.leaf is leaf
+
+
@pytest.mark.xfail(
sys.version_info < (3, 10),
reason="list[X]/dict[X,Y]/int|None not evaluable by get_type_hints on
Python < 3.10",