feat(dataclass): add asdict, astuple, and match_args support#556
Merged
junrushao merged 2 commits intoapache:mainfrom Apr 18, 2026
Merged
Conversation
Architecture: - common.py gains asdict/astuple plus _asdict_inner/_astuple_inner recursion driven by an _ATOMIC_TYPES frozenset mirroring stdlib dataclasses. FFI sequence containers (Array, List) recurse to Python list; FFI mapping containers (Map, Dict) recurse to Python dict so the result is plain Python data (JSON-ready). Non-dataclass leaves fall back to copy.deepcopy. - _dunder.py gains _set_match_args(cls, type_info), a helper that walks the TypeInfo parent chain parent-first and assembles the positional __init__ field names (init=True and not kw_only), then sets cls.__match_args__ if the class does not already define one. _install_dataclass_dunders accepts a new match_args parameter and invokes the helper alongside the other dunders. Public Interfaces: - tvm_ffi.dataclasses now exports asdict and astuple. - @py_class and @c_class accept match_args: bool = True, mirroring stdlib dataclasses. Default preserves prior behavior only when the class did not override __match_args__; newly decorated classes now gain __match_args__ automatically. UI/UX: none. Behavioral Changes: - Python 3.10+ match statements against @py_class / @c_class instances now bind positional captures by field order, matching stdlib dataclass expectations. - asdict is a TypeError when passed a type or a non-FFI-dataclass value, mirroring stdlib. - _is_ffi_dataclass_instance distinguishes FFI dataclass instances from FFI container instances (Array/List/Map/Dict), so asdict and astuple do not mistake containers for dataclasses. Docs: - API docs for the new decorator parameter and the two helpers live in the inline docstrings; no Sphinx RST updates required for this patch since public names are discovered via __all__. Tests: - Executed: uv run pytest tests/python/ - Result: 2184 passed, 38 skipped, 3 xfailed. - Added TestAsdict (15), TestAstuple (10), TestMatchArgs (11) in tests/python/test_dataclass_common.py covering inheritance, init=False, kw_only (field, decorator, KW_ONLY sentinel), match_args=False opt-out, user-defined __match_args__ override, FFI Array/List/Map/Dict recursion, dict_factory/tuple_factory, and error paths. Untested Edge Cases: - match statement semantics on Python 3.8/3.9 are not exercised because match syntax is 3.10+; the __match_args__ tuple is still populated on older interpreters and is tested there. - Deeply nested cyclic FFI graphs are not exercised by asdict / astuple; recursion relies on the caller's graph being acyclic (same assumption as stdlib dataclasses.asdict).
0dad751 to
29479ed
Compare
16 tasks
16 tasks
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces asdict and astuple utility functions for FFI dataclasses, mirroring the standard library's dataclasses functionality. It also adds support for __match_args__ in both @c_class and @py_class decorators to enable structural pattern matching. The review feedback correctly identifies a bug in the recursive handling of collections.defaultdict within both asdict and astuple, where the code incorrectly checks the class instead of the instance for the default_factory attribute.
Adds direct tests for the `_asdict_inner` and `_astuple_inner` defaultdict branches, locking in the stdlib-parity contract. Tests/Evidence: - The class-level check `hasattr(obj_type, 'default_factory')` mirrors CPython `dataclasses._asdict_inner` (lib/dataclasses.py). `collections.defaultdict` exposes `default_factory` as a class-level member_descriptor, so the check returns True. - Direct-branch coverage is necessary because storing a `defaultdict` in an FFI `Any` field converts it to an FFI `Map` on readback — the public `asdict`/`astuple` path never reaches this branch for FFI-backed graphs. Refs: review thread on PR apache#556 (gemini-code-assist).
yzh119
approved these changes
Apr 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds three stdlib-parity features to
tvm_ffi.dataclasses:asdict(obj, *, dict_factory=dict)— recursively converts a@py_class/@c_classinstance to a plain Pythondict. FFI containers (Array,List) recurse intolist; (Map,Dict) recurse intodict, yielding JSON-ready output.astuple(obj, *, tuple_factory=tuple)— the tuple analogue ofasdict, with the same recursion rules.match_args: bool = Trueparameter on@py_classand@c_class— setscls.__match_args__to the tuple of positional__init__field names (init=True and not kw_only), enabling Python 3.10+matchstatements. Skipped when the class body already defines__match_args__.Semantics follow CPython's
dataclassesmodule:asdict/astupleraiseTypeErrorfor types and non-dataclass values; kw-only fields (viafield(kw_only=True), decorator-levelkw_only=True, or theKW_ONLYsentinel) are excluded from__match_args__.Design notes
_is_ffi_dataclass_instancefilters FFI container instances (Array,List,Map,Dict) from FFI dataclass instances — both share__tvm_ffi_type_info__, so the container isinstance-check runs first during recursion._set_match_argswalks theTypeInfo.parent_type_infochain in parent-first order, matching the order of the auto-generated__init__signature._ATOMIC_TYPESfrozenset (mirroring stdlibdataclasses._ATOMIC_TYPES) for the fast path on immutable leaves.Test plan
uv run pytest tests/python/— 2184 passed, 38 skipped, 3 xfailedpre-commit run --files <touched files>— all hooks pass (ruff, ty, format)tests/python/test_dataclass_common.py:TestAsdict(15 tests): basic, nested, inheritance, FFI Array/List/Map/Dict recursion,dict_factory, result independence, error pathsTestAstuple(10 tests): basic, nested, recursion,tuple_factory, error pathsTestMatchArgs(11 tests): defaults,init=False, kw_only (field / decorator /KW_ONLYsentinel), inheritance order,match_args=Falseopt-out, user-defined__match_args__override, c_class basic and inheritance