blob: 423965e205f96ea297fb25705bc424c131b4387b [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for Python-defined TVM-FFI types: ``@py_class`` decorator and low-level Field API."""
# ruff: noqa: D102, PLR0124, PLW1641, UP006, UP045
from __future__ import annotations
import copy
import gc
import inspect
import itertools
import math
from typing import Any, ClassVar, Dict, List, Optional
import pytest
import tvm_ffi
from tvm_ffi import core
from tvm_ffi._dunder import _install_dataclass_dunders
from tvm_ffi._ffi_api import DeepCopy, RecursiveEq, RecursiveHash, ReprPrint
from tvm_ffi.core import MISSING, Object, TypeInfo, TypeSchema, _to_py_class_value
from tvm_ffi.dataclasses import KW_ONLY, Field, IntEnum, StrEnum, entry, field, fields, py_class
from tvm_ffi.registry import _add_class_attrs
from tvm_ffi.testing import TestObjectBase as _TestObjectBase
from tvm_ffi.testing.testing import requires_py310
# ---------------------------------------------------------------------------
# Unique type key generator (avoids collisions across tests)
# ---------------------------------------------------------------------------
_counter = itertools.count()
def _unique_key(base: str) -> str:
return f"testing.py_class_dec.{base}_{next(_counter)}"
def _get_type_info(cls: type) -> TypeInfo:
ret = cls.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
assert isinstance(ret, TypeInfo), f"Expected TypeInfo, got {type(ret)}"
return ret
# ---------------------------------------------------------------------------
# Low-level helpers for _make_type-based tests
# ---------------------------------------------------------------------------
_counter_ff = itertools.count()
def _unique_key_ff(base: str) -> str:
"""Return a globally unique type key for low-level field tests."""
return f"testing.py_class.{base}_{next(_counter_ff)}"
def _make_type(
name: str,
fields: List[Field],
*,
parent: type = core.Object,
eq: bool = False,
unsafe_hash: bool = False,
repr: bool = True,
) -> type:
"""Create, register, and fully set up a Python-defined TVM-FFI type.
Returns the ready-to-use Python class.
"""
type_key = _unique_key_ff(name)
parent_info = core._type_cls_to_type_info(parent)
assert parent_info is not None
cls = type(name, (parent,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
info._register_fields(fields)
setattr(cls, "__tvm_ffi_type_info__", info)
_add_class_attrs(cls, info)
_install_dataclass_dunders(
cls,
init=True,
repr=repr,
eq=eq,
order=False,
unsafe_hash=unsafe_hash,
)
return cls
# ###########################################################################
# 1. Basic registration
# ###########################################################################
class TestBasicRegistration:
"""@py_class decorator with different calling conventions."""
def test_bare_decorator(self) -> None:
@py_class(_unique_key("Bare"))
class Bare(Object):
x: int
info = _get_type_info(Bare)
assert info is not None
assert len(info.fields) == 1
assert info.fields[0].name == "x"
def test_decorator_with_options(self) -> None:
@py_class(_unique_key("Opts"), eq=True)
class Opts(Object):
x: int
assert hasattr(Opts, "__eq__")
assert Opts(x=1) == Opts(x=1)
def test_auto_type_key(self) -> None:
@py_class(_unique_key("AutoKey"))
class AutoKey(Object):
x: int
info = _get_type_info(AutoKey)
assert info.type_key.startswith("testing.")
def test_explicit_type_key(self) -> None:
key = _unique_key("ExplicitKey")
@py_class(key)
class ExplicitKey(Object):
x: int
assert _get_type_info(ExplicitKey).type_key == key
def test_empty_class(self) -> None:
@py_class(_unique_key("Empty"))
class Empty(Object):
pass
obj = Empty()
assert obj is not None
def test_isinstance_check(self) -> None:
@py_class(_unique_key("InstCheck"))
class InstCheck(Object):
x: int
obj = InstCheck(x=42)
assert isinstance(obj, InstCheck)
assert isinstance(obj, Object)
# ###########################################################################
# 2. Field parsing
# ###########################################################################
class TestFieldParsing:
"""Annotation-to-Field conversion."""
def test_int_field(self) -> None:
@py_class(_unique_key("IntFld"))
class IntFld(Object):
x: int
obj = IntFld(x=42)
assert obj.x == 42
def test_float_field(self) -> None:
@py_class(_unique_key("FltFld"))
class FltFld(Object):
x: float
obj = FltFld(x=3.14)
assert abs(obj.x - 3.14) < 1e-10
def test_str_field(self) -> None:
@py_class(_unique_key("StrFld"))
class StrFld(Object):
x: str
obj = StrFld(x="hello")
assert obj.x == "hello"
def test_bool_field(self) -> None:
@py_class(_unique_key("BoolFld"))
class BoolFld(Object):
x: bool
obj = BoolFld(x=True)
assert obj.x is True
def test_payload_enum_fields_end_to_end(self) -> None:
"""IntEnum/StrEnum fields on @py_class accept raw payloads and expose enum objects."""
class Priority(IntEnum, type_key=_unique_key("Priority")):
low = entry(value=10)
high = entry(value=20)
class Opcode(StrEnum, type_key=_unique_key("Opcode")):
add = entry(value="+")
mul = entry(value="*")
@py_class(_unique_key("EnumFields"))
class Instruction(Object):
priority: Priority
opcode: Opcode
from_payload = Instruction(priority=20, opcode="*") # ty: ignore[invalid-argument-type]
assert isinstance(from_payload.priority, Priority)
assert isinstance(from_payload.opcode, Opcode)
assert from_payload.priority.same_as(Priority.high)
assert from_payload.opcode.same_as(Opcode.mul)
assert from_payload.priority.value == 20
assert from_payload.opcode.value == "*"
from_enum = Instruction(priority=Priority.low, opcode=Opcode.add)
assert from_enum.priority.same_as(Priority.low)
assert from_enum.opcode.same_as(Opcode.add)
from_enum.priority = 20 # ty: ignore[invalid-assignment]
from_enum.opcode = "*" # ty: ignore[invalid-assignment]
assert from_enum.priority.same_as(Priority.high)
assert from_enum.opcode.same_as(Opcode.mul)
with pytest.raises((TypeError, RuntimeError), match="expected"):
from_enum.priority = 99 # ty: ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError), match="expected"):
from_enum.opcode = "/" # ty: ignore[invalid-assignment]
@requires_py310
def test_optional_field(self) -> None:
@py_class(_unique_key("OptFld"))
class OptFld(Object):
x: Optional[int]
obj = OptFld(x=42)
assert obj.x == 42
obj2 = OptFld(x=None)
assert obj2.x is None
def test_multiple_fields(self) -> None:
@py_class(_unique_key("Multi"))
class Multi(Object):
a: int
b: float
c: str
obj = Multi(a=1, b=2.0, c="three")
assert obj.a == 1
assert obj.b == 2.0
assert obj.c == "three"
# ###########################################################################
# 3. Defaults
# ###########################################################################
class TestDefaults:
"""Default values and default_factory."""
def test_bare_default(self) -> None:
@py_class(_unique_key("BareDef"))
class BareDef(Object):
x: int
y: int = 10
obj = BareDef(x=1)
assert obj.y == 10
def test_field_default(self) -> None:
@py_class(_unique_key("FldDef"))
class FldDef(Object):
x: int = field(default=42)
obj = FldDef()
assert obj.x == 42
def test_field_default_factory(self) -> None:
call_count = 0
def make_default() -> int:
nonlocal call_count
call_count += 1
return 99
@py_class(_unique_key("FldFact"))
class FldFact(Object):
x: int = field(default_factory=make_default)
obj1 = FldFact()
assert obj1.x == 99
obj2 = FldFact()
assert obj2.x == 99
assert call_count == 2
def test_default_and_factory_mutually_exclusive(self) -> None:
with pytest.raises(ValueError, match="cannot specify both"):
field(default=1, default_factory=int)
def test_non_callable_factory_rejected(self) -> None:
with pytest.raises(TypeError, match="default_factory must be a callable"):
field(default_factory=42) # ty: ignore[invalid-argument-type]
def test_required_before_optional(self) -> None:
@py_class(_unique_key("ReqOpt"))
class ReqOpt(Object):
a: int
b: int = 10
obj = ReqOpt(1)
assert obj.a == 1
assert obj.b == 10
# ###########################################################################
# 4. KW_ONLY
# ###########################################################################
class TestKwOnly:
"""Keyword-only field support."""
def test_kw_only_sentinel(self) -> None:
@py_class(_unique_key("KWSent"))
class KWSent(Object):
a: int
_: KW_ONLY
b: int = 10
obj = KWSent(1, b=20) # ty: ignore[missing-argument]
assert obj.a == 1
assert obj.b == 20
with pytest.raises(TypeError):
KWSent(1, 2) # ty: ignore[invalid-argument-type]
def test_decorator_level_kw_only(self) -> None:
@py_class(_unique_key("DecKW"), kw_only=True)
class DecKW(Object):
a: int
b: int = 10
obj = DecKW(a=1)
assert obj.a == 1
assert obj.b == 10
with pytest.raises(TypeError):
DecKW(1) # ty: ignore[missing-argument,too-many-positional-arguments]
def test_field_level_kw_only_override(self) -> None:
@py_class(_unique_key("FldKW"))
class FldKW(Object):
a: int
b: int = field(default=10, kw_only=True)
obj = FldKW(1)
assert obj.a == 1
assert obj.b == 10
with pytest.raises(TypeError):
FldKW(1, 2) # b is keyword-only
# ###########################################################################
# 5. ClassVar
# ###########################################################################
class TestClassVar:
"""ClassVar annotations are skipped."""
def test_classvar_skipped(self) -> None:
@py_class(_unique_key("CV"))
class CV(Object):
x: int
count: ClassVar[int] = 0
info = _get_type_info(CV)
field_names = [f.name for f in info.fields]
assert "x" in field_names
assert "count" not in field_names
def test_classvar_preserved_on_class(self) -> None:
@py_class(_unique_key("CVPres"))
class CVPres(Object):
x: int
tag: ClassVar[str] = "hello"
assert CVPres.tag == "hello"
# ###########################################################################
# 6. Init generation
# ###########################################################################
class TestInit:
"""Auto-generated __init__."""
def test_positional_args(self) -> None:
@py_class(_unique_key("Pos"))
class Pos(Object):
a: int
b: str
obj = Pos(1, "hello")
assert obj.a == 1
assert obj.b == "hello"
def test_keyword_args(self) -> None:
@py_class(_unique_key("Kw"))
class Kw(Object):
a: int
b: str
obj = Kw(a=1, b="hello")
assert obj.a == 1
assert obj.b == "hello"
def test_init_false_field(self) -> None:
@py_class(_unique_key("NoInit"))
class NoInit(Object):
a: int
b: int = field(default=99, init=False)
obj = NoInit(a=1)
assert obj.a == 1
assert obj.b == 99
def test_user_defined_init_preserved(self) -> None:
@py_class(_unique_key("UserInit"), init=False)
class UserInit(Object):
a: int
def __init__(self, val: int) -> None:
self.a = val
obj = UserInit(42)
assert obj.a == 42
def test_required_after_optional_reordered(self) -> None:
"""Required positional fields are reordered before optional ones in __init__."""
@py_class(_unique_key("ReorderOwn"))
class ReorderOwn(Object):
x: int = 0
y: int # ty: ignore[dataclass-field-order]
sig = inspect.signature(ReorderOwn.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert param_names[0] == "y" # required comes first
assert param_names[1] == "x" # optional comes second
obj = ReorderOwn(y=1) # ty: ignore[missing-argument]
assert obj.x == 0
assert obj.y == 1
def test_required_after_optional_in_parent(self) -> None:
"""Child required fields are reordered before parent optional fields."""
@py_class(_unique_key("OptParent"))
class OptParent(Object):
x: int
y: int = 0
@py_class(_unique_key("ReqChild"))
class ReqChild(OptParent):
z: int
sig = inspect.signature(ReqChild.__init__)
param_names = [n for n in sig.parameters if n != "self"]
# required (x, z) before optional (y)
assert param_names == ["x", "z", "y"]
obj = ReqChild(x=1, z=3)
assert obj.x == 1
assert obj.y == 0
assert obj.z == 3
def test_kw_only_exempt_from_reorder(self) -> None:
"""kw_only fields are not reordered with positional fields."""
@py_class(_unique_key("KwReorder"))
class KwReorder(Object):
x: int = 0
_: KW_ONLY # ty: ignore[dataclass-field-order]
y: int # ty: ignore[dataclass-field-order]
sig = inspect.signature(KwReorder.__init__)
params = sig.parameters
assert params["x"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert params["y"].kind == inspect.Parameter.KEYWORD_ONLY
obj = KwReorder(y=1) # ty: ignore[missing-argument]
assert obj.x == 0
assert obj.y == 1
def test_mixed_positional_and_kw_only_with_defaults(self) -> None:
"""Mixed positional/kw_only fields with defaults produce correct signature."""
@py_class(_unique_key("MixedSig"))
class MixedSig(Object):
a: int = 0
b: int # ty: ignore[dataclass-field-order]
_: KW_ONLY # ty: ignore[dataclass-field-order]
c: int = 10
d: int # ty: ignore[dataclass-field-order]
sig = inspect.signature(MixedSig.__init__)
param_names = [n for n in sig.parameters if n != "self"]
# positional: b (required) before a (optional); kw_only: d (required) before c (optional)
assert param_names == ["b", "a", "d", "c"]
obj = MixedSig(b=2, d=4) # ty: ignore[missing-argument]
assert obj.a == 0
assert obj.b == 2
assert obj.c == 10
assert obj.d == 4
def test_init_false_excluded_from_signature(self) -> None:
"""init=False fields do not appear in __init__ signature."""
@py_class(_unique_key("InitFalseSig"))
class InitFalseSig(Object):
a: int
b: int = field(default=99, init=False)
c: str
sig = inspect.signature(InitFalseSig.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert "b" not in param_names
assert "a" in param_names
assert "c" in param_names
# ###########################################################################
# 7. __post_init__
# ###########################################################################
class TestPostInit:
"""__post_init__ support."""
def test_post_init_called(self) -> None:
post_init_called = False
@py_class(_unique_key("PostInit"))
class PostInit(Object):
x: int
def __post_init__(self) -> None:
nonlocal post_init_called
post_init_called = True
PostInit(x=1)
assert post_init_called
def test_post_init_sees_field_values(self) -> None:
@py_class(_unique_key("PostInitVal"))
class PostInitVal(Object):
x: int
y: int = 10
def __post_init__(self) -> None:
# Fields should be set before __post_init__ is called
assert self.x is not None
assert self.y == 10
PostInitVal(x=5)
# ###########################################################################
# 8. Repr
# ###########################################################################
class TestRepr:
"""__repr__ generation."""
def test_repr_generated(self) -> None:
@py_class(_unique_key("Repr"))
class Repr(Object):
x: int
y: str
obj = Repr(x=1, y="hello")
r = repr(obj)
assert "1" in r
assert "hello" in r
def test_repr_disabled(self) -> None:
@py_class(_unique_key("NoRepr"), repr=False)
class NoRepr(Object):
x: int
obj = NoRepr(x=1)
# Should use default object repr
r = repr(obj)
assert "NoRepr" in r or "object at" in r
# ###########################################################################
# 9. Equality
# ###########################################################################
class TestEquality:
"""__eq__ and __ne__ generation."""
def test_eq_enabled(self) -> None:
@py_class(_unique_key("Eq"), eq=True)
class Eq(Object):
x: int
y: str
assert Eq(x=1, y="a") == Eq(x=1, y="a")
assert Eq(x=1, y="a") != Eq(x=2, y="a")
def test_eq_disabled_by_default(self) -> None:
@py_class(_unique_key("NoEq"))
class NoEq(Object):
x: int
a = NoEq(x=1)
b = NoEq(x=1)
# Without eq, identity comparison
assert a != b
assert a == a
# ###########################################################################
# 10. Order
# ###########################################################################
class TestOrder:
"""Comparison methods."""
def test_order_enabled(self) -> None:
@py_class(_unique_key("Ord"), eq=True, order=True)
class Ord(Object):
x: int
assert Ord(x=1) < Ord(x=2)
assert Ord(x=2) > Ord(x=1)
assert Ord(x=1) <= Ord(x=1)
assert Ord(x=1) >= Ord(x=1)
# ###########################################################################
# 11. Hash
# ###########################################################################
class TestHash:
"""__hash__ generation."""
def test_unsafe_hash(self) -> None:
@py_class(_unique_key("Hash"), eq=True, unsafe_hash=True)
class Hash(Object):
x: int
a = Hash(x=1)
b = Hash(x=1)
assert hash(a) == hash(b)
# Can be used in sets
s = {a, b}
assert len(s) == 1
# ###########################################################################
# 12. Copy
# ###########################################################################
class TestCopy:
"""__copy__, __deepcopy__, __replace__."""
def test_shallow_copy(self) -> None:
@py_class(_unique_key("SCopy"))
class SCopy(Object):
x: int
obj = SCopy(x=42)
obj2 = copy.copy(obj)
assert obj2.x == 42
def test_deep_copy(self) -> None:
@py_class(_unique_key("DCopy"))
class DCopy(Object):
x: int
obj = DCopy(x=42)
obj2 = copy.deepcopy(obj)
assert obj2.x == 42
def test_replace(self) -> None:
@py_class(_unique_key("Repl"))
class Repl(Object):
x: int
y: str
obj = Repl(x=1, y="a")
obj2 = obj.__replace__(x=2) # ty: ignore[unresolved-attribute]
assert obj2.x == 2
assert obj2.y == "a"
# ###########################################################################
# 13. Inheritance
# ###########################################################################
class TestInheritance:
"""Inheritance between py_class types."""
def test_child_adds_fields(self) -> None:
@py_class(_unique_key("Parent"))
class Parent(Object):
x: int
@py_class(_unique_key("Child"))
class Child(Parent):
y: str
obj = Child(x=1, y="hello")
assert obj.x == 1
assert obj.y == "hello"
def test_child_isinstance(self) -> None:
@py_class(_unique_key("P2"))
class P2(Object):
x: int
@py_class(_unique_key("C2"))
class C2(P2):
y: str
obj = C2(x=1, y="hello")
assert isinstance(obj, C2)
assert isinstance(obj, P2)
assert isinstance(obj, Object)
def test_three_level_inheritance(self) -> None:
@py_class(_unique_key("L1"))
class L1(Object):
a: int
@py_class(_unique_key("L2"))
class L2(L1):
b: int
@py_class(_unique_key("L3"))
class L3(L2):
c: int
obj = L3(a=1, b=2, c=3)
assert obj.a == 1
assert obj.b == 2
assert obj.c == 3
def test_collects_fields_from_non_py_class_parent(self) -> None:
@py_class(_unique_key("NPCNode"))
class Node(Object):
x: int
class BaseBinOp(Node):
lhs: int
rhs: int
@py_class(_unique_key("NPCAdd"))
class Add(BaseBinOp):
pass
obj = Add(lhs=1, rhs=2, x=0) # ty: ignore[unknown-argument]
assert obj.x == 0
assert obj.lhs == 1
assert obj.rhs == 2
assert [f.name for f in fields(Add)] == ["x", "lhs", "rhs"]
assert [f.name for f in _get_type_info(Add).fields] == ["lhs", "rhs"]
def test_collects_non_py_class_parent_field_options(self) -> None:
@py_class(_unique_key("NPCOptNode"))
class Node(Object):
x: int
class BaseOp(Node):
kind: ClassVar[str] = "binop"
lhs: int
hidden: int = field(default=99, init=False)
_: KW_ONLY
rhs: int
@py_class(_unique_key("NPCOptAdd"))
class Add(BaseOp):
scale: int
obj = Add(0, 1, rhs=3, scale=2) # ty: ignore[parameter-already-assigned,unknown-argument]
assert obj.x == 0
assert obj.lhs == 1
assert obj.scale == 2
assert obj.rhs == 3
assert obj.hidden == 99
assert [f.name for f in fields(Add)] == ["x", "lhs", "hidden", "rhs", "scale"]
assert [f.name for f in _get_type_info(Add).fields] == [
"lhs",
"hidden",
"rhs",
"scale",
]
with pytest.raises(TypeError):
Add(0, 1, 2, rhs=3) # ty: ignore[too-many-positional-arguments,unknown-argument]
def test_registered_parent_non_py_class_fields_not_duplicated(self) -> None:
@py_class(_unique_key("NPCDedupNode"))
class Node(Object):
x: int
class BaseBinOp(Node):
lhs: int
rhs: int
@py_class(_unique_key("NPCDedupAdd"))
class Add(BaseBinOp):
op_id: int
@py_class(_unique_key("NPCDedupWeightedAdd"))
class WeightedAdd(Add):
weight: int
obj = WeightedAdd(x=0, lhs=1, rhs=2, op_id=3, weight=4) # ty: ignore[unknown-argument]
assert obj.x == 0
assert obj.lhs == 1
assert obj.rhs == 2
assert obj.op_id == 3
assert obj.weight == 4
assert [f.name for f in fields(WeightedAdd)] == ["x", "lhs", "rhs", "op_id", "weight"]
assert [f.name for f in _get_type_info(WeightedAdd).fields] == ["weight"]
def test_multiple_non_py_class_parents_single_ffi_lineage(self) -> None:
@py_class(_unique_key("MROBase"))
class Base(Object):
x: int
class NonFFIClassC(Base):
c: int
class NonFFIClassA:
a: int
class NonFFIClassB:
b: int
@py_class(_unique_key("MROChild"))
class Child(NonFFIClassA, NonFFIClassB, NonFFIClassC):
y: int
obj = Child(x=0, c=3, b=2, a=1, y=4) # ty: ignore[unknown-argument]
assert obj.x == 0
assert obj.a == 1
assert obj.b == 2
assert obj.c == 3
assert obj.y == 4
assert [f.name for f in fields(Child)] == ["x", "c", "b", "a", "y"]
assert [f.name for f in _get_type_info(Child).fields] == ["c", "b", "a", "y"]
def test_non_py_class_parent_override_preserves_field_position(self) -> None:
@py_class(_unique_key("OverrideBase"))
class Base(Object):
x: int
class Parent(Base):
a: int
b: int
c: int
@py_class(_unique_key("OverrideChild"))
class Child(Parent):
b: str
d: int
obj = Child(x=0, a=1, b="two", c=3, d=4) # ty: ignore[unknown-argument]
assert obj.b == "two"
assert [f.name for f in fields(Child)] == ["x", "a", "b", "c", "d"]
own_fields = _get_type_info(Child).fields
assert [f.name for f in own_fields] == ["a", "b", "c", "d"]
assert own_fields[1].dataclass_field is not None
assert own_fields[1].dataclass_field.type is str
# ###########################################################################
# 14. Forward references / deferred resolution
# ###########################################################################
class TestForwardReferences:
"""Deferred annotation resolution for mutual and self-references."""
@requires_py310
def test_self_reference(self) -> None:
@py_class(_unique_key("SelfRef"))
class SelfRef(Object):
value: int
next_node: SelfRef | None
leaf = SelfRef(value=2, next_node=None)
head = SelfRef(value=1, next_node=leaf)
assert head.next_node is not None
assert head.next_node.value == 2
@requires_py310
def test_mutual_reference(self) -> None:
"""Two classes that reference each other."""
@py_class(_unique_key("Foo"))
class Foo(Object):
value: int
bar: Bar | None
@py_class(_unique_key("Bar"))
class Bar(Object):
value: int
foo: Foo | None
bar = Bar(value=2, foo=None)
foo = Foo(value=1, bar=bar)
assert foo.bar is not None
assert foo.bar.value == 2
@requires_py310
def test_deferred_resolution_on_instantiation(self) -> None:
"""Forward ref resolved on first instantiation."""
@py_class(_unique_key("Early"))
class Early(Object):
value: int
ref: Late | None
# At this point, Early's fields are deferred because Late doesn't exist
@py_class(_unique_key("Late"))
class Late(Object):
value: int
# Now Early should resolve (either via flush or on instantiation)
obj = Early(value=1, ref=Late(value=2))
assert obj.ref is not None
assert obj.ref.value == 2
@requires_py310
def test_cross_module_forward_ref_via_circular_import(self) -> None:
"""Forward ref to a class in another module that isn't in the declaring
module's globals (circular import / TYPE_CHECKING-gated) still resolves.
Mirrors the loom codegen case where ``weave_ir.TaskSpec`` has a field
``body: tuple[Op, ...]`` and ``Op`` lives in ``ops`` — ``ops`` already
imports from ``weave_ir``, so ``Op`` cannot appear in ``weave_ir``'s
globals without introducing a circular import.
"""
# Module B: the target of the forward ref. Define it first so it's
# registered in ``_PY_CLASS_BY_MODULE`` under its own module key when
# module A's phase-2 runs.
ns_b: Dict[str, Any] = {
"__name__": "testing.cross_mod_b",
"py_class": py_class,
"Object": Object,
"_unique_key": _unique_key,
}
exec(
"from __future__ import annotations\n"
"@py_class(_unique_key('CrossChild'))\n"
"class CrossChild(Object):\n"
" x: int\n",
ns_b,
)
child_cls = ns_b["CrossChild"]
assert child_cls.__module__ == "testing.cross_mod_b"
# Module A: references ``CrossChild`` by name but does NOT have it
# in its globals (simulating a ``TYPE_CHECKING``-gated import).
ns_a: Dict[str, Any] = {
"__name__": "testing.cross_mod_a",
"py_class": py_class,
"Object": Object,
"_unique_key": _unique_key,
}
exec(
"from __future__ import annotations\n"
"@py_class(_unique_key('CrossHolder'))\n"
"class Holder(Object):\n"
" value: int\n"
" child: CrossChild | None = None\n",
ns_a,
)
holder_cls = ns_a["Holder"]
assert holder_cls.__module__ == "testing.cross_mod_a"
# Instantiation forces phase-2 to run; the cross-module localns
# fallback should pick up CrossChild from module B even though it's
# neither in module A's globals nor in module A's per-module registry.
child = child_cls(x=7)
holder = holder_cls(value=1, child=child)
assert holder.child is not None
assert holder.child.x == 7
# ###########################################################################
# 15. User-defined dunder preservation
# ###########################################################################
class TestDunderPreservation:
"""User-defined dunders are not overwritten."""
def test_user_repr_preserved(self) -> None:
@py_class(_unique_key("UserRepr"))
class UserRepr(Object):
x: int
def __repr__(self) -> str:
return f"Custom({self.x})"
obj = UserRepr(x=42)
assert repr(obj) == "Custom(42)"
def test_user_eq_preserved(self) -> None:
@py_class(_unique_key("UserEq"), eq=True)
class UserEq(Object):
x: int
def __eq__(self, other: object) -> bool:
return False
assert not (UserEq(x=1) == UserEq(x=1))
# ###########################################################################
# 16. field() API
# ###########################################################################
class TestFieldAPI:
"""field() function returns a Field."""
def test_field_returns_field(self) -> None:
f = field(default=42)
assert isinstance(f, Field)
assert f.default == 42
def test_field_defaults(self) -> None:
f = field()
assert f.init is True
assert f.repr is True
assert f.hash is None # None = follow compare
assert f.compare is True
def test_field_kw_only_missing_by_default(self) -> None:
f = field()
assert f.kw_only is None
def test_field_repr_false(self) -> None:
@py_class(_unique_key("FldRepr"))
class FldRepr(Object):
x: int
y: int = field(default=0, repr=False)
obj = FldRepr(x=1)
r = repr(obj)
assert "1" in r
# y with repr=False should not appear in repr
# (depends on C++ ReprPrint implementation respecting the flag)
# ###########################################################################
# 17. Edge cases
# ###########################################################################
class TestEdgeCases:
"""Edge cases and error conditions."""
def test_no_ffi_parent_raises(self) -> None:
with pytest.raises(TypeError, match="must inherit from"):
@py_class(_unique_key("NoPar"))
class NoPar: # no Object parent!
x: int
def test_only_classvar(self) -> None:
@py_class(_unique_key("OnlyCV"))
class OnlyCV(Object):
count: ClassVar[int] = 0
obj = OnlyCV()
assert obj is not None
def test_mutation_after_creation(self) -> None:
@py_class(_unique_key("Mut"))
class Mut(Object):
x: int
obj = Mut(x=1)
obj.x = 42
assert obj.x == 42
# ###########################################################################
# 18. hash=None tri-state
# ###########################################################################
class TestHashTriState:
"""field(hash=None) means 'follow compare' (native dataclass semantics)."""
def test_hash_none_follows_compare_true(self) -> None:
"""hash=None + compare=True → field participates in hash."""
@py_class(_unique_key("HNT"), eq=True, unsafe_hash=True)
class HNT(Object):
x: int # default: compare=True, hash=None → hash=True
a = HNT(x=1)
b = HNT(x=1)
assert hash(a) == hash(b)
def test_hash_none_follows_compare_false(self) -> None:
"""hash=None + compare=False → field excluded from hash."""
@py_class(_unique_key("HNF"), eq=True, unsafe_hash=True)
class HNF(Object):
x: int
y: int = field(compare=False) # hash=None → follows compare=False
# y doesn't participate in hash, so different y values → same hash
a = HNF(x=1, y=10)
b = HNF(x=1, y=20)
assert hash(a) == hash(b)
def test_hash_explicit_true_with_compare_true(self) -> None:
"""hash=True + compare=True → field participates in hash."""
@py_class(_unique_key("HET"), eq=True, unsafe_hash=True)
class HET(Object):
x: int = field(hash=True) # compare=True (default)
a = HET(x=1)
b = HET(x=2)
assert hash(a) != hash(b)
def test_hash_explicit_false(self) -> None:
"""hash=False excludes field from hashing even with compare=True."""
@py_class(_unique_key("HEF"), eq=True, unsafe_hash=True)
class HEF(Object):
x: int
y: int = field(hash=False) # compare=True but hash=False
a = HEF(x=1, y=10)
b = HEF(x=1, y=20)
assert hash(a) == hash(b)
# ###########################################################################
# 19. Deferred resolution + user __init__ / init=False
# ###########################################################################
class TestDeferredInitPreservation:
"""Deferred resolution preserves user-defined __init__ and init=False."""
@requires_py310
def test_deferred_with_user_init(self) -> None:
"""User-defined __init__ is preserved after deferred resolution."""
@py_class(_unique_key("DefUI"))
class DefUI(Object):
value: int
ref: DefUILate | None
def __init__(self, value: int) -> None:
self.value = value
self.ref = None
@py_class(_unique_key("DefUILate"))
class DefUILate(Object):
x: int
# DefUI should use the user-defined __init__ (one positional arg)
obj = DefUI(42)
assert obj.value == 42
assert obj.ref is None
@requires_py310
def test_deferred_with_init_false(self) -> None:
"""init=False is respected after deferred resolution."""
@py_class(_unique_key("DefNoInit"), init=False)
class DefNoInit(Object):
value: int
ref: DefNoInitLate | None
def __init__(self, v: int) -> None:
self.value = v
self.ref = None
@py_class(_unique_key("DefNoInitLate"))
class DefNoInitLate(Object):
x: int
obj = DefNoInit(10)
assert obj.value == 10
# ###########################################################################
# 21. order=True requires eq=True
# ###########################################################################
class TestOrderEqValidation:
"""order=True without eq=True is rejected."""
def test_order_without_eq_raises(self) -> None:
with pytest.raises(ValueError, match="order=True requires eq=True"):
@py_class(_unique_key("OrdNoEq"), order=True)
class OrdNoEq(Object):
x: int
# ###########################################################################
# 23. Registration rollback on failure
# ###########################################################################
class TestRegistrationRollback:
"""Failed decorations don't permanently poison the type registry."""
def test_failed_decoration_allows_retry(self) -> None:
key = _unique_key("Rollback")
with pytest.raises(Exception):
@py_class(key)
class Bad(Object):
x: object # unsupported annotation type
# The type key should be available for reuse
@py_class(key)
class Good(Object):
x: int
y: int = 0
assert Good(x=1).y == 0
# ###########################################################################
# 24. User-defined __replace__ preserved
# ###########################################################################
class TestUserReplace:
"""User-defined __replace__ is not overwritten by py_class."""
def test_user_replace_preserved(self) -> None:
@py_class(_unique_key("UserRepl"))
class UserRepl(Object):
x: int
def __replace__(self, **changes: object) -> str:
return "custom"
obj = UserRepl(x=1)
assert obj.__replace__(x=2) == "custom"
# ###########################################################################
# 25. default_factory=None raises
# ###########################################################################
class TestDefaultFactoryNone:
"""Explicit default_factory=None matches stdlib semantics (raises)."""
def test_explicit_none_raises(self) -> None:
with pytest.raises(TypeError, match="default_factory must be a callable"):
field(default_factory=None)
# ###########################################################################
# 26. Adversarial edge cases for init reordering
# ###########################################################################
class TestInitReorderingAdversarial:
"""Tricky scenarios that catch bugs in naive init-signature generation."""
def test_positional_call_maps_to_required_not_declared_order(self) -> None:
"""Positional arg 1 maps to the first *required* field, not the first declared."""
@py_class(_unique_key("PosMap"))
class PosMap(Object):
x: int = 0 # optional, declared first
y: int # ty: ignore[dataclass-field-order] # required, declared second
# Positional call: first arg is y (required), not x (optional)
obj = PosMap(42) # ty: ignore[missing-argument]
assert obj.y == 42
assert obj.x == 0
def test_relative_order_preserved_within_groups(self) -> None:
"""Within required and optional groups, declaration order is preserved."""
@py_class(_unique_key("RelOrder"))
class RelOrder(Object):
a: int = 0
b: int # ty: ignore[dataclass-field-order]
c: int = 1
d: int # ty: ignore[dataclass-field-order]
sig = inspect.signature(RelOrder.__init__)
param_names = [n for n in sig.parameters if n != "self"]
# required: b, d (declaration order); optional: a, c (declaration order)
assert param_names == ["b", "d", "a", "c"]
obj = RelOrder(10, 20) # ty: ignore[missing-argument]
assert obj.b == 10
assert obj.d == 20
assert obj.a == 0
assert obj.c == 1
def test_default_factory_counts_as_optional(self) -> None:
"""default_factory makes a field optional for reordering purposes."""
@py_class(_unique_key("DFReorder"))
class DFReorder(Object):
items: str = field(default_factory=lambda: "hello")
count: int # ty: ignore[dataclass-field-order]
sig = inspect.signature(DFReorder.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert param_names[0] == "count" # required first
assert param_names[1] == "items" # optional (factory) second
obj = DFReorder(count=5)
assert obj.count == 5
assert obj.items == "hello"
def test_three_level_hierarchy_reorder(self) -> None:
"""Required fields from all levels come before optional fields from all levels."""
@py_class(_unique_key("G1"))
class G1(Object):
a: int # required
@py_class(_unique_key("P1"))
class P1(G1):
b: int = 0 # optional
@py_class(_unique_key("C1"))
class C1(P1):
c: int # required
sig = inspect.signature(C1.__init__)
param_names = [n for n in sig.parameters if n != "self"]
# required (a, c) before optional (b)
assert param_names == ["a", "c", "b"]
obj = C1(a=1, c=3)
assert obj.a == 1
assert obj.b == 0
assert obj.c == 3
def test_kw_only_false_overrides_sentinel(self) -> None:
"""kw_only=False on a field after KW_ONLY sentinel makes it positional."""
@py_class(_unique_key("KwOverride"))
class KwOverride(Object):
_: KW_ONLY
a: int # kw_only (inherits sentinel)
b: int = field(kw_only=False) # positional (explicit override)
sig = inspect.signature(KwOverride.__init__)
assert sig.parameters["a"].kind == inspect.Parameter.KEYWORD_ONLY
assert sig.parameters["b"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
obj = KwOverride(42, a=1) # ty: ignore[missing-argument,invalid-argument-type]
assert obj.b == 42
assert obj.a == 1
def test_init_false_field_gets_default(self) -> None:
"""init=False field with default is set to default, not left uninitialized."""
@py_class(_unique_key("InitFalseDef"))
class InitFalseDef(Object):
visible: int
hidden: str = field(default="secret", init=False)
obj = InitFalseDef(visible=1)
assert obj.hidden == "secret"
def test_post_init_sees_reordered_fields(self) -> None:
"""__post_init__ sees correct values even when __init__ reorders fields."""
seen: Dict[str, int] = {}
@py_class(_unique_key("PostReorder"))
class PostReorder(Object):
x: int = 0
y: int # ty: ignore[dataclass-field-order]
def __post_init__(self) -> None:
seen["x"] = self.x
seen["y"] = self.y
PostReorder(y=10, x=20)
assert seen == {"x": 20, "y": 10}
@requires_py310
def test_deferred_forward_ref_with_reordering(self) -> None:
"""Deferred forward-reference resolution still produces correct reordering."""
@py_class(_unique_key("DeferReorder"))
class DeferReorder(Object):
opt: DeferLate | None = None
req: int # ty: ignore[dataclass-field-order]
@py_class(_unique_key("DeferLate"))
class DeferLate(Object):
x: int
sig = inspect.signature(DeferReorder.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert param_names[0] == "req"
assert param_names[1] == "opt"
obj = DeferReorder(req=1)
assert obj.req == 1
assert obj.opt is None
def test_all_optional_preserves_declaration_order(self) -> None:
"""When all fields are optional, declaration order is preserved."""
@py_class(_unique_key("AllOpt"))
class AllOpt(Object):
c: int = 3
a: int = 1
b: int = 2
sig = inspect.signature(AllOpt.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert param_names == ["c", "a", "b"]
obj = AllOpt()
assert obj.c == 3
assert obj.a == 1
assert obj.b == 2
def test_all_required_preserves_declaration_order(self) -> None:
"""When all fields are required, declaration order is preserved."""
@py_class(_unique_key("AllReq"))
class AllReq(Object):
c: int
a: int
b: int
sig = inspect.signature(AllReq.__init__)
param_names = [n for n in sig.parameters if n != "self"]
assert param_names == ["c", "a", "b"]
obj = AllReq(10, 20, 30)
assert obj.c == 10
assert obj.a == 20
assert obj.b == 30
# ###########################################################################
# 1. Registration
# ###########################################################################
class TestRegisterPyClass:
"""Low-level _register_py_class: type allocation, ancestors, field lifecycle."""
def test_basic_registration(self) -> None:
type_key = _unique_key_ff("RegBasic")
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls = type("RegBasic", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
assert info is not None
assert info.type_key == type_key
def test_type_index_allocated(self) -> None:
type_key = _unique_key_ff("RegIndex")
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls = type("RegIndex", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
assert isinstance(info.type_index, int)
assert info.type_index > 0
def test_ancestors_include_parent(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
type_key = _unique_key_ff("RegAncestors")
cls = type("RegAncestors", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
assert parent_info.type_index in info.type_ancestors
def test_parent_type_info_set(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
type_key = _unique_key_ff("RegParent")
cls = type("RegParent", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
assert info.parent_type_info is parent_info
def test_initial_fields_none_and_methods_empty(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
type_key = _unique_key_ff("RegEmpty")
cls = type("RegEmpty", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, type_key, cls)
assert info.fields is None
assert len(info.methods) == 0
def test_two_registrations_different_indices(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls1 = type("RegDiff1", (core.Object,), {"__slots__": ()})
cls2 = type("RegDiff2", (core.Object,), {"__slots__": ()})
info1 = core._register_py_class(parent_info, _unique_key_ff("RegDiff1"), cls1)
info2 = core._register_py_class(parent_info, _unique_key_ff("RegDiff2"), cls2)
assert info1.type_index != info2.type_index
def test_fields_none_before_registration(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls = type("Pending", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, _unique_key_ff("Pending"), cls)
assert info.fields is None
def test_register_fields_is_instance_method(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls = type("PendingM", (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, _unique_key_ff("PendingM"), cls)
assert hasattr(info, "_register_fields")
def test_duplicate_type_key_raises(self) -> None:
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
type_key = _unique_key_ff("Dup")
cls1 = type("Dup1", (core.Object,), {"__slots__": ()})
core._register_py_class(parent_info, type_key, cls1)
cls2 = type("Dup2", (core.Object,), {"__slots__": ()})
with pytest.raises((RuntimeError, ValueError)):
core._register_py_class(parent_info, type_key, cls2)
def test_duplicate_type_key_preserves_original(self) -> None:
"""After rejected duplicate, original entry is intact."""
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
type_key = _unique_key_ff("DupPreserve")
cls1 = type("DupPreserve1", (core.Object,), {"__slots__": ()})
info1 = core._register_py_class(parent_info, type_key, cls1)
info1._register_fields([Field(name="x", _ty_schema=TypeSchema("int"))])
setattr(cls1, "__tvm_ffi_type_info__", info1)
_add_class_attrs(cls1, info1)
cls2 = type("DupPreserve2", (core.Object,), {"__slots__": ()})
with pytest.raises((RuntimeError, ValueError)):
core._register_py_class(parent_info, type_key, cls2)
reloaded = core._lookup_or_register_type_info_from_type_key(type_key)
assert reloaded.type_cls is cls1
assert [f.name for f in reloaded.fields] == ["x"]
# ###########################################################################
# 2. Field Registration
# ###########################################################################
class TestFieldRegistration:
"""Low-level _register_fields: field types, metadata, offsets."""
def test_int_field_registered(self) -> None:
cls = _make_type(
"FldInt",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert len(info.fields) == 1
assert info.fields[0].name == "x"
def test_float_field_registered(self) -> None:
cls = _make_type(
"FldFloat",
[Field(name="val", _ty_schema=TypeSchema("float"), default=0.0)],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert info.fields[0].name == "val"
def test_str_field_registered(self) -> None:
cls = _make_type(
"FldStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default="hello")],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert info.fields[0].name == "s"
def test_bool_field_registered(self) -> None:
cls = _make_type(
"FldBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=False)],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert info.fields[0].name == "flag"
def test_multiple_fields_count(self) -> None:
cls = _make_type(
"FldMulti",
[
Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("float"), default=0.0),
Field(name="c", _ty_schema=TypeSchema("str"), default="x"),
],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert len(info.fields) == 3
assert [f.name for f in info.fields] == ["a", "b", "c"]
def test_field_offsets_increasing(self) -> None:
cls = _make_type(
"FldOff",
[
Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("float"), default=MISSING),
Field(name="c", _ty_schema=TypeSchema("str"), default=MISSING),
],
)
info = getattr(cls, "__tvm_ffi_type_info__")
offsets = [f.offset for f in info.fields]
for i in range(1, len(offsets)):
assert offsets[i] > offsets[i - 1], f"Field offsets not increasing: {offsets}"
def test_ffi_init_method_registered(self) -> None:
cls = _make_type(
"FldInit",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
info = getattr(cls, "__tvm_ffi_type_info__")
ffi_init = core._lookup_type_attr(info.type_index, "__ffi_init__")
assert ffi_init is not None
def test_field_metadata_repr_flag(self) -> None:
cls = _make_type(
"FldReprMeta",
[
Field(
name="visible",
_ty_schema=TypeSchema("int"),
default=MISSING,
repr=True,
),
Field(
name="hidden",
_ty_schema=TypeSchema("int"),
default=0,
repr=False,
),
],
)
info = getattr(cls, "__tvm_ffi_type_info__")
assert len(info.fields) == 2
# ###########################################################################
# 3. Field Descriptor
# ###########################################################################
class TestFieldDescriptor:
"""Field class: validation, defaults, default_factory checks."""
def test_compare_default_is_false(self) -> None:
f = Field(name="x", _ty_schema=TypeSchema("int"))
assert f.compare is False
def test_default_and_factory_mutually_exclusive(self) -> None:
with pytest.raises(ValueError):
Field(name="x", _ty_schema=TypeSchema("int"), default=0, default_factory=lambda: 0)
def test_factory_must_be_callable(self) -> None:
with pytest.raises(TypeError, match="callable"):
Field(name="x", _ty_schema=TypeSchema("int"), default_factory=0) # ty: ignore[invalid-argument-type]
def test_non_callable_factory_rejected(self) -> None:
with pytest.raises(TypeError, match="callable"):
Field(name="x", _ty_schema=TypeSchema("int"), default_factory="not_callable") # ty: ignore[invalid-argument-type]
# ###########################################################################
# 4. Construction
# ###########################################################################
class TestConstruction:
"""Low-level __init__ via _make_type: positional/keyword args, defaults, errors."""
def test_keyword_args(self) -> None:
Cls = _make_type(
"ConKw",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
],
)
obj = Cls(x=42, y=3.14)
assert obj.x == 42
assert obj.y == pytest.approx(3.14)
def test_positional_args(self) -> None:
Cls = _make_type(
"ConPos",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
],
)
obj = Cls(10, 2.5)
assert obj.x == 10
assert obj.y == pytest.approx(2.5)
def test_mixed_positional_and_keyword(self) -> None:
Cls = _make_type(
"ConMixed",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
],
)
obj = Cls(7, y=1.5)
assert obj.x == 7
assert obj.y == pytest.approx(1.5)
def test_default_value_int(self) -> None:
Cls = _make_type(
"ConDefInt",
[Field(name="x", _ty_schema=TypeSchema("int"), default=99)],
)
assert Cls().x == 99
def test_default_value_float(self) -> None:
Cls = _make_type(
"ConDefFloat",
[Field(name="x", _ty_schema=TypeSchema("float"), default=1.5)],
)
assert Cls().x == pytest.approx(1.5)
def test_default_value_str(self) -> None:
Cls = _make_type(
"ConDefStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default="hello")],
)
assert Cls().s == "hello"
def test_override_default(self) -> None:
Cls = _make_type(
"ConOverride",
[Field(name="x", _ty_schema=TypeSchema("int"), default=0)],
)
assert Cls(x=42).x == 42
def test_required_and_optional_together(self) -> None:
Cls = _make_type(
"ConReqOpt",
[
Field(name="required", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="optional", _ty_schema=TypeSchema("float"), default=0.0),
],
)
obj = Cls(required=5)
assert obj.required == 5
assert obj.optional == pytest.approx(0.0)
def test_missing_required_raises(self) -> None:
Cls = _make_type(
"ConMissing",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
with pytest.raises(TypeError):
Cls()
def test_extra_kwarg_raises(self) -> None:
Cls = _make_type(
"ConExtra",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
with pytest.raises(TypeError):
Cls(x=1, bogus=2)
def test_str_field_construction(self) -> None:
Cls = _make_type(
"ConStr",
[Field(name="name", _ty_schema=TypeSchema("str"), default=MISSING)],
)
assert Cls(name="world").name == "world"
def test_bool_field_construction(self) -> None:
Cls = _make_type(
"ConBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
assert Cls(flag=True).flag is True
assert Cls(flag=False).flag is False
def test_kw_only_field(self) -> None:
Cls = _make_type(
"ConKwOnly",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="y",
_ty_schema=TypeSchema("int"),
default=MISSING,
kw_only=True,
),
],
)
obj = Cls(1, y=2)
assert obj.x == 1
assert obj.y == 2
def test_kw_only_rejects_positional(self) -> None:
Cls = _make_type(
"ConKwOnlyReject",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="y",
_ty_schema=TypeSchema("int"),
default=MISSING,
kw_only=True,
),
],
)
with pytest.raises(TypeError):
Cls(1, 2)
def test_isinstance_check(self) -> None:
Cls = _make_type(
"ConIsInstance",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=1)
assert isinstance(obj, Cls)
assert isinstance(obj, core.Object)
# ###########################################################################
# 5. Getter / Setter
# ###########################################################################
class TestGetterSetter:
"""Field access: get/set POD, str, bool, mutation isolation."""
def test_get_int(self) -> None:
Cls = _make_type(
"GSInt",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
assert Cls(x=42).x == 42
def test_set_int(self) -> None:
Cls = _make_type(
"GSSetInt",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=1)
obj.x = 100
assert obj.x == 100
def test_get_float(self) -> None:
Cls = _make_type(
"GSFloat",
[Field(name="val", _ty_schema=TypeSchema("float"), default=MISSING)],
)
assert Cls(val=3.14).val == pytest.approx(3.14)
def test_set_float(self) -> None:
Cls = _make_type(
"GSSetFloat",
[Field(name="val", _ty_schema=TypeSchema("float"), default=MISSING)],
)
obj = Cls(val=1.0)
obj.val = 2.718
assert obj.val == pytest.approx(2.718)
def test_get_str(self) -> None:
Cls = _make_type(
"GSStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
assert Cls(s="hello").s == "hello"
def test_set_str(self) -> None:
Cls = _make_type(
"GSSetStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
obj = Cls(s="hello")
obj.s = "world"
assert obj.s == "world"
def test_get_bool(self) -> None:
Cls = _make_type(
"GSBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
assert Cls(flag=True).flag is True
def test_set_bool(self) -> None:
Cls = _make_type(
"GSSetBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
obj = Cls(flag=True)
obj.flag = False
assert obj.flag is False
def test_mutation_isolated(self) -> None:
Cls = _make_type(
"GSIsolate",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
a = Cls(x=1)
b = Cls(x=1)
a.x = 99
assert a.x == 99
assert b.x == 1
def test_multiple_fields_mutation(self) -> None:
Cls = _make_type(
"GSMultiMut",
[
Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("float"), default=MISSING),
Field(name="c", _ty_schema=TypeSchema("str"), default=MISSING),
],
)
obj = Cls(a=1, b=2.0, c="x")
obj.a = 10
obj.b = 20.0
obj.c = "y"
assert obj.a == 10
assert obj.b == pytest.approx(20.0)
assert obj.c == "y"
def test_set_array_field(self) -> None:
Cls = _make_type(
"GSSetArr",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(arr=[1])
obj.arr = tvm_ffi.Array([4, 5, 6])
assert len(obj.arr) == 3
assert obj.arr[0] == 4
# ###########################################################################
# 6. ObjectRef Fields
# ###########################################################################
class TestObjectRefFields:
"""Fields holding ObjectRef types: Array, custom objects."""
def test_array_field(self) -> None:
Cls = _make_type(
"ObjArr",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(arr=tvm_ffi.Array([1, 2, 3]))
assert len(obj.arr) == 3
assert obj.arr[0] == 1
assert obj.arr[2] == 3
def test_array_field_from_list(self) -> None:
Cls = _make_type(
"ObjArrList",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(arr=[10, 20, 30])
assert len(obj.arr) == 3
assert obj.arr[1] == 20
def test_nested_object_field(self) -> None:
Inner = _make_type(
"ObjInner",
[Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING)],
)
inner_info = getattr(Inner, "__tvm_ffi_type_info__")
inner_schema = TypeSchema(inner_info.type_key, origin_type_index=inner_info.type_index)
Outer = _make_type(
"ObjOuter",
[Field(name="child", _ty_schema=inner_schema, default=MISSING)],
)
assert Outer(child=Inner(val=42)).child.val == 42
# ###########################################################################
# 7. Optional Fields
# ###########################################################################
class TestOptionalFields:
"""Optional/Union fields: None and non-None values."""
def test_optional_int_with_value(self) -> None:
Cls = _make_type(
"OptIntV",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=MISSING,
),
],
)
assert Cls(x=42).x == 42
def test_optional_int_with_none(self) -> None:
Cls = _make_type(
"OptIntN",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
],
)
assert Cls().x is None
def test_optional_str_with_value(self) -> None:
Cls = _make_type(
"OptStrV",
[
Field(
name="s",
_ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
default=MISSING,
),
],
)
assert Cls(s="hello").s == "hello"
def test_optional_str_with_none(self) -> None:
Cls = _make_type(
"OptStrN",
[
Field(
name="s",
_ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
default=None,
),
],
)
assert Cls().s is None
def test_optional_set_to_none(self) -> None:
Cls = _make_type(
"OptSet",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(x=42)
obj.x = None
assert obj.x is None
def test_optional_set_back_to_value(self) -> None:
Cls = _make_type(
"OptBack",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
],
)
obj = Cls()
obj.x = 99
assert obj.x == 99
def test_all_optional_fields_default_none(self) -> None:
Cls = _make_type(
"AllOpt",
[
Field(
name="a",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
Field(
name="b",
_ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
default=None,
),
Field(
name="c",
_ty_schema=TypeSchema("Optional", (TypeSchema("float"),)),
default=None,
),
],
)
obj = Cls()
assert obj.a is None
assert obj.b is None
assert obj.c is None
obj.a = 42
assert obj.a == 42
obj.b = "hello"
assert obj.b == "hello"
def test_optional_object_none_and_back(self) -> None:
Cls = _make_type(
"OptObjRound",
[
Field(
name="ref",
_ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
default=None,
),
],
)
obj = Cls()
assert obj.ref is None
obj.ref = tvm_ffi.Array([1])
assert len(obj.ref) == 1
obj.ref = None
assert obj.ref is None
def test_union_int_str(self) -> None:
"""Union[int, str] field should accept both types."""
Cls = _make_type(
"UnionIntStr",
[
Field(
name="val",
_ty_schema=TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),
default=MISSING,
),
],
)
obj = Cls(val=42)
assert obj.val == 42
obj.val = "hello"
assert obj.val == "hello"
def test_union_int_str_rejects_float(self) -> None:
"""Union[int, str] should reject float (not in union)."""
Cls = _make_type(
"UnionReject",
[
Field(
name="val",
_ty_schema=TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),
default=MISSING,
),
],
)
obj = Cls(val=1)
with pytest.raises((TypeError, RuntimeError)):
obj.val = 3.14
def test_optional_union(self) -> None:
"""Optional[Union[int, str]] should accept None, int, and str."""
Cls = _make_type(
"OptUnion",
[
Field(
name="val",
_ty_schema=TypeSchema(
"Optional",
(TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),),
),
default=None,
),
],
)
obj = Cls()
assert obj.val is None
obj.val = 42
assert obj.val == 42
obj.val = "hi"
assert obj.val == "hi"
obj.val = None
assert obj.val is None
# ###########################################################################
# 8. Any Fields
# ###########################################################################
class TestAnyField:
"""Fields with TypeSchema('Any'): hold any value type."""
def test_any_holds_int(self) -> None:
Cls = _make_type(
"AnyI",
[Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
)
assert Cls(val=42).val == 42
def test_any_holds_str(self) -> None:
Cls = _make_type(
"AnyS",
[Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
)
assert Cls(val="hello").val == "hello"
def test_any_holds_none(self) -> None:
Cls = _make_type(
"AnyN",
[Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
)
assert Cls().val is None
def test_any_holds_object(self) -> None:
Cls = _make_type(
"AnyObj",
[Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
)
arr = tvm_ffi.Array([1, 2])
assert len(Cls(val=arr).val) == 2
def test_any_type_change(self) -> None:
Cls = _make_type(
"AnyChg",
[Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
)
obj = Cls()
obj.val = 42
assert obj.val == 42
obj.val = "hello"
assert obj.val == "hello"
obj.val = None
assert obj.val is None
obj.val = tvm_ffi.Array([1])
assert len(obj.val) == 1
# ###########################################################################
# 9. Default Factory
# ###########################################################################
class TestDefaultFactory:
"""default_factory support: fresh instances, override, various types."""
def test_factory_produces_fresh_instances(self) -> None:
Cls = _make_type(
"DFFresh",
[
Field(
name="data",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default_factory=lambda: tvm_ffi.Array([]),
),
],
)
a = Cls()
b = Cls()
assert not a.data.same_as(b.data)
def test_factory_with_content(self) -> None:
Cls = _make_type(
"DFContent",
[
Field(
name="items",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default_factory=lambda: tvm_ffi.Array([1, 2, 3]),
),
],
)
obj = Cls()
assert len(obj.items) == 3
assert obj.items[0] == 1
def test_factory_override(self) -> None:
Cls = _make_type(
"DFOverride",
[Field(name="x", _ty_schema=TypeSchema("int"), default_factory=lambda: 42)],
)
assert Cls(x=99).x == 99
def test_factory_str(self) -> None:
Cls = _make_type(
"DFStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default_factory=lambda: "generated")],
)
assert Cls().s == "generated"
# ###########################################################################
# 10. Repr
# ###########################################################################
class TestFieldRepr:
"""Low-level repr via _make_type: field values, repr=False exclusion."""
def test_repr_includes_fields(self) -> None:
Cls = _make_type(
"ReprBasic",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("float"), default=0.0),
],
)
r = ReprPrint(Cls(x=42, y=3.14))
assert "x=42" in r
assert "y=3.14" in r
def test_repr_str_field(self) -> None:
Cls = _make_type(
"ReprStr",
[Field(name="name", _ty_schema=TypeSchema("str"), default=MISSING)],
)
assert '"hello"' in ReprPrint(Cls(name="hello"))
def test_repr_bool_field(self) -> None:
Cls = _make_type(
"ReprBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
assert "flag=True" in ReprPrint(Cls(flag=True))
def test_repr_false_excluded(self) -> None:
Cls = _make_type(
"ReprExcl",
[
Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="hidden",
_ty_schema=TypeSchema("int"),
default=0,
repr=False,
),
],
)
r = ReprPrint(Cls(visible=42))
assert "visible=42" in r
assert "hidden" not in r
def test_python_repr_delegates(self) -> None:
Cls = _make_type(
"ReprDeleg",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
assert "x=7" in repr(Cls(x=7))
def test_repr_contains_type_key(self) -> None:
Cls = _make_type(
"ReprKey",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
info = getattr(Cls, "__tvm_ffi_type_info__")
assert info.type_key in ReprPrint(Cls(x=1))
def test_repr_optional_none(self) -> None:
Cls = _make_type(
"ReprOptNone",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
],
)
r = ReprPrint(Cls())
assert isinstance(r, str)
def test_repr_array_field(self) -> None:
Cls = _make_type(
"ReprArr",
[
Field(
name="items",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
r = ReprPrint(Cls(items=[1, 2, 3]))
assert isinstance(r, str)
# ###########################################################################
# 11. Hash
# ###########################################################################
class TestFieldHash:
"""Low-level hash via _make_type: equal objects same hash, hash=False exclusion."""
def test_equal_objects_same_hash(self) -> None:
Cls = _make_type(
"HashEq",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="y",
_ty_schema=TypeSchema("float"),
default=MISSING,
compare=True,
),
],
eq=True,
unsafe_hash=True,
)
assert RecursiveHash(Cls(x=1, y=2.0)) == RecursiveHash(Cls(x=1, y=2.0))
def test_different_objects_different_hash(self) -> None:
Cls = _make_type(
"HashDiff",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
unsafe_hash=True,
)
assert RecursiveHash(Cls(x=1)) != RecursiveHash(Cls(x=2))
def test_hash_false_field_ignored(self) -> None:
Cls = _make_type(
"HashIgn",
[
Field(
name="key",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="ignored",
_ty_schema=TypeSchema("int"),
default=0,
hash=False,
),
],
eq=True,
unsafe_hash=True,
)
assert RecursiveHash(Cls(key=42, ignored=100)) == RecursiveHash(Cls(key=42, ignored=999))
def test_hash_dunder_installed(self) -> None:
Cls = _make_type(
"HashDunder",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
eq=True,
unsafe_hash=True,
)
assert isinstance(hash(Cls(x=42)), int)
def test_usable_as_dict_key(self) -> None:
Cls = _make_type(
"HashDict",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
unsafe_hash=True,
)
assert {Cls(x=1): "value"}[Cls(x=1)] == "value"
def test_usable_in_set(self) -> None:
Cls = _make_type(
"HashSet",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
unsafe_hash=True,
)
assert len({Cls(x=1), Cls(x=1), Cls(x=2)}) == 2
# ###########################################################################
# 12. Equality
# ###########################################################################
class TestFieldEquality:
"""Low-level equality via _make_type: structural compare, compare=False exclusion."""
def test_equal_objects(self) -> None:
Cls = _make_type(
"EqEqual",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="y",
_ty_schema=TypeSchema("float"),
default=MISSING,
compare=True,
),
],
eq=True,
)
assert Cls(x=1, y=2.0) == Cls(x=1, y=2.0)
def test_different_objects(self) -> None:
Cls = _make_type(
"EqDiff",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
)
assert Cls(x=1) != Cls(x=2)
def test_compare_false_field_ignored(self) -> None:
Cls = _make_type(
"EqIgn",
[
Field(
name="key",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="ignored",
_ty_schema=TypeSchema("int"),
default=0,
compare=False,
),
],
eq=True,
)
assert RecursiveEq(Cls(key=42, ignored=100), Cls(key=42, ignored=999))
def test_compare_off_excludes_from_eq(self) -> None:
"""Fields with compare=False (default) are ignored by RecursiveEq."""
Cls = _make_type(
"CmpOff",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
eq=True,
)
assert RecursiveEq(Cls(x=1), Cls(x=2))
def test_compare_true_includes_in_eq(self) -> None:
Cls = _make_type(
"CmpOn",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
)
assert not RecursiveEq(Cls(x=1), Cls(x=2))
def test_eq_reflexive(self) -> None:
Cls = _make_type(
"EqRefl",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
)
a = Cls(x=42)
assert a == a
def test_eq_symmetric(self) -> None:
Cls = _make_type(
"EqSym",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
],
eq=True,
)
a, b = Cls(x=1), Cls(x=1)
assert a == b
assert b == a
def test_eq_with_str_field(self) -> None:
Cls = _make_type(
"EqStr",
[
Field(
name="s",
_ty_schema=TypeSchema("str"),
default=MISSING,
compare=True,
),
],
eq=True,
)
assert RecursiveEq(Cls(s="hello"), Cls(s="hello"))
assert not RecursiveEq(Cls(s="hello"), Cls(s="world"))
def test_eq_hash_consistency(self) -> None:
Cls = _make_type(
"EqHashCon",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="y",
_ty_schema=TypeSchema("float"),
default=MISSING,
compare=True,
),
],
eq=True,
unsafe_hash=True,
)
a, b = Cls(x=1, y=2.0), Cls(x=1, y=2.0)
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
# ###########################################################################
# 13. Edge Cases
# ###########################################################################
class TestFieldEdgeCases:
"""Low-level edge cases via _make_type: empty class, extreme values, init=False."""
def test_empty_class_no_fields(self) -> None:
Cls = _make_type("EdgeEmpty", [])
obj = Cls()
assert isinstance(obj, core.Object)
assert isinstance(obj, Cls)
def test_empty_class_repr(self) -> None:
Cls = _make_type("EdgeEmptyRepr", [])
info = getattr(Cls, "__tvm_ffi_type_info__")
assert info.type_key in ReprPrint(Cls())
def test_bool_true_and_false(self) -> None:
Cls = _make_type(
"EdgeBool",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
assert Cls(flag=True).flag is True
assert Cls(flag=False).flag is False
def test_bool_default_false(self) -> None:
Cls = _make_type(
"EdgeBoolDef",
[Field(name="flag", _ty_schema=TypeSchema("bool"), default=False)],
)
assert Cls().flag is False
def test_multiple_types_together(self) -> None:
Cls = _make_type(
"EdgeMulti",
[
Field(
name="i",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
],
)
obj = Cls(i=42, f=3.14, s="test", b=True)
assert obj.i == 42
assert obj.f == pytest.approx(3.14)
assert obj.s == "test"
assert obj.b is True
def test_pod_and_objectref_mixed(self) -> None:
Cls = _make_type(
"EdgeMixed",
[
Field(name="count", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="items",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
Field(name="label", _ty_schema=TypeSchema("str"), default=""),
],
)
obj = Cls(count=3, items=[1, 2, 3])
assert obj.count == 3
assert len(obj.items) == 3
assert obj.label == ""
def test_multiple_types_with_defaults(self) -> None:
Cls = _make_type(
"EdgeMultiDef",
[
Field(name="i", _ty_schema=TypeSchema("int"), default=0),
Field(name="f", _ty_schema=TypeSchema("float"), default=1.0),
Field(name="s", _ty_schema=TypeSchema("str"), default="default"),
Field(name="b", _ty_schema=TypeSchema("bool"), default=True),
],
)
obj = Cls()
assert obj.i == 0
assert obj.f == pytest.approx(1.0)
assert obj.s == "default"
assert obj.b is True
def test_zero_values(self) -> None:
Cls = _make_type(
"EdgeZero",
[
Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
],
)
obj = Cls(i=0, f=0.0)
assert obj.i == 0
assert obj.f == 0.0
def test_negative_values(self) -> None:
Cls = _make_type(
"EdgeNeg",
[
Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
],
)
obj = Cls(i=-42, f=-3.14)
assert obj.i == -42
assert obj.f == pytest.approx(-3.14)
def test_large_int(self) -> None:
Cls = _make_type(
"EdgeLargeInt",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
large = 2**62
assert Cls(x=large).x == large
def test_empty_string_field(self) -> None:
Cls = _make_type(
"EdgeEmptyStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
assert Cls(s="").s == ""
def test_long_string_field(self) -> None:
Cls = _make_type(
"EdgeLongStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
long_str = "a" * 1000
assert Cls(s=long_str).s == long_str
def test_equality_empty_class(self) -> None:
Cls = _make_type("EdgeEmptyEq", [], eq=True, unsafe_hash=True)
assert RecursiveEq(Cls(), Cls())
assert RecursiveHash(Cls()) == RecursiveHash(Cls())
def test_init_false_field_excluded_from_init(self) -> None:
Cls = _make_type(
"EdgeInitFalse",
[
Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="internal",
_ty_schema=TypeSchema("int"),
default=0,
init=False,
),
],
)
obj = Cls(visible=42)
assert obj.visible == 42
assert obj.internal == 0
def test_init_false_field_rejected_as_kwarg(self) -> None:
Cls = _make_type(
"EdgeInitFalseReject",
[
Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="internal",
_ty_schema=TypeSchema("int"),
default=0,
init=False,
),
],
)
with pytest.raises(TypeError):
Cls(visible=1, internal=2)
def test_init_false_field_writable(self) -> None:
Cls = _make_type(
"EdgeInitFalseWrite",
[
Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="internal",
_ty_schema=TypeSchema("int"),
default=0,
init=False,
),
],
)
obj = Cls(visible=1)
obj.internal = 99
assert obj.internal == 99
# ###########################################################################
# 14. Inheritance (Python-defined parent)
# ###########################################################################
class TestFieldInheritance:
"""Low-level inheritance via _make_type: field offsets, parent-child layout."""
def test_child_fields_after_parent(self) -> None:
Parent = _make_type(
"InhParent",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
Child = _make_type(
"InhChild",
[Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=Parent,
)
obj = Child(1, 2)
assert obj.x == 1
assert obj.y == 2
def test_child_field_offsets_non_overlapping(self) -> None:
Parent = _make_type(
"InhParentOff",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
Child = _make_type(
"InhChildOff",
[Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=Parent,
)
p_info = getattr(Parent, "__tvm_ffi_type_info__")
c_info = getattr(Child, "__tvm_ffi_type_info__")
parent_end = p_info.fields[0].offset + p_info.fields[0].size
assert c_info.fields[0].offset >= parent_end
def test_mutation_no_aliasing(self) -> None:
Parent = _make_type(
"InhParentAlias",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
Child = _make_type(
"InhChildAlias",
[Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=Parent,
)
obj = Child(1, 2)
obj.y = 9
assert obj.x == 1
assert obj.y == 9
def test_three_level_inheritance(self) -> None:
"""Object → A → B → C: all fields accessible and non-overlapping."""
A = _make_type(
"InhA",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
)
B = _make_type(
"InhB",
[Field(name="b", _ty_schema=TypeSchema("str"), default=MISSING)],
parent=A,
)
C = _make_type(
"InhC",
[Field(name="c", _ty_schema=TypeSchema("float"), default=MISSING)],
parent=B,
)
obj = C(a=1, b="two", c=3.0)
assert obj.a == 1
assert obj.b == "two"
assert obj.c == pytest.approx(3.0)
def test_three_level_offsets_non_overlapping(self) -> None:
A = _make_type(
"InhAOff",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
)
B = _make_type(
"InhBOff",
[Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=A,
)
C = _make_type(
"InhCOff",
[Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=B,
)
a_info = getattr(A, "__tvm_ffi_type_info__")
b_info = getattr(B, "__tvm_ffi_type_info__")
c_info = getattr(C, "__tvm_ffi_type_info__")
a_end = a_info.fields[0].offset + a_info.fields[0].size
b_end = b_info.fields[0].offset + b_info.fields[0].size
assert b_info.fields[0].offset >= a_end
assert c_info.fields[0].offset >= b_end
def test_three_level_mutation_no_aliasing(self) -> None:
A = _make_type(
"InhAMut",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
)
B = _make_type(
"InhBMut",
[Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=A,
)
C = _make_type(
"InhCMut",
[Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=B,
)
obj = C(a=1, b=2, c=3)
obj.c = 99
assert obj.a == 1
assert obj.b == 2
assert obj.c == 99
obj.a = 77
assert obj.a == 77
assert obj.b == 2
assert obj.c == 99
def test_three_level_isinstance(self) -> None:
A = _make_type(
"InhAIs",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
)
B = _make_type(
"InhBIs",
[Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=A,
)
C = _make_type(
"InhCIs",
[Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=B,
)
obj = C(a=1, b=2, c=3)
assert isinstance(obj, C)
assert isinstance(obj, B)
assert isinstance(obj, A)
assert isinstance(obj, core.Object)
def test_three_level_deep_copy(self) -> None:
A = _make_type(
"InhACopy",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
)
B = _make_type(
"InhBCopy",
[Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=A,
)
C = _make_type(
"InhCCopy",
[Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=B,
)
obj = C(a=1, b=2, c=3)
obj_copy = DeepCopy(obj)
assert not obj.same_as(obj_copy)
assert obj_copy.a == 1
assert obj_copy.b == 2
assert obj_copy.c == 3
obj_copy.c = 99
assert obj.c == 3
# -- Regression: empty intermediate parent must propagate total_size -----
def test_empty_intermediate_parent_offsets(self) -> None:
"""GrandParent(x,y) -> Parent(no fields) -> Child(a,b): no overlap."""
GP = _make_type(
"OffGP",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
P = _make_type("OffP", [], parent=GP)
C = _make_type(
"OffC",
[
Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING),
],
parent=P,
)
obj = C(x=10, y=20, a=30, b=40)
assert obj.x == 10
assert obj.y == 20
assert obj.a == 30
assert obj.b == 40
def test_empty_intermediate_parent_total_size(self) -> None:
"""Parent with no own fields inherits total_size from its parent."""
GP = _make_type(
"OffGP2",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
P = _make_type("OffP2", [], parent=GP)
gp_info = getattr(GP, "__tvm_ffi_type_info__")
p_info = getattr(P, "__tvm_ffi_type_info__")
assert p_info.total_size >= gp_info.total_size
def test_empty_intermediate_child_offsets_non_overlapping(self) -> None:
"""Child field offsets must start at or after Parent.total_size."""
GP = _make_type(
"OffGP3",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
P = _make_type("OffP3", [], parent=GP)
C = _make_type(
"OffC3",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=P,
)
p_info = getattr(P, "__tvm_ffi_type_info__")
c_info = getattr(C, "__tvm_ffi_type_info__")
assert c_info.fields[0].offset >= p_info.total_size
def test_two_empty_intermediate_parents(self) -> None:
"""GP(x) -> Mid1() -> Mid2() -> Leaf(a): offsets propagated through two empty layers."""
GP = _make_type(
"OffGP4",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
Mid1 = _make_type("OffMid1", [], parent=GP)
Mid2 = _make_type("OffMid2", [], parent=Mid1)
Leaf = _make_type(
"OffLeaf",
[Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=Mid2,
)
obj = Leaf(x=1, a=2)
assert obj.x == 1
assert obj.a == 2
gp_info = getattr(GP, "__tvm_ffi_type_info__")
leaf_info = getattr(Leaf, "__tvm_ffi_type_info__")
assert leaf_info.fields[0].offset >= gp_info.total_size
def test_empty_intermediate_mutation_no_aliasing(self) -> None:
"""Mutating Child.a must not corrupt GrandParent.x through an empty parent."""
GP = _make_type(
"OffGP5",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
P = _make_type("OffP5", [], parent=GP)
C = _make_type(
"OffC5",
[
Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING),
],
parent=P,
)
obj = C(x=10, y=20, a=30, b=40)
obj.a = 99
assert obj.x == 10
assert obj.y == 20
obj.x = 77
assert obj.a == 99
assert obj.b == 40
def test_empty_intermediate_py_class_decorator(self) -> None:
"""Same bug via @py_class (the high-level API)."""
@py_class(_unique_key("OffDecGP"))
class GP(Object):
x: int = 0
y: int = 0
@py_class(_unique_key("OffDecP"))
class P(GP):
pass
@py_class(_unique_key("OffDecC"))
class C(P):
a: int = 0
b: int = 0
obj = C(x=10, y=20, a=30, b=40)
assert obj.x == 10
assert obj.y == 20
assert obj.a == 30
assert obj.b == 40
# ###########################################################################
# 15. Mutual / Self References
# ###########################################################################
class TestMutualReferences:
"""Low-level mutual and self-referential type fields via two-phase registration."""
def _register_bare(self, name: str) -> tuple[type, core.TypeInfo]:
"""Register a type with no fields (phase 1 of two-phase)."""
parent_info = core._type_cls_to_type_info(core.Object)
assert parent_info is not None
cls = type(name, (core.Object,), {"__slots__": ()})
info = core._register_py_class(parent_info, _unique_key_ff(name), cls)
return cls, info
def _finalize(self, cls: type, info: core.TypeInfo, fields: List[Field]) -> None:
"""Register fields and install class attrs (phase 2 of two-phase)."""
info._register_fields(fields)
setattr(cls, "__tvm_ffi_type_info__", info)
_add_class_attrs(cls, info)
_install_dataclass_dunders(
cls,
init=True,
repr=True,
eq=False,
order=False,
unsafe_hash=False,
)
def test_mutual_references(self) -> None:
"""Foo has Optional[Bar], Bar has Optional[Foo]."""
Foo, foo_info = self._register_bare("MutFoo")
Bar, bar_info = self._register_bare("MutBar")
foo_schema = TypeSchema(foo_info.type_key, origin_type_index=foo_info.type_index)
bar_schema = TypeSchema(bar_info.type_key, origin_type_index=bar_info.type_index)
self._finalize(
Foo,
foo_info,
[
Field(name="a", _ty_schema=TypeSchema("str"), default=MISSING),
Field(
name="bar",
_ty_schema=TypeSchema("Optional", (bar_schema,)),
default=None,
),
],
)
self._finalize(
Bar,
bar_info,
[
Field(
name="foo",
_ty_schema=TypeSchema("Optional", (foo_schema,)),
default=None,
),
],
)
foo = Foo(a="hello")
bar = Bar()
bar.foo = foo
foo.bar = bar
assert foo.bar.foo.a == "hello"
def test_self_referential_field(self) -> None:
"""Bar has Optional[Bar] (self-reference)."""
Bar, bar_info = self._register_bare("SelfRef")
bar_schema = TypeSchema(bar_info.type_key, origin_type_index=bar_info.type_index)
self._finalize(
Bar,
bar_info,
[
Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="next",
_ty_schema=TypeSchema("Optional", (bar_schema,)),
default=None,
),
],
)
a = Bar(val=1)
b = Bar(val=2, next=a)
assert b.next.val == 1
assert a.next is None
# Circular: a → b → a
a.next = b
assert a.next.next.val == 1
def test_typed_mutual_ref_rejects_wrong_type(self) -> None:
"""Optional[Foo] field should reject Bar objects."""
Foo, foo_info = self._register_bare("TypedFoo")
Bar, bar_info = self._register_bare("TypedBar")
foo_schema = TypeSchema(foo_info.type_key, origin_type_index=foo_info.type_index)
self._finalize(
Foo,
foo_info,
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
self._finalize(
Bar,
bar_info,
[
Field(
name="foo",
_ty_schema=TypeSchema("Optional", (foo_schema,)),
default=None,
),
],
)
bar = Bar()
bar.foo = Foo(x=1) # OK
assert bar.foo.x == 1
with pytest.raises((TypeError, RuntimeError)):
bar.foo = bar # Bar is not Foo
# ###########################################################################
# 16. Inheritance (native C++ parent)
# ###########################################################################
class TestNativeParentInheritance:
"""Low-level Python child of C++ TestObjectBase: offsets, fields, methods, copy."""
def test_non_overlapping_offsets(self) -> None:
parent_info = core._type_cls_to_type_info(_TestObjectBase)
assert parent_info is not None
Child = _make_type(
"InhNativeChild",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
child_info = getattr(Child, "__tvm_ffi_type_info__")
parent_end = max(f.offset + f.size for f in parent_info.fields)
assert child_info.fields[0].offset >= parent_end
def test_preserves_parent_fields(self) -> None:
Child = _make_type(
"InhNativePreserve",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
assert obj.extra == 7
assert obj.v_i64 == 1
assert obj.v_f64 == 2.0
assert obj.v_str == "x"
def test_mutation_no_aliasing(self) -> None:
Child = _make_type(
"InhNativeMut",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
obj.extra = 33
assert obj.extra == 33
assert obj.v_i64 == 1
assert obj.v_f64 == 2.0
assert obj.v_str == "x"
def test_parent_method_uses_parent_state(self) -> None:
Child = _make_type(
"InhNativeMethod",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
assert obj.add_i64(5) == 6
def test_copy_preserves_all_fields(self) -> None:
Child = _make_type(
"InhNativeCopy",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
obj_copy = copy.copy(obj)
assert obj_copy.extra == 7
assert obj_copy.v_i64 == 1
assert obj_copy.v_f64 == 2.0
assert obj_copy.v_str == "x"
def test_deepcopy_preserves_all_fields(self) -> None:
Child = _make_type(
"InhNativeDeepCopy",
[Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
parent=_TestObjectBase,
)
obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
obj_copy = copy.deepcopy(obj)
assert obj_copy.extra == 7
assert obj_copy.v_i64 == 1
assert obj_copy.v_f64 == 2.0
assert obj_copy.v_str == "x"
# ###########################################################################
# 16. Deep Copy
# ###########################################################################
class TestDeepCopy:
"""Low-level DeepCopy via _make_type: nested ObjectRef, mutation independence."""
def test_deep_copy_basic(self) -> None:
Cls = _make_type(
"DCBasic",
[
Field(
name="x",
_ty_schema=TypeSchema("int"),
default=MISSING,
compare=True,
),
Field(
name="s",
_ty_schema=TypeSchema("str"),
default=MISSING,
compare=True,
),
],
eq=True,
)
obj = Cls(x=42, s="hello")
obj_copy = DeepCopy(obj)
assert not obj.same_as(obj_copy)
assert RecursiveEq(obj, obj_copy)
def test_deep_copy_nested_objectref(self) -> None:
Cls = _make_type(
"DCNested",
[
Field(
name="items",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(items=tvm_ffi.Array([1, 2, 3]))
obj_copy = DeepCopy(obj)
assert not obj.items.same_as(obj_copy.items)
assert len(obj_copy.items) == 3
def test_deep_copy_mutate_independent(self) -> None:
Cls = _make_type(
"DCMut",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=1)
obj_copy = DeepCopy(obj)
obj_copy.x = 99
assert obj.x == 1
def test_python_deepcopy_dunder(self) -> None:
Cls = _make_type(
"DCPython",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=42)
obj_copy = copy.deepcopy(obj)
assert obj_copy.x == 42
assert not obj.same_as(obj_copy)
# ###########################################################################
# 17. Memory / Lifetime
# ###########################################################################
class TestMemoryLifetime:
"""Low-level ref-counting: ObjectRef/Any fields are properly ref-counted."""
def test_objectref_field_kept_alive(self) -> None:
Cls = _make_type(
"MemAlive",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
arr = tvm_ffi.Array([1, 2, 3])
obj = Cls(arr=arr)
del arr
gc.collect()
assert len(obj.arr) == 3
def test_multiple_objects_independent_lifetime(self) -> None:
Cls = _make_type(
"MemIndep",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
shared = tvm_ffi.Array([10, 20])
a = Cls(arr=shared)
b = Cls(arr=shared)
del a
gc.collect()
assert len(b.arr) == 2
assert b.arr[0] == 10
def test_str_field_any_storage(self) -> None:
Cls = _make_type(
"MemStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
assert Cls(s="hi").s == "hi"
long_str = "a" * 500
assert Cls(s=long_str).s == long_str
# ###########################################################################
# 18. Bool Alignment
# ###########################################################################
class TestBoolAlignment:
"""Low-level bool field layout: 1-byte packing, padding, alternating layouts."""
def test_bool_then_int_alignment(self) -> None:
Cls = _make_type(
"BoolAlign",
[
Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
info = getattr(Cls, "__tvm_ffi_type_info__")
assert info.fields[0].offset == 24
assert info.fields[1].offset % 8 == 0
assert info.fields[1].offset >= info.fields[0].offset + 1
def test_bool_then_int_values(self) -> None:
Cls = _make_type(
"BoolAlignVal",
[
Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
obj = Cls(flag=True, val=42)
assert obj.flag is True
assert obj.val == 42
obj.flag = False
assert obj.flag is False
assert obj.val == 42
def test_multiple_bools_packed(self) -> None:
Cls = _make_type(
"MultiBool",
[
Field(name="a", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="c", _ty_schema=TypeSchema("bool"), default=MISSING),
],
)
info = getattr(Cls, "__tvm_ffi_type_info__")
assert [f.offset for f in info.fields] == [24, 25, 26]
obj = Cls(a=True, b=False, c=True)
assert obj.a is True
assert obj.b is False
assert obj.c is True
def test_bool_int_bool_int_alternating(self) -> None:
Cls = _make_type(
"BoolIntBoolInt",
[
Field(name="b1", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="i1", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="b2", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="i2", _ty_schema=TypeSchema("int"), default=MISSING),
],
)
obj = Cls(b1=True, i1=100, b2=False, i2=200)
assert obj.b1 is True
assert obj.i1 == 100
assert obj.b2 is False
assert obj.i2 == 200
# ###########################################################################
# 19. Type Conversion Errors
# ###########################################################################
class TestTypeConversionErrors:
"""Low-level type conversion: wrong-type setter/construction raises."""
def test_set_int_field_to_str_raises(self) -> None:
Cls = _make_type(
"ErrIntStr",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=1)
with pytest.raises((TypeError, RuntimeError)):
obj.x = "not_an_int"
def test_set_str_field_to_int_raises(self) -> None:
Cls = _make_type(
"ErrStrInt",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
obj = Cls(s="hello")
with pytest.raises((TypeError, RuntimeError)):
obj.s = 42
def test_construct_with_wrong_type_raises(self) -> None:
Cls = _make_type(
"ErrInit",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
with pytest.raises((TypeError, RuntimeError)):
Cls(x="bad")
def test_set_wrong_type_preserves_old_value(self) -> None:
"""Failed type-checked mutation preserves old value."""
Cls = _make_type(
"ErrPreserve",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=42)
with pytest.raises((TypeError, RuntimeError)):
obj.x = "bad_value"
assert obj.x == 42
def test_type_schema_convert_raises_directly(self) -> None:
"""TypeSchema.convert raises TypeError for incompatible values."""
ts = TypeSchema("int")
assert _to_py_class_value(ts.convert(42)) == 42
with pytest.raises(TypeError):
ts.convert("not_an_int")
def test_set_non_optional_object_field_to_none_raises(self) -> None:
"""A non-Optional Object field must reject None."""
Cls = _make_type(
"ErrObjNone",
[
Field(
name="child",
_ty_schema=TypeSchema("Object"),
default=MISSING,
),
],
)
obj = Cls(child=tvm_ffi.Array([1]))
with pytest.raises((TypeError, RuntimeError)):
obj.child = None
def test_construct_non_optional_object_field_with_none_raises(self) -> None:
"""Constructing with None for a non-Optional Object field must fail."""
Cls = _make_type(
"ErrObjNoneInit",
[
Field(
name="child",
_ty_schema=TypeSchema("Object"),
default=MISSING,
),
],
)
with pytest.raises((TypeError, RuntimeError)):
Cls(child=None)
def test_optional_object_field_accepts_none(self) -> None:
"""An Optional[Object] field should accept None."""
Cls = _make_type(
"OptObjNone",
[
Field(
name="child",
_ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
default=None,
),
],
)
obj = Cls()
assert obj.child is None
obj.child = tvm_ffi.Array([1, 2])
assert len(obj.child) == 2
obj.child = None
assert obj.child is None
def test_set_bool_field_to_str_raises(self) -> None:
Cls = _make_type(
"ErrBoolStr",
[Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
obj = Cls(b=True)
with pytest.raises((TypeError, RuntimeError)):
obj.b = "not_a_bool"
def test_set_array_field_to_int_raises(self) -> None:
Cls = _make_type(
"ErrArrInt",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(arr=[1])
with pytest.raises((TypeError, RuntimeError)):
obj.arr = 42
def test_construct_multiple_wrong_types_first_caught(self) -> None:
"""When the first field has a wrong type, the error is caught."""
Cls = _make_type(
"ErrMulti",
[
Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="y", _ty_schema=TypeSchema("str"), default=MISSING),
],
)
with pytest.raises((TypeError, RuntimeError)):
Cls(x="bad", y="ok")
def test_set_optional_to_wrong_inner_type_raises(self) -> None:
Cls = _make_type(
"ErrOptWrong",
[
Field(
name="x",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
],
)
obj = Cls()
with pytest.raises((TypeError, RuntimeError)):
obj.x = "not_an_int"
# ###########################################################################
# 20. Setter / Getter Corner Cases
# ###########################################################################
class TestSetterGetterCornerCases:
"""Low-level setter/getter corner cases: conversions, nesting, edge values."""
# --- Bool / int coercion ---
def test_bool_field_accepts_true_false(self) -> None:
Cls = _make_type(
"SGBool",
[Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING)],
)
obj = Cls(b=True)
assert obj.b is True
obj.b = False
assert obj.b is False
def test_int_field_accepts_bool(self) -> None:
"""Python bool is a subclass of int — FFI should accept it."""
Cls = _make_type(
"SGIntBool",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=True)
assert obj.x == 1
obj.x = False
assert obj.x == 0
# --- Float edge values ---
def test_float_field_inf_nan(self) -> None:
Cls = _make_type(
"SGFloatEdge",
[Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING)],
)
obj = Cls(f=float("inf"))
assert math.isinf(obj.f)
obj.f = float("-inf")
assert math.isinf(obj.f) and obj.f < 0
obj.f = float("nan")
assert math.isnan(obj.f)
def test_float_field_accepts_int(self) -> None:
Cls = _make_type(
"SGFloatInt",
[Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING)],
)
obj = Cls(f=42)
assert obj.f == pytest.approx(42.0)
# --- String edge values ---
def test_str_field_unicode(self) -> None:
Cls = _make_type(
"SGStrUni",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
obj = Cls(s="日本語テスト 🎉")
assert obj.s == "日本語テスト 🎉"
def test_str_field_null_bytes(self) -> None:
Cls = _make_type(
"SGStrNull",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
s = "hello\x00world"
obj = Cls(s=s)
assert obj.s == s
# --- Multiple mutations ---
def test_repeated_mutation_same_field(self) -> None:
Cls = _make_type(
"SGRepeat",
[Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
)
obj = Cls(x=0)
for i in range(100):
obj.x = i
assert obj.x == 99
def test_repeated_str_mutation(self) -> None:
"""Stress: repeated str assignment should not leak."""
Cls = _make_type(
"SGRepeatStr",
[Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
)
obj = Cls(s="init")
for i in range(100):
obj.s = f"value_{i}"
assert obj.s == "value_99"
def test_repeated_objectref_mutation(self) -> None:
"""Stress: repeated ObjectRef assignment should properly DecRef old values."""
Cls = _make_type(
"SGRepeatArr",
[
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
],
)
obj = Cls(arr=[0])
for i in range(50):
obj.arr = tvm_ffi.Array([i])
assert obj.arr[0] == 49
# --- Nested object fields ---
def test_nested_two_levels(self) -> None:
Inner = _make_type(
"SGInner",
[Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING)],
)
inner_info = getattr(Inner, "__tvm_ffi_type_info__")
inner_schema = TypeSchema(inner_info.type_key, origin_type_index=inner_info.type_index)
Outer = _make_type(
"SGOuter",
[Field(name="child", _ty_schema=inner_schema, default=MISSING)],
)
obj = Outer(child=Inner(val=42))
assert obj.child.val == 42
# Mutate inner through outer
new_inner = Inner(val=99)
obj.child = new_inner
assert obj.child.val == 99
def test_self_referential_optional_field(self) -> None:
"""A type with an Optional[Self] field (stored as Any)."""
Cls = _make_type(
"SGSelfRef",
[
Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
Field(
name="next",
_ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
default=None,
),
],
)
a = Cls(val=1)
b = Cls(val=2, next=a)
assert b.val == 2
assert b.next.val == 1
assert a.next is None
# --- Default factory edge cases ---
def test_default_factory_called_each_time(self) -> None:
call_count = [0]
def factory() -> int:
call_count[0] += 1
return call_count[0]
Cls = _make_type(
"SGFactoryCount",
[Field(name="x", _ty_schema=TypeSchema("int"), default_factory=factory)],
)
a = Cls()
b = Cls()
c = Cls()
assert a.x == 1
assert b.x == 2
assert c.x == 3
# --- Mixed types in one class ---
def test_all_pod_plus_objectref_plus_optional(self) -> None:
Cls = _make_type(
"SGKitchenSink",
[
Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING),
Field(
name="arr",
_ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
default=MISSING,
),
Field(
name="opt",
_ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
default=None,
),
],
)
obj = Cls(i=1, f=2.0, b=True, s="hi", arr=[10, 20])
assert obj.i == 1
assert obj.f == pytest.approx(2.0)
assert obj.b is True
assert obj.s == "hi"
assert len(obj.arr) == 2
assert obj.opt is None
# Mutate all fields
obj.i = -1
obj.f = -2.0
obj.b = False
obj.s = "bye"
obj.arr = tvm_ffi.Array([30])
obj.opt = 42
assert obj.i == -1
assert obj.f == pytest.approx(-2.0)
assert obj.b is False
assert obj.s == "bye"
assert len(obj.arr) == 1
assert obj.opt == 42
# ###########################################################################
# 21. FFI Global Function Existence
# ###########################################################################
class TestFFIGlobalFunctions:
"""Low-level FFI global function registration checks."""
def test_py_class_register_type_attr_columns_exists(self) -> None:
assert (
tvm_ffi.get_global_func("ffi._PyClassRegisterTypeAttrColumns", allow_missing=True)
is not None
)
def test_get_field_getter_exists(self) -> None:
assert tvm_ffi.get_global_func("ffi.MakeFieldGetter", allow_missing=True) is not None
def test_make_field_setter_exists(self) -> None:
assert tvm_ffi.get_global_func("ffi.MakeFieldSetter", allow_missing=True) is not None
def test_make_new_removed(self) -> None:
assert tvm_ffi.get_global_func("ffi.MakeNew", allow_missing=True) is None
# ###########################################################################
# 22. Container field annotations
# ###########################################################################
class TestContainerFieldAnnotations:
"""Container field annotations: List[T], Dict[K,V], nested."""
def test_list_int_field(self) -> None:
@py_class(_unique_key("ListInt"))
class ListInt(Object):
items: List[int]
obj = ListInt(items=[1, 2, 3])
assert len(obj.items) == 3
assert obj.items[0] == 1
assert obj.items[2] == 3
def test_list_int_from_tuple(self) -> None:
@py_class(_unique_key("ListIntTup"))
class ListIntTup(Object):
items: List[int]
obj = ListIntTup(items=(10, 20, 30)) # ty:ignore[invalid-argument-type]
assert len(obj.items) == 3
assert obj.items[1] == 20
def test_dict_str_int_field(self) -> None:
@py_class(_unique_key("DictStrInt"))
class DictStrInt(Object):
mapping: Dict[str, int]
obj = DictStrInt(mapping={"a": 1, "b": 2})
assert len(obj.mapping) == 2
assert obj.mapping["a"] == 1
assert obj.mapping["b"] == 2
def test_list_list_int_field(self) -> None:
@py_class(_unique_key("ListListInt"))
class ListListInt(Object):
matrix: List[List[int]]
obj = ListListInt(matrix=[[1, 2, 3], [4, 5, 6]])
assert len(obj.matrix) == 2
assert len(obj.matrix[0]) == 3
assert obj.matrix[0][0] == 1
assert obj.matrix[1][2] == 6
def test_dict_str_list_int_field(self) -> None:
@py_class(_unique_key("DictStrListInt"))
class DictStrListInt(Object):
data: Dict[str, List[int]]
obj = DictStrListInt(data={"x": [1, 2, 3], "y": [4, 5, 6]})
assert len(obj.data) == 2
assert tuple(obj.data["x"]) == (1, 2, 3)
assert tuple(obj.data["y"]) == (4, 5, 6)
def test_container_field_set(self) -> None:
@py_class(_unique_key("ContSet"))
class ContSet(Object):
items: List[int]
obj = ContSet(items=[1, 2])
assert tuple(obj.items) == (1, 2)
obj.items = [3, 4, 5]
assert len(obj.items) == 3
assert obj.items[0] == 3
def test_dict_field_set(self) -> None:
@py_class(_unique_key("DictSet"))
class DictSet(Object):
mapping: Dict[str, int]
obj = DictSet(mapping={"a": 1})
obj.mapping = {"b": 2, "c": 3}
assert len(obj.mapping) == 2
assert obj.mapping["b"] == 2
def test_container_shared_reference(self) -> None:
@py_class(_unique_key("ContShare"))
class ContShare(Object):
a: List[int]
b: List[int]
obj = ContShare(a=[1, 2], b=[1, 2])
assert tuple(obj.a) == tuple(obj.b)
def test_untyped_list_field(self) -> None:
@py_class(_unique_key("UList"))
class UList(Object):
items: list
obj = UList(items=[1, "two", 3.0])
assert len(obj.items) == 3
assert obj.items[0] == 1
assert obj.items[1] == "two"
def test_untyped_dict_field(self) -> None:
@py_class(_unique_key("UDict"))
class UDict(Object):
data: dict
obj = UDict(data={"a": 1, "b": "two"})
assert len(obj.data) == 2
# ###########################################################################
# 23. Optional container fields
# ###########################################################################
class TestOptionalContainerFields:
"""Optional[List[T]], Optional[Dict[K,V]] via @py_class."""
@requires_py310
def test_optional_list_int(self) -> None:
@py_class(_unique_key("OptListInt"))
class OptListInt(Object):
items: Optional[List[int]]
obj = OptListInt(items=[1, 2, 3])
assert len(obj.items) == 3 # ty:ignore[invalid-argument-type]
obj.items = None
assert obj.items is None
obj.items = [4, 5]
assert len(obj.items) == 2
@requires_py310
def test_optional_dict_str_int(self) -> None:
@py_class(_unique_key("OptDictStrInt"))
class OptDictStrInt(Object):
data: Optional[Dict[str, int]]
obj = OptDictStrInt(data={"a": 1})
assert obj.data["a"] == 1 # ty:ignore[not-subscriptable]
obj.data = None
assert obj.data is None
obj.data = {"b": 2}
assert obj.data["b"] == 2
@requires_py310
def test_optional_list_list_int(self) -> None:
@py_class(_unique_key("OptLLI"))
class OptLLI(Object):
matrix: Optional[List[List[int]]]
obj = OptLLI(matrix=[[1, 2], [3, 4]])
assert obj.matrix[0][0] == 1 # ty:ignore[not-subscriptable]
obj.matrix = None
assert obj.matrix is None
@requires_py310
def test_optional_dict_str_list_int(self) -> None:
@py_class(_unique_key("OptDSLI"))
class OptDSLI(Object):
data: Optional[Dict[str, List[int]]]
obj = OptDSLI(data={"x": [1, 2, 3]})
assert tuple(obj.data["x"]) == (1, 2, 3) # ty:ignore[not-subscriptable]
obj.data = None
assert obj.data is None
def test_optional_list_with_typing_optional(self) -> None:
@py_class(_unique_key("OptListTyping"))
class OptListTyping(Object):
items: Optional[List[int]]
obj = OptListTyping(items=[1, 2, 3])
assert len(obj.items) == 3 # ty:ignore[invalid-argument-type]
obj.items = None
assert obj.items is None
def test_optional_dict_with_typing_optional(self) -> None:
@py_class(_unique_key("OptDictTyping"))
class OptDictTyping(Object):
data: Optional[Dict[str, int]]
obj = OptDictTyping(data={"a": 1})
assert obj.data["a"] == 1 # ty:ignore[not-subscriptable]
obj.data = None
assert obj.data is None
# ###########################################################################
# 24. Callable / Function fields
# ###########################################################################
class TestFunctionField:
"""Function/Callable field via @py_class decorator."""
def test_function_field(self) -> None:
@py_class(_unique_key("FuncFld"))
class FuncFld(Object):
func: tvm_ffi.Function
fn = tvm_ffi.convert(lambda x: x + 1)
obj = FuncFld(func=fn)
assert obj.func(1) == 2
def test_function_field_set(self) -> None:
@py_class(_unique_key("FuncSet"))
class FuncSet(Object):
func: tvm_ffi.Function
fn1 = tvm_ffi.convert(lambda x: x + 1)
fn2 = tvm_ffi.convert(lambda x: x + 2)
obj = FuncSet(func=fn1)
assert obj.func(1) == 2
obj.func = fn2
assert obj.func(1) == 3
@requires_py310
def test_optional_function_field(self) -> None:
@py_class(_unique_key("OptFunc"))
class OptFunc(Object):
func: Optional[tvm_ffi.Function]
obj = OptFunc(func=None)
assert obj.func is None
obj.func = tvm_ffi.convert(lambda x: x * 2)
assert obj.func(3) == 6
obj.func = None
assert obj.func is None
# ###########################################################################
# 25. Any-typed fields (decorator level)
# ###########################################################################
class TestAnyFieldDecorator:
"""Any-typed field via @py_class decorator."""
def test_any_holds_int(self) -> None:
@py_class(_unique_key("AnyI"))
class AnyI(Object):
val: Any
assert AnyI(val=42).val == 42
def test_any_holds_str(self) -> None:
@py_class(_unique_key("AnyS"))
class AnyS(Object):
val: Any
assert AnyS(val="hello").val == "hello"
def test_any_holds_none(self) -> None:
@py_class(_unique_key("AnyN"))
class AnyN(Object):
val: Any = None
assert AnyN().val is None
def test_any_holds_list(self) -> None:
@py_class(_unique_key("AnyL"))
class AnyL(Object):
val: Any
assert len(AnyL(val=[1, 2, 3]).val) == 3
def test_any_type_change(self) -> None:
@py_class(_unique_key("AnyChg"))
class AnyChg(Object):
val: Any = None
obj = AnyChg()
assert obj.val is None
obj.val = 42
assert obj.val == 42
obj.val = "hello"
assert obj.val == "hello"
obj.val = tvm_ffi.Array([1, 2])
assert len(obj.val) == 2
obj.val = None
assert obj.val is None
# ###########################################################################
# 26. Post-init field mutation
# ###########################################################################
class TestPostInitMutation:
"""__post_init__ that mutates field values."""
def test_post_init_mutates_str(self) -> None:
@py_class(_unique_key("PostMut"))
class PostMut(Object):
a: int
b: str
def __post_init__(self) -> None:
self.b = self.b.upper()
obj = PostMut(a=1, b="hello")
assert obj.a == 1
assert obj.b == "HELLO"
def test_post_init_computes_derived(self) -> None:
@py_class(_unique_key("PostDeriv"))
class PostDeriv(Object):
x: int
doubled: int = 0
def __post_init__(self) -> None:
self.doubled = self.x * 2
assert PostDeriv(x=5).doubled == 10
# ###########################################################################
# 27. Custom __init__ with init=False
# ###########################################################################
class TestCustomInitFalse:
"""Custom __init__ with init=False and reordered parameters."""
def test_custom_init_reordered_params(self) -> None:
@py_class(_unique_key("CustomOrd"), init=False)
class CustomOrd(Object):
a: int
b: float
c: str
d: bool
def __init__(self, b: float, c: str, a: int, d: bool) -> None:
self.a = a
self.b = b
self.c = c
self.d = d
obj = CustomOrd(b=2.0, c="3", a=1, d=True)
assert obj.a == 1
assert obj.b == 2.0
assert obj.c == "3"
assert obj.d is True
def test_custom_init_keyword_only(self) -> None:
@py_class(_unique_key("CustomKW"), init=False)
class CustomKW(Object):
a: int
b: str
def __init__(self, *, b: str, a: int) -> None:
self.a = a
self.b = b
obj = CustomKW(a=1, b="hello")
assert obj.a == 1
assert obj.b == "hello"
# ###########################################################################
# 28. Inheritance with defaults and containers
# ###########################################################################
class TestInheritanceWithDefaults:
"""Inheritance with default values and container fields."""
def test_base_with_default_factory(self) -> None:
@py_class(_unique_key("BaseDef"))
class BaseDef(Object):
a: int
b: List[int] = field(default_factory=list)
obj = BaseDef(a=42)
assert obj.a == 42
assert len(obj.b) == 0
def test_derived_adds_optional_fields(self) -> None:
@py_class(_unique_key("BaseD"))
class BaseD(Object):
a: int
b: List[int] = field(default_factory=list)
@py_class(_unique_key("DerivedD"))
class DerivedD(BaseD):
c: Optional[int] = None
d: Optional[str] = "default"
obj = DerivedD(a=12)
assert obj.a == 12
assert len(obj.b) == 0
assert obj.c is None
assert obj.d == "default"
def test_derived_interleaved_required_optional(self) -> None:
@py_class(_unique_key("BaseIL"))
class BaseIL(Object):
a: int
b: List[int] = field(default_factory=list)
@py_class(_unique_key("DerivedIL"))
class DerivedIL(BaseIL):
c: int
d: Optional[str] = "default"
obj = DerivedIL(a=1, c=2)
assert obj.a == 1
assert len(obj.b) == 0
assert obj.c == 2
assert obj.d == "default"
def test_three_level_with_defaults(self) -> None:
@py_class(_unique_key("L1D"))
class L1D(Object):
a: int
@py_class(_unique_key("L2D"))
class L2D(L1D):
b: Optional[int] = None
c: Optional[str] = "hello"
@py_class(_unique_key("L3D"))
class L3D(L2D):
d: str
obj = L3D(a=1, d="world")
assert obj.a == 1
assert obj.b is None
assert obj.c == "hello"
assert obj.d == "world"
def test_derived_with_container_init(self) -> None:
@py_class(_unique_key("BaseC"))
class BaseC(Object):
items: List[int]
@py_class(_unique_key("DerivedC"))
class DerivedC(BaseC):
name: str
obj = DerivedC(items=[1, 2, 3], name="test")
assert len(obj.items) == 3
assert obj.name == "test"
# ###########################################################################
# 29. Decorator-level type validation
# ###########################################################################
class TestFieldTypeValidation:
"""Type validation on set for @py_class fields."""
def test_set_int_to_str_raises(self) -> None:
@py_class(_unique_key("ValInt"))
class ValInt(Object):
x: int
obj = ValInt(x=1)
with pytest.raises((TypeError, RuntimeError)):
obj.x = "not_an_int" # ty:ignore[invalid-assignment]
def test_set_str_to_int_raises(self) -> None:
@py_class(_unique_key("ValStr"))
class ValStr(Object):
x: str
obj = ValStr(x="hello")
with pytest.raises((TypeError, RuntimeError)):
obj.x = 42 # ty:ignore[invalid-assignment]
def test_set_bool_to_str_raises(self) -> None:
@py_class(_unique_key("ValBool"))
class ValBool(Object):
x: bool
obj = ValBool(x=True)
with pytest.raises((TypeError, RuntimeError)):
obj.x = "not_a_bool" # ty:ignore[invalid-assignment]
def test_set_list_to_wrong_type_raises(self) -> None:
@py_class(_unique_key("ValList"))
class ValList(Object):
items: List[int]
obj = ValList(items=[1, 2])
with pytest.raises((TypeError, RuntimeError)):
obj.items = "not_a_list" # ty:ignore[invalid-assignment]
def test_set_dict_to_wrong_type_raises(self) -> None:
@py_class(_unique_key("ValDict"))
class ValDict(Object):
data: Dict[str, int]
obj = ValDict(data={"a": 1})
with pytest.raises((TypeError, RuntimeError)):
obj.data = "not_a_dict" # ty:ignore[invalid-assignment]
# ###########################################################################
# 30. Three-level inheritance with containers
# ###########################################################################
class TestDerivedDerivedContainers:
"""Three-level inheritance with container fields and init reordering."""
def test_three_level_with_container_and_defaults(self) -> None:
@py_class(_unique_key("DD_L1"))
class L1(Object):
a: int
b: List[int] = field(default_factory=list)
@py_class(_unique_key("DD_L2"))
class L2(L1):
c: Optional[int] = None
d: Optional[str] = "hello"
@py_class(_unique_key("DD_L3"))
class L3(L2):
e: str
obj = L3(a=1, e="world", b=[1, 2])
assert obj.a == 1
assert tuple(obj.b) == (1, 2)
assert obj.c is None
assert obj.d == "hello"
assert obj.e == "world"
def test_three_level_positional_call(self) -> None:
@py_class(_unique_key("DD2_L1"))
class L1(Object):
a: int
@py_class(_unique_key("DD2_L2"))
class L2(L1):
b: List[int] = field(default_factory=list)
@py_class(_unique_key("DD2_L3"))
class L3(L2):
c: str
obj = L3(a=1, c="x")
assert obj.a == 1
assert len(obj.b) == 0
assert obj.c == "x"
# ###########################################################################
# 31. Container field mutation and type rejection
# ###########################################################################
class TestContainerFieldMutation:
"""Container field set, mutation, and type rejection."""
def test_untyped_list_mutation(self) -> None:
obj = _make_multi_type_obj()
assert len(obj.list_any) == 3
assert obj.list_any[0] == 1
obj.list_any = [4, 3.0, "two"]
assert len(obj.list_any) == 3
assert obj.list_any[0] == 4
assert obj.list_any[2] == "two"
def test_untyped_dict_mutation(self) -> None:
obj = _make_multi_type_obj()
assert len(obj.dict_any) == 2
obj.dict_any = {"4": 4, "3": "two", "2": 3.0}
assert len(obj.dict_any) == 3
assert obj.dict_any["4"] == 4
assert obj.dict_any["3"] == "two"
def test_list_any_type_rejection(self) -> None:
@py_class(_unique_key("LAReject"))
class LAReject(Object):
items: List[Any]
obj = LAReject(items=[1, 2])
with pytest.raises((TypeError, RuntimeError)):
obj.items = "wrong" # ty:ignore[invalid-assignment]
def test_list_list_int_type_rejection(self) -> None:
@py_class(_unique_key("LLReject"))
class LLReject(Object):
matrix: List[List[int]]
obj = LLReject(matrix=[[1, 2]])
with pytest.raises((TypeError, RuntimeError)):
obj.matrix = [4, 3, 2, 1] # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.matrix = None # ty:ignore[invalid-assignment]
assert len(obj.matrix) == 1
def test_dict_any_any_type_rejection(self) -> None:
@py_class(_unique_key("DAAReject"))
class DAAReject(Object):
data: Dict[Any, Any]
obj = DAAReject(data={1: 2})
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = None # ty:ignore[invalid-assignment]
def test_dict_str_any_type_rejection(self) -> None:
@py_class(_unique_key("DSAReject"))
class DSAReject(Object):
data: Dict[str, Any]
obj = DSAReject(data={"a": 1})
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = None # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = {4: 4, 3.0: 3} # ty:ignore[invalid-assignment]
def test_dict_str_list_int_type_rejection(self) -> None:
@py_class(_unique_key("DSLReject"))
class DSLReject(Object):
data: Dict[str, List[int]]
obj = DSLReject(data={"a": [1, 2]})
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = None # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = {"a": 1, "b": [2]} # ty:ignore[invalid-assignment]
# ###########################################################################
# 32. Optional field set/unset cycles with type rejection
# ###########################################################################
class TestOptionalFieldCycles:
"""Optional field set → None → set-back cycles with type rejection."""
def test_opt_func_type_rejection(self) -> None:
@py_class(_unique_key("OptFuncR"))
class OptFuncR(Object):
func: Optional[tvm_ffi.Function]
obj = OptFuncR(func=None)
assert obj.func is None
obj.func = tvm_ffi.convert(lambda x: x + 2)
assert obj.func(1) == 3
obj.func = None
assert obj.func is None
with pytest.raises((TypeError, RuntimeError)):
obj.func = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.func = 42 # ty:ignore[invalid-assignment]
def test_opt_ulist_cycle(self) -> None:
@py_class(_unique_key("OptUListC"))
class OptUListC(Object):
items: Optional[list]
obj = OptUListC(items=None)
assert obj.items is None
obj.items = [4, 3.0, "two"]
assert len(obj.items) == 3
assert obj.items[0] == 4
with pytest.raises((TypeError, RuntimeError)):
obj.items = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.items = 42 # ty:ignore[invalid-assignment]
obj.items = None
assert obj.items is None
def test_opt_udict_cycle(self) -> None:
@py_class(_unique_key("OptUDictC"))
class OptUDictC(Object):
data: Optional[dict]
obj = OptUDictC(data=None)
assert obj.data is None
obj.data = {"4": 4, "3": "two"}
assert len(obj.data) == 2
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
obj.data = None
assert obj.data is None
def test_opt_list_any_cycle(self) -> None:
@py_class(_unique_key("OptLAC"))
class OptLAC(Object):
items: Optional[List[Any]]
obj = OptLAC(items=[1, 2.0, "three"])
assert len(obj.items) == 3 # ty:ignore[invalid-argument-type]
obj.items = [4, 3.0, "two"]
assert obj.items[0] == 4
with pytest.raises((TypeError, RuntimeError)):
obj.items = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.items = 42 # ty:ignore[invalid-assignment]
obj.items = None
assert obj.items is None
def test_opt_list_list_int_cycle(self) -> None:
@py_class(_unique_key("OptLLIC"))
class OptLLIC(Object):
matrix: Optional[List[List[int]]]
obj = OptLLIC(matrix=[[1, 2, 3], [4, 5, 6]])
assert tuple(obj.matrix[0]) == (1, 2, 3) # ty:ignore[not-subscriptable]
obj.matrix = [[4, 3, 2]]
assert len(obj.matrix) == 1
with pytest.raises((TypeError, RuntimeError)):
obj.matrix = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.matrix = [1, 2, 3] # ty:ignore[invalid-assignment]
obj.matrix = None
assert obj.matrix is None
def test_opt_dict_any_any_cycle(self) -> None:
@py_class(_unique_key("OptDAAC"))
class OptDAAC(Object):
data: Optional[Dict[Any, Any]]
obj = OptDAAC(data=None)
assert obj.data is None
obj.data = {4: 4, "three": "two"}
assert len(obj.data) == 2
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
obj.data = None
assert obj.data is None
def test_opt_dict_str_any_cycle(self) -> None:
@py_class(_unique_key("OptDSAC"))
class OptDSAC(Object):
data: Optional[Dict[str, Any]]
obj = OptDSAC(data={"a": 1})
assert obj.data["a"] == 1 # ty:ignore[not-subscriptable]
obj.data = {}
assert len(obj.data) == 0
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
obj.data = None
assert obj.data is None
def test_opt_dict_any_str_cycle(self) -> None:
@py_class(_unique_key("OptDASC"))
class OptDASC(Object):
data: Optional[Dict[Any, str]]
obj = OptDASC(data={1: "a", "two": "b"})
assert obj.data[1] == "a" # ty:ignore[not-subscriptable]
obj.data = {4: "4", "three": "two"}
assert len(obj.data) == 2
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
obj.data = None
assert obj.data is None
def test_opt_dict_str_list_int_cycle(self) -> None:
@py_class(_unique_key("OptDSLIC"))
class OptDSLIC(Object):
data: Optional[Dict[str, List[int]]]
obj = OptDSLIC(data={"1": [1, 2, 3], "2": [4, 5, 6]})
assert tuple(obj.data["1"]) == (1, 2, 3) # ty:ignore[not-subscriptable]
obj.data = {"a": [7, 8]}
assert tuple(obj.data["a"]) == (7, 8)
with pytest.raises((TypeError, RuntimeError)):
obj.data = "wrong" # ty:ignore[invalid-assignment]
with pytest.raises((TypeError, RuntimeError)):
obj.data = 42 # ty:ignore[invalid-assignment]
obj.data = None
assert obj.data is None
# ###########################################################################
# 33. Multi-type field class
# ###########################################################################
@py_class(_unique_key("MultiType"))
class _PyClassMultiType(Object):
"""@py_class with many field types for cross-cutting field tests."""
bool_: bool
i64: int
f64: float
str_: str
any_val: Any
list_int: List[int]
list_any: list
dict_str_int: Dict[str, int]
dict_any: dict
list_list_int: List[List[int]]
dict_str_list_int: Dict[str, List[int]]
opt_bool: Optional[bool]
opt_int: Optional[int]
opt_float: Optional[float]
opt_str: Optional[str]
opt_list_int: Optional[List[int]]
opt_dict_str_int: Optional[Dict[str, int]]
def _make_multi_type_obj() -> _PyClassMultiType:
return _PyClassMultiType(
bool_=False,
i64=64,
f64=2.5,
str_="world",
any_val="hello",
list_int=[1, 2, 3],
list_any=[1, "two", 3.0],
dict_str_int={"a": 1, "b": 2},
dict_any={"x": 1, "y": "two"},
list_list_int=[[1, 2, 3], [4, 5, 6]],
dict_str_list_int={"p": [1, 2], "q": [3, 4]},
opt_bool=True,
opt_int=-64,
opt_float=None,
opt_str=None,
opt_list_int=[10, 20],
opt_dict_str_int=None,
)
class TestMultiTypeFieldOps:
"""Per-field get/set/validation on a many-field @py_class type."""
def test_bool_field(self) -> None:
obj = _make_multi_type_obj()
assert obj.bool_ is False
obj.bool_ = True
assert obj.bool_ is True
with pytest.raises((TypeError, RuntimeError)):
obj.bool_ = "not_a_bool" # ty:ignore[invalid-assignment]
def test_int_field(self) -> None:
obj = _make_multi_type_obj()
assert obj.i64 == 64
obj.i64 = -128
assert obj.i64 == -128
with pytest.raises((TypeError, RuntimeError)):
obj.i64 = "wrong" # ty:ignore[invalid-assignment]
def test_float_field(self) -> None:
obj = _make_multi_type_obj()
assert abs(obj.f64 - 2.5) < 1e-10
obj.f64 = 5.0
assert abs(obj.f64 - 5.0) < 1e-10
with pytest.raises((TypeError, RuntimeError)):
obj.f64 = "wrong" # ty:ignore[invalid-assignment]
def test_str_field(self) -> None:
obj = _make_multi_type_obj()
assert obj.str_ == "world"
obj.str_ = "hello"
assert obj.str_ == "hello"
def test_any_field(self) -> None:
obj = _make_multi_type_obj()
assert obj.any_val == "hello"
obj.any_val = 42
assert obj.any_val == 42
obj.any_val = [1, 2]
assert len(obj.any_val) == 2
def test_list_int_field(self) -> None:
obj = _make_multi_type_obj()
assert tuple(obj.list_int) == (1, 2, 3)
obj.list_int = [4, 5]
assert len(obj.list_int) == 2
with pytest.raises((TypeError, RuntimeError)):
obj.list_int = "wrong" # ty:ignore[invalid-assignment]
def test_untyped_list_field(self) -> None:
obj = _make_multi_type_obj()
assert len(obj.list_any) == 3
assert obj.list_any[0] == 1
assert obj.list_any[1] == "two"
obj.list_any = [4, 3.0, "new"]
assert len(obj.list_any) == 3
def test_dict_str_int_field(self) -> None:
obj = _make_multi_type_obj()
assert obj.dict_str_int["a"] == 1
obj.dict_str_int = {"c": 3}
assert obj.dict_str_int["c"] == 3
with pytest.raises((TypeError, RuntimeError)):
obj.dict_str_int = "wrong" # ty:ignore[invalid-assignment]
def test_untyped_dict_field(self) -> None:
obj = _make_multi_type_obj()
assert len(obj.dict_any) == 2
obj.dict_any = {"new": 42}
assert obj.dict_any["new"] == 42
def test_nested_list_field(self) -> None:
obj = _make_multi_type_obj()
assert tuple(obj.list_list_int[0]) == (1, 2, 3)
assert tuple(obj.list_list_int[1]) == (4, 5, 6)
def test_nested_dict_field(self) -> None:
obj = _make_multi_type_obj()
assert tuple(obj.dict_str_list_int["p"]) == (1, 2)
assert tuple(obj.dict_str_list_int["q"]) == (3, 4)
def test_optional_bool(self) -> None:
obj = _make_multi_type_obj()
assert obj.opt_bool is True
obj.opt_bool = False
assert obj.opt_bool is False
obj.opt_bool = None
assert obj.opt_bool is None
def test_optional_int(self) -> None:
obj = _make_multi_type_obj()
assert obj.opt_int == -64
obj.opt_int = None
assert obj.opt_int is None
obj.opt_int = 128
assert obj.opt_int == 128
def test_optional_float(self) -> None:
obj = _make_multi_type_obj()
assert obj.opt_float is None
obj.opt_float = 1.5
assert abs(obj.opt_float - 1.5) < 1e-10
obj.opt_float = None
assert obj.opt_float is None
def test_optional_str(self) -> None:
obj = _make_multi_type_obj()
assert obj.opt_str is None
obj.opt_str = "hello"
assert obj.opt_str == "hello"
obj.opt_str = None
assert obj.opt_str is None
def test_optional_list_int(self) -> None:
obj = _make_multi_type_obj()
assert tuple(obj.opt_list_int) == (10, 20) # ty:ignore[invalid-argument-type]
obj.opt_list_int = None
assert obj.opt_list_int is None
obj.opt_list_int = [30]
assert len(obj.opt_list_int) == 1
def test_optional_dict_str_int(self) -> None:
obj = _make_multi_type_obj()
assert obj.opt_dict_str_int is None
obj.opt_dict_str_int = {"z": 99}
assert obj.opt_dict_str_int["z"] == 99
obj.opt_dict_str_int = None
assert obj.opt_dict_str_int is None
class TestMultiTypeCopy:
"""Copy with the comprehensive multi-type class."""
def test_shallow_copy_comprehensive(self) -> None:
obj = _make_multi_type_obj()
obj2 = copy.copy(obj)
assert obj2.bool_ == obj.bool_
assert obj2.i64 == obj.i64
assert obj2.f64 == obj.f64
assert obj2.str_ == obj.str_
assert obj2.any_val == obj.any_val
assert obj.list_int.same_as(obj2.list_int) # ty:ignore[unresolved-attribute]
assert obj.dict_str_int.same_as(obj2.dict_str_int) # ty:ignore[unresolved-attribute]
def test_deep_copy_comprehensive(self) -> None:
obj = _make_multi_type_obj()
obj2 = copy.deepcopy(obj)
assert obj2.bool_ == obj.bool_
assert obj2.i64 == obj.i64
assert not obj.list_int.same_as(obj2.list_int) # ty:ignore[unresolved-attribute]
assert not obj.dict_str_int.same_as(obj2.dict_str_int) # ty:ignore[unresolved-attribute]
assert tuple(obj2.list_int) == (1, 2, 3)
assert obj2.dict_str_int["a"] == 1
# ---------------------------------------------------------------------------
# _collect_py_methods allowlist and method introspection
# ---------------------------------------------------------------------------
class TestPyMethodAllowlist:
"""Only names in ``_FFI_RECOGNIZED_METHODS`` are collected by ``_collect_py_methods``."""
def test_system_methods_not_in_allowlist(self) -> None:
from tvm_ffi.dataclasses.py_class import _collect_py_methods # noqa: PLC0415
@py_class(_unique_key("Allow"))
class Allow(core.Object):
x: int
def __ffi_init__(self, x: int) -> None: # ty: ignore[invalid-method-override]
pass
def __ffi_shallow_copy__(self) -> None:
pass
def __ffi_repr__(self, fn_repr: Any) -> str:
return "repr"
collected = _collect_py_methods(Allow)
assert collected is not None
names = {name for name, _, _ in collected}
assert "__ffi_repr__" in names
assert "__ffi_init__" not in names
assert "__ffi_shallow_copy__" not in names
def test_arbitrary_ffi_dunder_not_collected(self) -> None:
from tvm_ffi.dataclasses.py_class import _collect_py_methods # noqa: PLC0415
@py_class(_unique_key("Arb"))
class Arb(core.Object):
x: int
def __ffi_custom_op__(self, y: int) -> int:
return self.x + y
collected = _collect_py_methods(Arb)
assert collected is None
class TestPyMethodIntrospection:
"""Registered __ffi_* hooks appear in TypeAttrColumn (not TypeMethod)."""
def test_ffi_repr_in_type_attr(self) -> None:
@py_class(_unique_key("IntrRepr"))
class IntrRepr(core.Object):
x: int
def __ffi_repr__(self, fn_repr: Any) -> str:
return "repr"
info = getattr(IntrRepr, "__tvm_ffi_type_info__")
# User-defined hooks are registered as TypeAttrColumn, not TypeMethod
repr_attr = core._lookup_type_attr(info.type_index, "__ffi_repr__")
assert repr_attr is not None
# System-generated hooks are also TypeAttrColumn only
ffi_init = core._lookup_type_attr(info.type_index, "__ffi_init__")
assert ffi_init is not None
ffi_copy = core._lookup_type_attr(info.type_index, "__ffi_shallow_copy__")
assert ffi_copy is not None
# ---------------------------------------------------------------------------
# super().__init__() support for @py_class(init=False) subclasses
# ---------------------------------------------------------------------------
class TestSuperInitPattern:
"""Regression tests for using ``super().__init__()`` + field assignment
in ``@py_class(init=False)`` custom ``__init__`` methods.
Previously, ``super().__init__()`` would dispatch to the parent's
auto-generated ``__init__`` which called ``self.__ffi_init__()`` with
the **child** type's C++ constructor (requiring field arguments that
weren't provided), causing a crash.
"""
def test_basic_super_init_with_field_setters(self) -> None:
"""The original crash scenario: init=False with super().__init__() and field setters."""
@py_class(_unique_key("SIBase"))
class SIBase(Object):
pass
@py_class(_unique_key("SIChild"), init=False)
class SIChild(SIBase):
x: int
y: str
def __init__(self, x: int, y: str) -> None:
super().__init__()
self.x = x
self.y = y
obj = SIChild(42, "hello")
assert obj.x == 42
assert obj.y == "hello"
def test_super_init_deep_hierarchy(self) -> None:
"""super().__init__() through multiple levels of py_class inheritance."""
@py_class(_unique_key("SIDH_Node"))
class Node(Object):
pass
@py_class(_unique_key("SIDH_BaseType"))
class BaseType(Node):
pass
@py_class(_unique_key("SIDH_PtrType"), init=False)
class PtrType(BaseType):
base_type: Any
specifiers: list
use_bracket: bool
def __init__(
self,
base_type: Any,
specifiers: Optional[list] = None,
use_bracket: bool = False,
) -> None:
super().__init__()
self.base_type = base_type
self.specifiers = list(specifiers) if specifiers else []
self.use_bracket = use_bracket
void_p = PtrType("void")
assert void_p.base_type == "void"
assert list(void_p.specifiers) == []
assert void_p.use_bracket is False
int_p = PtrType("int", ["const", "volatile"], True)
assert int_p.base_type == "int"
assert list(int_p.specifiers) == ["const", "volatile"]
assert int_p.use_bracket is True
def test_super_init_with_defaults_from_calloc(self) -> None:
"""Fields not set after super().__init__() should be zero-initialized."""
@py_class(_unique_key("SIDef"))
class SIDefBase(Object):
pass
@py_class(_unique_key("SIDef_Child"), init=False)
class SIDefChild(SIDefBase):
a: int
b: float
c: bool
d: Any
def __init__(self) -> None:
super().__init__()
# Don't set any fields — they should be zero/None from calloc
obj = SIDefChild()
assert obj.a == 0
assert obj.b == 0.0
assert obj.c is False
assert obj.d is None
def test_super_init_intermediate_custom_init(self) -> None:
"""Chained super().__init__() through an intermediate init=False class."""
@py_class(_unique_key("SIChain_A"))
class A(Object):
pass
@py_class(_unique_key("SIChain_B"), init=False)
class B(A):
x: int
def __init__(self, x: int) -> None:
super().__init__()
self.x = x
@py_class(_unique_key("SIChain_C"), init=False)
class C(B):
y: str
def __init__(self, x: int, y: str) -> None:
super().__init__(x)
self.y = y
obj = C(10, "world")
assert obj.x == 10
assert obj.y == "world"
def test_super_init_intermediate_auto_init(self) -> None:
"""py_class init=False: no need to call super().__init__(), just set fields directly."""
@py_class(_unique_key("SIAI_Mid"))
class Mid(Object):
a: int
@py_class(_unique_key("SIAI_Leaf"), init=False)
class Leaf(Mid):
b: str
def __init__(self, a: int, b: str) -> None:
# For py_class, __new__ already allocated via __ffi_new__.
# No need to call super().__init__() — just set fields directly.
self.a = a
self.b = b
obj = Leaf(5, "hi")
assert obj.a == 5
assert obj.b == "hi"
def test_normal_init_unaffected(self) -> None:
"""Normal init=True construction must still work correctly."""
@py_class(_unique_key("SINorm"))
class SINorm(Object):
x: int
y: str
obj = SINorm(1, "a")
assert obj.x == 1
assert obj.y == "a"
def test_non_pyclass_subclass_no_args_errors(self) -> None:
"""A non-py_class subclass calling parent init with no args should still error
for required fields (not silently create an empty object).
"""
@py_class(_unique_key("SINonPC"))
class SINonPC(Object):
x: int
class Plain(SINonPC):
pass
with pytest.raises(TypeError):
Plain() # type: ignore[missing-argument]
def test_super_init_isinstance(self) -> None:
"""Objects created via super().__init__() pattern have correct isinstance."""
@py_class(_unique_key("SIInst_B"))
class Base(Object):
pass
@py_class(_unique_key("SIInst_C"), init=False)
class Child(Base):
val: int
def __init__(self, val: int) -> None:
super().__init__()
self.val = val
obj = Child(99)
assert isinstance(obj, Child)
assert isinstance(obj, Base)
assert isinstance(obj, Object)
def test_super_init_field_overwrite(self) -> None:
"""Fields can be overwritten multiple times after super().__init__()."""
@py_class(_unique_key("SIOverwrite_B"))
class Base(Object):
pass
@py_class(_unique_key("SIOverwrite_C"), init=False)
class Child(Base):
x: int
def __init__(self, x: int) -> None:
super().__init__()
self.x = 0
self.x = x
obj = Child(42)
assert obj.x == 42
def test_super_init_copy_deepcopy(self) -> None:
"""copy/deepcopy work on objects created via the super().__init__() pattern."""
@py_class(_unique_key("SICopyBase"))
class SICopyBase(Object):
pass
@py_class(_unique_key("SICopy"), init=False)
class SICopy(SICopyBase):
x: int
y: str
def __init__(self, x: int, y: str) -> None:
super().__init__()
self.x = x
self.y = y
obj = SICopy(42, "hello")
obj2 = copy.copy(obj)
assert obj2.x == 42
assert obj2.y == "hello"
assert not obj.same_as(obj2)
obj3 = copy.deepcopy(obj)
assert obj3.x == 42
assert obj3.y == "hello"
assert not obj.same_as(obj3)
def test_super_init_direct_from_object(self) -> None:
"""super().__init__() works when inheriting directly from Object (no intermediate)."""
@py_class(_unique_key("SIDirect"), init=False)
class SIDirect(Object):
x: int
y: str
def __init__(self, x: int, y: str) -> None:
super().__init__()
self.x = x
self.y = y
obj = SIDirect(10, "direct")
assert obj.x == 10
assert obj.y == "direct"
assert isinstance(obj, Object)
def test_super_init_direct_from_object_copy(self) -> None:
"""copy/deepcopy work for init=False classes inheriting directly from Object."""
@py_class(_unique_key("SIDirectCopy"), init=False)
class SIDirectCopy(Object):
x: int
y: str
def __init__(self, x: int, y: str) -> None:
super().__init__()
self.x = x
self.y = y
obj = SIDirectCopy(42, "hello")
obj2 = copy.copy(obj)
assert obj2.x == 42
assert obj2.y == "hello"
assert not obj.same_as(obj2)
obj3 = copy.deepcopy(obj)
assert obj3.x == 42
assert obj3.y == "hello"
assert not obj.same_as(obj3)
class TestDtypeDeviceFields:
"""Regression: @py_class should accept tvm_ffi.dtype and tvm_ffi.Device as field types."""
def test_dtype_field(self) -> None:
@py_class(_unique_key("DtypeField"))
class DtypeHolder(Object):
dt: tvm_ffi.dtype
obj = DtypeHolder(dt=tvm_ffi.dtype("float32"))
assert obj.dt == "float32"
assert isinstance(obj.dt, tvm_ffi.dtype)
def test_dtype_field_setter(self) -> None:
@py_class(_unique_key("DtypeFieldSet"))
class DtypeHolder2(Object):
dt: tvm_ffi.dtype
obj = DtypeHolder2(dt=tvm_ffi.dtype("float32"))
obj.dt = tvm_ffi.dtype("int8")
assert obj.dt == "int8"
def test_device_field(self) -> None:
@py_class(_unique_key("DeviceField"))
class DeviceHolder(Object):
dev: tvm_ffi.Device
dev = tvm_ffi.device("cpu", 0)
obj = DeviceHolder(dev=dev)
assert obj.dev == dev
def test_dtype_device_together(self) -> None:
@py_class(_unique_key("DtypeDeviceTogether"))
class DtypeDeviceHolder(Object):
dt: tvm_ffi.dtype
dev: tvm_ffi.Device
name: str
dev = tvm_ffi.device("cpu", 0)
obj = DtypeDeviceHolder(dt=tvm_ffi.dtype("float16"), dev=dev, name="test")
assert obj.dt == "float16"
assert obj.dev == dev
assert obj.name == "test"
def test_optional_dtype_field(self) -> None:
@py_class(_unique_key("OptDtype"))
class OptDtype(Object):
dt: Optional[tvm_ffi.dtype] = None
obj_none = OptDtype()
assert obj_none.dt is None
obj_val = OptDtype(dt=tvm_ffi.dtype("bfloat16"))
assert obj_val.dt == "bfloat16"
def test_optional_device_field(self) -> None:
@py_class(_unique_key("OptDevice"))
class OptDevice(Object):
dev: Optional[tvm_ffi.Device] = None
obj_none = OptDevice()
assert obj_none.dev is None
obj_val = OptDevice(dev=tvm_ffi.device("cpu", 0))
assert obj_val.dev == tvm_ffi.device("cpu", 0)
# ###########################################################################
# kw_only regression tests (py_class via __ffi_init__)
# ###########################################################################
class TestPyClassKwOnlyRegression:
"""Regression tests ensuring kw_only enforcement via C++ __ffi_init__."""
def test_missing_kw_only_error_says_keyword_only(self) -> None:
"""Missing required kw_only field produces 'keyword-only' in the error."""
@py_class(_unique_key("KwOnlyErr"))
class KwOnlyErr(Object):
x: int
_: KW_ONLY
y: int
with pytest.raises(TypeError, match="keyword-only"):
KwOnlyErr(1) # ty: ignore[missing-argument]
def test_missing_positional_error_not_keyword_only(self) -> None:
"""Missing required positional field does NOT say 'keyword-only'."""
@py_class(_unique_key("PosErr"))
class PosErr(Object):
x: int
_: KW_ONLY
y: int
with pytest.raises(TypeError, match="missing required argument") as exc_info:
PosErr(y=1) # ty: ignore[missing-argument]
assert "keyword-only" not in str(exc_info.value)
def test_kw_only_with_default(self) -> None:
"""kw_only field with default can be omitted."""
@py_class(_unique_key("KwOnlyDef"))
class KwOnlyDef(Object):
x: int
_: KW_ONLY
y: int = 42
obj = KwOnlyDef(1) # ty: ignore[missing-argument]
assert obj.x == 1
assert obj.y == 42
def test_kw_only_rejects_positional(self) -> None:
"""kw_only fields cannot be passed positionally."""
@py_class(_unique_key("KwOnlyReject"))
class KwOnlyReject(Object):
x: int
_: KW_ONLY
y: int
with pytest.raises(TypeError):
KwOnlyReject(1, 2) # ty: ignore[missing-argument, invalid-argument-type]
def test_kw_only_all_fields(self) -> None:
"""All fields kw_only via decorator kwarg."""
@py_class(_unique_key("AllKw"), kw_only=True)
class AllKw(Object):
a: int
b: int = 10
obj = AllKw(a=1)
assert obj.a == 1
assert obj.b == 10
with pytest.raises(TypeError):
AllKw(1) # ty: ignore[missing-argument, too-many-positional-arguments]
def test_kw_only_field_override_false(self) -> None:
"""kw_only=False on a field after KW_ONLY sentinel makes it positional."""
@py_class(_unique_key("KwOverride2"))
class KwOverride2(Object):
_: KW_ONLY
a: int
b: int = field(kw_only=False)
obj = KwOverride2(42, a=1) # ty: ignore[missing-argument, invalid-argument-type]
assert obj.b == 42
assert obj.a == 1
def test_decorator_kw_only_with_override(self) -> None:
"""Decorator-level kw_only with field-level override."""
@py_class(_unique_key("DecKwOverride"), kw_only=True)
class DecKwOverride(Object):
a: int = field(kw_only=False)
b: int
obj = DecKwOverride(1, b=2) # ty: ignore[missing-argument, too-many-positional-arguments]
assert obj.a == 1
assert obj.b == 2
def test_kw_only_inheritance(self) -> None:
"""kw_only enforcement works through inheritance."""
@py_class(_unique_key("KwParent"))
class KwParent(Object):
x: int
@py_class(_unique_key("KwChild"))
class KwChild(KwParent):
_: KW_ONLY
y: int
obj = KwChild(1, y=2) # ty: ignore[missing-argument]
assert obj.x == 1
assert obj.y == 2
with pytest.raises(TypeError):
KwChild(1, 2) # ty: ignore[missing-argument, invalid-argument-type]
with pytest.raises(TypeError, match="keyword-only"):
KwChild(1) # ty: ignore[missing-argument]
# ###########################################################################
# No-leak / no-spurious-allocation tests for py_class
# ###########################################################################
class TestPyClassNoLeak:
"""Verify that the removal of custom __new__ eliminates the memory leak.
The original bug: ``@py_class`` installed a custom ``__new__`` that called
``__ffi_new__`` to pre-allocate a C++ object. When a py_class object was
returned from C++ through ``make_ret_object``, ``cls.__new__(cls)``
triggered that custom ``__new__``, allocating a spurious C++ object whose
refcount was never decremented.
"""
def test_no_custom_new_on_py_class(self) -> None:
"""py_class must NOT install a custom __new__."""
@py_class(_unique_key("NoNew"))
class NoNew(Object):
x: int
assert "__new__" not in NoNew.__dict__
def test_no_custom_new_with_user_init(self) -> None:
"""py_class with user-defined __init__ must NOT install a custom __new__."""
@py_class(_unique_key("NoNewUI"), init=False)
class NoNewUI(Object):
x: int
def __init__(self, x: int) -> None:
self.x = x
assert "__new__" not in NoNewUI.__dict__
def test_make_ret_no_spurious_alloc(self) -> None:
"""Objects returned from C++ (via DeepCopy) must not trigger spurious allocation."""
@py_class(_unique_key("RetTest"))
class RetTest(Object):
x: int
obj = RetTest(42)
# DeepCopy returns through make_ret_object
obj2 = DeepCopy(obj)
assert obj2.x == 42
assert not obj.same_as(obj2)
def test_repeated_roundtrip_no_leak(self) -> None:
"""Repeated construct + DeepCopy cycles must not leak."""
@py_class(_unique_key("RoundTrip"))
class RoundTrip(Object):
x: int
y: str = "default"
gc.collect()
for i in range(1000):
o = RoundTrip(i, str(i))
o2 = DeepCopy(o)
del o, o2
gc.collect()
def test_user_init_wraps_metadata(self) -> None:
"""Wrapped user __init__ preserves docstring and __wrapped__."""
@py_class(_unique_key("WrapMeta"), init=False)
class WrapMeta(Object):
x: int
def __init__(self, x: int) -> None:
"""Initialize WrapMeta."""
self.x = x
assert WrapMeta.__init__.__doc__ == "Initialize WrapMeta."
assert hasattr(WrapMeta.__init__, "__wrapped__")
def test_chandle_guard_skips_on_make_ret(self) -> None:
"""Auto-generated __init__ with chandle guard: make_ret never calls __init__."""
@py_class(_unique_key("ChandleGuard"))
class ChandleGuard(Object):
x: int
obj = ChandleGuard(7)
obj2 = DeepCopy(obj)
# If __init__ were called by make_ret, it would fail (no args)
# or overwrite fields. Verify the value survived intact.
assert obj2.x == 7
def test_super_init_noop_after_ffi_init(self) -> None:
"""super().__init__() is a no-op when chandle is already set."""
@py_class(_unique_key("SuperBase"))
class SuperBase(Object):
pass
call_log: list[str] = []
@py_class(_unique_key("SuperChild"), init=False)
class SuperChild(SuperBase):
x: int
def __init__(self, x: int) -> None:
call_log.append("before_super")
super().__init__()
call_log.append("after_super")
self.x = x
obj = SuperChild(42)
assert obj.x == 42
assert call_log == ["before_super", "after_super"]
def test_user_init_mismatched_signature(self) -> None:
"""User __init__ whose args don't match field layout still works."""
@py_class(_unique_key("Mismatch"), init=False)
class Mismatch(Object):
value: int
ref: Optional[Object]
def __init__(self, val: int) -> None:
self.value = val
self.ref = None
obj = Mismatch(99)
assert obj.value == 99
assert obj.ref is None