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 b29dd742 [FIX] Stabilize Python annotation metadata (#718)
b29dd742 is described below

commit b29dd742ae1b7ac8fffd2ee7142b05869db04800
Author: Junru Shao <[email protected]>
AuthorDate: Sat Aug 29 11:39:59 2026 -0700

    [FIX] Stabilize Python annotation metadata (#718)
    
    ## Summary
    
    - use `typing_extensions.get_annotations` for class-local annotations
    across Python versions
    - preserve decorated enum subclass identity under static type checking
    - keep `converter` in dataclass-transform metadata without passing an
    unsupported decorator keyword
    - raise the minimum `typing-extensions` version to the release that
    provides `get_annotations`
    
    This branch contains only the content of commit
    `3c6003f0809174477004ccf900108e2b893ae126`, replayed independently onto
    current Apache `main`.
    
    ## Testing
    
    - `uv run pytest -q
    
tests/python/test_dataclass_c_class.py::test_c_class_dataclass_transform_has_converter
    
tests/python/test_dataclass_py_class.py::test_py_class_dataclass_transform_has_converter`
    - changed-file pre-commit hooks
---
 pyproject.toml                                |  2 +-
 python/tvm_ffi/dataclasses/_resolve_fields.py | 15 +++++++--------
 python/tvm_ffi/dataclasses/c_class.py         |  7 ++++++-
 python/tvm_ffi/dataclasses/enum.py            | 12 +++++-------
 python/tvm_ffi/dataclasses/py_class.py        |  7 ++++++-
 python/tvm_ffi/registry.py                    |  4 +++-
 6 files changed, 28 insertions(+), 19 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 5c0bdf8a..68c9480a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,7 @@ classifiers = [
 ]
 keywords = ["machine learning", "inference"]
 requires-python = ">=3.9"
-dependencies = ["typing-extensions>=4.5"]
+dependencies = ["typing-extensions>=4.13"]  # 4.13 added `get_annotations`
 
 [project.urls]
 Homepage = "https://github.com/apache/tvm-ffi";
diff --git a/python/tvm_ffi/dataclasses/_resolve_fields.py 
b/python/tvm_ffi/dataclasses/_resolve_fields.py
index 88bc215e..b15b4ac6 100644
--- a/python/tvm_ffi/dataclasses/_resolve_fields.py
+++ b/python/tvm_ffi/dataclasses/_resolve_fields.py
@@ -37,6 +37,8 @@ from collections.abc import Callable
 from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any
 
+from typing_extensions import get_annotations
+
 from .. import core
 from .field import KW_ONLY
 
@@ -190,14 +192,11 @@ def rollback_registration(cls: type, type_info: TypeInfo) 
-> None:
 
 def own_annotations(cls: type) -> dict[str, Any]:
     """Return annotations declared directly on ``cls`` without MRO merging."""
-    # Python 3.14+ (PEP 749): annotations are lazily evaluated via
-    # __annotate__ and no longer stored directly in __dict__.  getattr()
-    # triggers evaluation and returns per-class annotations correctly.
-    # On Python < 3.14, getattr() follows MRO and returns *parent*
-    # annotations when the child has none — use __dict__ to avoid that.
-    if sys.version_info >= (3, 14):
-        return getattr(cls, "__annotations__", {})
-    return cls.__dict__.get("__annotations__", {})
+    # Reading ``__annotations__`` directly gets this wrong in opposite ways on
+    # either side of Python 3.14: before it, ``getattr`` follows the MRO; from
+    # 3.14 on (PEP 649/749), annotations are computed lazily. This helper means
+    # "declared on this class" consistently and returns a fresh dictionary.
+    return get_annotations(cls)
 
 
 def _field_owner_classes(cls: type) -> list[type]:
diff --git a/python/tvm_ffi/dataclasses/c_class.py 
b/python/tvm_ffi/dataclasses/c_class.py
index d9990a1f..390747df 100644
--- a/python/tvm_ffi/dataclasses/c_class.py
+++ b/python/tvm_ffi/dataclasses/c_class.py
@@ -87,7 +87,6 @@ def _reinstall_field_properties(cls: type, type_info: Any, 
shadowed_names: set[s
     eq_default=False,
     order_default=False,
     field_specifiers=(Field, field),
-    converter=_field_converter,
 )
 def c_class(
     type_key: str,
@@ -216,3 +215,9 @@ def c_class(
         return cls
 
     return decorator
+
+
+# `converter` is runtime metadata rather than part of the type checker's
+# declared `dataclass_transform` signature. Set it on the generated metadata
+# so checkers can still model converted fields without rejecting the decorator.
+c_class.__dataclass_transform__["kwargs"]["converter"] = _field_converter  # 
ty: ignore[unresolved-attribute]
diff --git a/python/tvm_ffi/dataclasses/enum.py 
b/python/tvm_ffi/dataclasses/enum.py
index 1898895c..99987b77 100644
--- a/python/tvm_ffi/dataclasses/enum.py
+++ b/python/tvm_ffi/dataclasses/enum.py
@@ -18,12 +18,11 @@
 
 from __future__ import annotations
 
-import sys
 import typing
 from collections.abc import Callable, Iterator
-from typing import TYPE_CHECKING, Any, ClassVar, overload
+from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
 
-from typing_extensions import Self
+from typing_extensions import Self, get_annotations
 
 from .. import core
 from ..container import Dict, List
@@ -37,8 +36,9 @@ else:
     _EnumMetaBase = type(Object)
 
 if TYPE_CHECKING:
+    _EnumClsT = TypeVar("_EnumClsT", bound=type)
 
-    def _enum_c_class(type_key: str, **kwargs: Any) -> Callable[[type], type]:
+    def _enum_c_class(type_key: str, **kwargs: Any) -> Callable[[_EnumClsT], 
_EnumClsT]:
         return lambda cls: cls
 
 else:
@@ -496,9 +496,7 @@ def _register(entries: Any, indexes: Any, instance: Any) -> 
None:
 
 
 def _own_annotations(cls: type) -> dict[str, Any]:
-    if sys.version_info >= (3, 14):
-        return dict(getattr(cls, "__annotations__", {}) or {})
-    return dict(cls.__dict__.get("__annotations__", {}))
+    return get_annotations(cls)
 
 
 def _is_class_var(annotation: Any) -> bool:
diff --git a/python/tvm_ffi/dataclasses/py_class.py 
b/python/tvm_ffi/dataclasses/py_class.py
index 1109cbff..12ad81e0 100644
--- a/python/tvm_ffi/dataclasses/py_class.py
+++ b/python/tvm_ffi/dataclasses/py_class.py
@@ -448,7 +448,6 @@ def on_fields_resolved(  # noqa: PLR0912, PLR0915
     eq_default=False,
     order_default=False,
     field_specifiers=(Field, field),
-    converter=_field_converter,
 )
 def py_class(  # noqa: PLR0913
     cls_or_type_key: type | str | None = None,
@@ -658,3 +657,9 @@ def py_class(  # noqa: PLR0913
     if isinstance(cls_or_type_key, type):
         return decorator(cls_or_type_key)
     raise TypeError(f"py_class: expected str or type, got 
{type(cls_or_type_key)}")
+
+
+# `converter` is runtime metadata rather than part of the type checker's
+# declared `dataclass_transform` signature. Set it on the generated metadata
+# so checkers can still model converted fields without rejecting the decorator.
+py_class.__dataclass_transform__["kwargs"]["converter"] = _field_converter  # 
ty: ignore[unresolved-attribute]
diff --git a/python/tvm_ffi/registry.py b/python/tvm_ffi/registry.py
index 4fb9c448..3eae09e1 100644
--- a/python/tvm_ffi/registry.py
+++ b/python/tvm_ffi/registry.py
@@ -24,6 +24,8 @@ import warnings
 from collections.abc import Collection, Sequence
 from typing import Any, Callable, Literal, TypeVar, overload
 
+from typing_extensions import get_annotations
+
 from . import core
 from .core import Function, TypeInfo
 
@@ -485,7 +487,7 @@ def _warn_missing_field_annotations(cls: type, type_info: 
TypeInfo, *, stackleve
     reflected_names = {field.name for field in type_info.fields}
     if not reflected_names:
         return
-    own_annotations = cls.__dict__.get("__annotations__", {})
+    own_annotations = get_annotations(cls)
     missing = sorted(reflected_names - set(own_annotations))
     if missing:
         missing_str = ", ".join(missing)

Reply via email to