This is an automated email from the ASF dual-hosted git repository.
junrushao 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 f41dbfd feat(c_class): warn when reflected fields lack Python type
annotations (#531)
f41dbfd is described below
commit f41dbfd648f5f2be82986aebcd3201f10c1d2974
Author: Junru Shao <[email protected]>
AuthorDate: Fri Apr 10 08:41:17 2026 -0700
feat(c_class): warn when reflected fields lack Python type annotations
(#531)
## Summary
- When a `@c_class`-decorated class has C++-reflected fields without
corresponding Python type annotations, the decorator now emits a
`UserWarning` at class-definition time listing the missing names.
- This gives library authors an early, actionable signal that IDE
support (autocompletion, type checking, hover docs) is degraded for
those fields.
- The warning is non-breaking: filterable via the standard `warnings`
module and does not alter runtime semantics.
## Changes
- **`python/tvm_ffi/registry.py`** — new
`_warn_missing_field_annotations` helper
- **`python/tvm_ffi/dataclasses/c_class.py`** — call the helper after
`register_object()` succeeds
- **`tests/python/test_dataclass_c_class.py`** — 5 new test cases
(all/no/partial annotations, zero fields, derived class)
## Test plan
- [x] `uv run pytest -vvs tests/python/test_dataclass_c_class.py` passes
locally
- [ ] CI green on all platforms
---
python/tvm_ffi/dataclasses/c_class.py | 9 +++-
python/tvm_ffi/registry.py | 25 +++++++++++
tests/python/test_dataclass_c_class.py | 80 ++++++++++++++++++++++++++++++++++
3 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/python/tvm_ffi/dataclasses/c_class.py
b/python/tvm_ffi/dataclasses/c_class.py
index ee6d432..d462ca4 100644
--- a/python/tvm_ffi/dataclasses/c_class.py
+++ b/python/tvm_ffi/dataclasses/c_class.py
@@ -104,10 +104,17 @@ def c_class(
installing structural dunders.
"""
- from ..registry import _install_dataclass_dunders, register_object #
noqa: PLC0415
+ from ..registry import ( # noqa: PLC0415
+ _install_dataclass_dunders,
+ _warn_missing_field_annotations,
+ register_object,
+ )
def decorator(cls: _T) -> _T:
cls = register_object(type_key)(cls)
+ type_info = getattr(cls, "__tvm_ffi_type_info__", None)
+ if type_info is not None:
+ _warn_missing_field_annotations(cls, type_info, stacklevel=2)
_install_dataclass_dunders(
cls, init=init, repr=repr, eq=eq, order=order,
unsafe_hash=unsafe_hash
)
diff --git a/python/tvm_ffi/registry.py b/python/tvm_ffi/registry.py
index 8a84bff..93ad92c 100644
--- a/python/tvm_ffi/registry.py
+++ b/python/tvm_ffi/registry.py
@@ -21,6 +21,7 @@ from __future__ import annotations
import inspect
import json
import sys
+import warnings
from typing import Any, Callable, Literal, Sequence, TypeVar, overload
from . import core
@@ -467,6 +468,30 @@ def _add_class_attrs(type_cls: type, type_info: TypeInfo)
-> type:
return type_cls
+def _warn_missing_field_annotations(cls: type, type_info: TypeInfo, *,
stacklevel: int) -> None:
+ """Emit a warning if any C++ reflected fields lack Python annotations on
*cls*.
+
+ Only checks fields owned by *type_info* (not inherited from parents).
+ Only checks annotations defined directly on *cls* (``cls.__dict__``),
+ so parent annotations do not suppress warnings for child-level fields.
+ """
+ reflected_names = {field.name for field in type_info.fields}
+ if not reflected_names:
+ return
+ own_annotations = cls.__dict__.get("__annotations__", {})
+ missing = sorted(reflected_names - set(own_annotations))
+ if missing:
+ missing_str = ", ".join(missing)
+ warnings.warn(
+ f"@c_class({type_info.type_key!r}): class `{cls.__qualname__}`
does not "
+ f"annotate the following reflected field(s): {missing_str}. "
+ f"Add type annotations (e.g. `field_name: type`) to the class body
"
+ f"for IDE support and documentation.",
+ UserWarning,
+ stacklevel=stacklevel,
+ )
+
+
def _setup_copy_methods(
type_cls: type, has_shallow_copy: bool, *, is_container: bool = False
) -> None:
diff --git a/tests/python/test_dataclass_c_class.py
b/tests/python/test_dataclass_c_class.py
index 9128660..2844c8e 100644
--- a/tests/python/test_dataclass_c_class.py
+++ b/tests/python/test_dataclass_c_class.py
@@ -19,8 +19,11 @@
from __future__ import annotations
import inspect
+import warnings
import pytest
+from tvm_ffi.core import TypeInfo
+from tvm_ffi.registry import _warn_missing_field_annotations
from tvm_ffi.testing import (
_TestCxxClassBase,
_TestCxxClassDerived,
@@ -317,3 +320,80 @@ def test_c_class_unequal_objects_in_set() -> None:
"""Distinct objects are separate entries in a set."""
objs = {_TestCxxClassDerived(i, i, float(i), float(i)) for i in range(5)}
assert len(objs) == 5
+
+
+# ---------------------------------------------------------------------------
+# 12. Field annotation warnings
+# ---------------------------------------------------------------------------
+
+
+def test_c_class_warns_on_missing_field_annotations() -> None:
+ """@c_class warns when reflected fields lack Python annotations."""
+ type_info: TypeInfo = getattr(_TestCxxClassBase, "__tvm_ffi_type_info__")
+ field_names = {f.name for f in type_info.fields}
+ assert field_names # sanity: there are reflected fields
+
+ # A class with no annotations should trigger a warning
+ DummyCls = type("DummyCls", (), {})
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ _warn_missing_field_annotations(DummyCls, type_info, stacklevel=2)
+ assert len(w) == 1
+ assert "does not annotate" in str(w[0].message)
+ for name in field_names:
+ assert name in str(w[0].message)
+
+
+def test_c_class_no_warning_when_all_fields_annotated() -> None:
+ """@c_class does not warn when all reflected fields are annotated."""
+ type_info: TypeInfo = getattr(_TestCxxClassBase, "__tvm_ffi_type_info__")
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ _warn_missing_field_annotations(_TestCxxClassBase, type_info,
stacklevel=2)
+ assert len(w) == 0
+
+
+def test_c_class_warns_only_for_missing_annotations() -> None:
+ """Warning lists only the missing fields, not the annotated ones."""
+ type_info: TypeInfo = getattr(_TestCxxClassBase, "__tvm_ffi_type_info__")
+ field_names = sorted(f.name for f in type_info.fields)
+ assert len(field_names) >= 2 # need at least 2 fields for this test
+
+ # Annotate only the first field, leave the rest unannotated
+ PartialCls = type("PartialCls", (), {"__annotations__": {field_names[0]:
int}})
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ _warn_missing_field_annotations(PartialCls, type_info, stacklevel=2)
+ assert len(w) == 1
+ msg = str(w[0].message)
+ # The annotated field should NOT appear in the warning
+ assert field_names[0] not in msg
+ # The unannotated fields should appear
+ for name in field_names[1:]:
+ assert name in msg
+
+
+def test_c_class_warns_only_own_fields_not_inherited() -> None:
+ """Warning only checks own fields, not parent fields."""
+ # _TestCxxClassDerived's type_info.fields contains only its own fields
+ # (v_f64, v_f32), not parent fields (v_i64, v_i32).
+ derived_type_info: TypeInfo = getattr(_TestCxxClassDerived,
"__tvm_ffi_type_info__")
+ own_field_names = {f.name for f in derived_type_info.fields}
+ assert own_field_names # sanity
+
+ # A class with no annotations: warning should mention only own fields
+ DummyCls = type("DummyCls", (), {})
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ _warn_missing_field_annotations(DummyCls, derived_type_info,
stacklevel=2)
+ assert len(w) == 1
+ msg = str(w[0].message)
+ for name in own_field_names:
+ assert name in msg
+ # Parent fields should NOT appear in the warning
+ parent_type_info = derived_type_info.parent_type_info
+ if parent_type_info is not None:
+ parent_field_names = {f.name for f in parent_type_info.fields}
+ for name in parent_field_names:
+ assert name not in msg