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

Reply via email to