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 721d8781 fix(python): remove broken
__instancecheck__/__subclasscheck__ from _ObjectSlotsMeta (#498)
721d8781 is described below
commit 721d87816152e4a1cdc5c7906b116d46007699f8
Author: Junru Shao <[email protected]>
AuthorDate: Fri Mar 6 16:29:13 2026 -0800
fix(python): remove broken __instancecheck__/__subclasscheck__ from
_ObjectSlotsMeta (#498)
## Summary
- Remove `__instancecheck__` and `__subclasscheck__` from
`_ObjectSlotsMeta` in `python/tvm_ffi/cython/object.pxi`. These methods
unconditionally returned `True` for any `CObject` instance/subclass
regardless of which class was being checked, causing incorrect behavior
(e.g., `isinstance(Map(...), Array)` returning `True`).
- The methods are unnecessary because all objects returned from C++ are
always constructed as proper `Object` subclasses, so standard Python
MRO-based `isinstance`/`issubclass` checks work correctly.
- Update `test_dataclass_init.py` to assert correct behavior (parent is
not an instance of child).
- Add 22 new regression tests in `TestIsinstanceIssubclass` covering
containers, inheritance, and cross-hierarchy `isinstance`/`issubclass`
checks.
## Test plan
- [x] All 1517 existing Python tests pass
- [x] 22 new tests verify correct isinstance/issubclass behavior for
containers (Array, List, Map, Dict), inheritance hierarchies, and
cross-hierarchy checks
- [x] Pre-commit hooks pass (ruff, cython-lint, ty, etc.)
---
python/tvm_ffi/cython/object.pxi | 13 ----
tests/python/test_dataclass_init.py | 11 +--
tests/python/test_object.py | 139 ++++++++++++++++++++++++++++++++++++
3 files changed, 142 insertions(+), 21 deletions(-)
diff --git a/python/tvm_ffi/cython/object.pxi b/python/tvm_ffi/cython/object.pxi
index 6eb22539..b02c95e0 100644
--- a/python/tvm_ffi/cython/object.pxi
+++ b/python/tvm_ffi/cython/object.pxi
@@ -175,19 +175,6 @@ class _ObjectSlotsMeta(ABCMeta):
def __init__(cls, name: str, bases: tuple[type, ...], ns: dict[str, Any],
**kwargs: Any):
super().__init__(name, bases, ns, **kwargs)
- def __instancecheck__(cls, instance: Any) -> bool:
- if isinstance(instance, CObject):
- return True
- return super().__instancecheck__(instance)
-
- def __subclasscheck__(cls, subclass: type) -> bool:
- try:
- if issubclass(subclass, CObject):
- return True
- except TypeError:
- pass
- return super().__subclasscheck__(subclass)
-
class Object(CObject, metaclass=_ObjectSlotsMeta):
"""Base class of all TVM FFI objects.
diff --git a/tests/python/test_dataclass_init.py
b/tests/python/test_dataclass_init.py
index 918838cf..bc349746 100644
--- a/tests/python/test_dataclass_init.py
+++ b/tests/python/test_dataclass_init.py
@@ -740,15 +740,10 @@ class TestAutoInitTypeChecks:
assert isinstance(obj, _TestCxxAutoInitParent)
assert isinstance(obj, core.Object)
- def test_parent_isinstance_child_due_to_metaclass(self) -> None:
- """Due to _ObjectSlotsMeta, any CObject passes isinstance for any FFI
class.
-
- This is a pre-existing design choice in the TVM FFI type system, not a
bug
- introduced by the auto-init feature.
- """
+ def test_parent_not_isinstance_child(self) -> None:
+ """A parent object should not pass isinstance check for a child
class."""
obj = _TestCxxAutoInitParent(1)
- # _ObjectSlotsMeta.__instancecheck__ returns True for any CObject
- assert isinstance(obj, _TestCxxAutoInitChild)
+ assert not isinstance(obj, _TestCxxAutoInitChild)
class TestAutoInitInstanceIsolation:
diff --git a/tests/python/test_object.py b/tests/python/test_object.py
index 74ced83b..edb6fec2 100644
--- a/tests/python/test_object.py
+++ b/tests/python/test_object.py
@@ -326,3 +326,142 @@ def test_get_registered_type_keys() -> None:
assert ty.startswith("ffi.") or ty.startswith("testing."), (
f"Expected type key `{ty}` to start with `ffi.` or `testing.`"
)
+
+
+# ---------------------------------------------------------------------------
+# isinstance / issubclass correctness for the Object type hierarchy
+# ---------------------------------------------------------------------------
+
+
+class TestIsinstanceIssubclass:
+ """Verify that isinstance/issubclass respect the actual type hierarchy.
+
+ Regression tests for a bug where _ObjectSlotsMeta.__instancecheck__
+ and __subclasscheck__ returned True for *any* CObject against *any*
+ Object subclass, making e.g. isinstance(Map(...), Array) == True.
+ """
+
+ # -- containers: sibling types must not match each other ----------------
+
+ def test_map_not_isinstance_array(self) -> None:
+ """Map instance should not pass isinstance check for Array."""
+ m = tvm_ffi.Map({"a": 1})
+ assert not isinstance(m, tvm_ffi.Array)
+
+ def test_array_not_isinstance_map(self) -> None:
+ """Array instance should not pass isinstance check for Map."""
+ a = tvm_ffi.Array([1, 2])
+ assert not isinstance(a, tvm_ffi.Map)
+
+ def test_list_not_isinstance_dict(self) -> None:
+ """List instance should not pass isinstance check for Dict."""
+ lst = tvm_ffi.List([1, 2])
+ assert not isinstance(lst, tvm_ffi.Dict)
+
+ def test_dict_not_isinstance_list(self) -> None:
+ """Dict instance should not pass isinstance check for List."""
+ d = tvm_ffi.Dict({"k": 1})
+ assert not isinstance(d, tvm_ffi.List)
+
+ def test_array_not_isinstance_list(self) -> None:
+ """Array instance should not pass isinstance check for List."""
+ a = tvm_ffi.Array([1])
+ assert not isinstance(a, tvm_ffi.List)
+
+ def test_map_not_isinstance_dict(self) -> None:
+ """Map instance should not pass isinstance check for Dict."""
+ m = tvm_ffi.Map({"a": 1})
+ assert not isinstance(m, tvm_ffi.Dict)
+
+ # -- containers: positive isinstance ------------------------------------
+
+ def test_array_isinstance_object(self) -> None:
+ """Array instance should pass isinstance check for Object."""
+ a = tvm_ffi.Array([1])
+ assert isinstance(a, tvm_ffi.Object)
+
+ def test_map_isinstance_object(self) -> None:
+ """Map instance should pass isinstance check for Object."""
+ m = tvm_ffi.Map({"a": 1})
+ assert isinstance(m, tvm_ffi.Object)
+
+ def test_list_isinstance_object(self) -> None:
+ """List instance should pass isinstance check for Object."""
+ lst = tvm_ffi.List([1])
+ assert isinstance(lst, tvm_ffi.Object)
+
+ def test_dict_isinstance_object(self) -> None:
+ """Dict instance should pass isinstance check for Object."""
+ d = tvm_ffi.Dict({"k": 1})
+ assert isinstance(d, tvm_ffi.Object)
+
+ # -- registered user types: parent / child ------------------------------
+
+ def test_derived_isinstance_base(self) -> None:
+ """Derived object should pass isinstance check for its base class."""
+ obj = tvm_ffi.testing.TestObjectDerived(
+ v_map={"a": 1},
+ v_array=[1],
+ )
+ assert isinstance(obj, tvm_ffi.testing.TestObjectBase)
+ assert isinstance(obj, tvm_ffi.Object)
+
+ def test_base_not_isinstance_derived(self) -> None:
+ """Base object should not pass isinstance check for a derived class."""
+ obj = tvm_ffi.testing.TestObjectBase()
+ assert not isinstance(obj, tvm_ffi.testing.TestObjectDerived)
+
+ def test_derived_isinstance_own_class(self) -> None:
+ """Derived object should pass isinstance check for its own class."""
+ obj = tvm_ffi.testing.TestObjectDerived(
+ v_map={"a": 1},
+ v_array=[1],
+ )
+ assert isinstance(obj, tvm_ffi.testing.TestObjectDerived)
+
+ # -- cross-hierarchy: user object vs container --------------------------
+
+ def test_user_object_not_isinstance_array(self) -> None:
+ """User-defined object should not pass isinstance check for Array."""
+ obj = tvm_ffi.testing.TestObjectBase()
+ assert not isinstance(obj, tvm_ffi.Array)
+
+ def test_array_not_isinstance_user_object(self) -> None:
+ """Array should not pass isinstance check for a user-defined type."""
+ a = tvm_ffi.Array([1])
+ assert not isinstance(a, tvm_ffi.testing.TestObjectBase)
+
+ # -- issubclass mirrors isinstance --------------------------------------
+
+ def test_issubclass_derived_base(self) -> None:
+ """Derived class should be a subclass of its base."""
+ assert issubclass(tvm_ffi.testing.TestObjectDerived,
tvm_ffi.testing.TestObjectBase)
+
+ def test_issubclass_base_not_derived(self) -> None:
+ """Base class should not be a subclass of its derived class."""
+ assert not issubclass(tvm_ffi.testing.TestObjectBase,
tvm_ffi.testing.TestObjectDerived)
+
+ def test_issubclass_array_not_map(self) -> None:
+ """Array should not be a subclass of Map."""
+ assert not issubclass(tvm_ffi.Array, tvm_ffi.Map)
+
+ def test_issubclass_map_not_array(self) -> None:
+ """Map should not be a subclass of Array."""
+ assert not issubclass(tvm_ffi.Map, tvm_ffi.Array)
+
+ def test_issubclass_all_containers_are_object(self) -> None:
+ """All container types should be subclasses of Object."""
+ for cls in (tvm_ffi.Array, tvm_ffi.List, tvm_ffi.Map, tvm_ffi.Dict):
+ assert issubclass(cls, tvm_ffi.Object)
+
+ def test_issubclass_user_type_is_object(self) -> None:
+ """User-defined types should be subclasses of Object."""
+ assert issubclass(tvm_ffi.testing.TestObjectBase, tvm_ffi.Object)
+ assert issubclass(tvm_ffi.testing.TestObjectDerived, tvm_ffi.Object)
+
+ def test_issubclass_sibling_containers(self) -> None:
+ """Sibling container types should not be subclasses of each other."""
+ assert not issubclass(tvm_ffi.List, tvm_ffi.Array)
+ assert not issubclass(tvm_ffi.Dict, tvm_ffi.Map)
+ assert not issubclass(tvm_ffi.Array, tvm_ffi.List)
+ assert not issubclass(tvm_ffi.Map, tvm_ffi.Dict)