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)