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",

Reply via email to