blob: 639c1803a9286e21d1bae28941495b032a52096f [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.
"""Comprehensive tests for reflection-driven auto-generated ``__ffi_init__``.
This file exercises:
1. metadata emitted by C++ for auto-init traits
2. Python ``__init__`` signature generation
3. constructor behavior across positional/kw-only/default/init=False combinations
4. low-level KWARGS protocol via TypeAttrColumn __ffi_init__ lookup
5. inheritance behavior for auto-generated init
6. copy/deepcopy/replace interplay with auto-init objects
7. re-initialization, isinstance checks, and instance isolation
"""
# ruff: noqa: D102
from __future__ import annotations
import copy
import inspect
from typing import Any
import pytest
from tvm_ffi import core
from tvm_ffi.dataclasses import py_class
from tvm_ffi.testing import (
TestCompare,
TestHash,
TestIntPair,
_TestCxxAutoInit,
_TestCxxAutoInitAllInitOff,
_TestCxxAutoInitChild,
_TestCxxAutoInitKwOnlyDefaults,
_TestCxxAutoInitParent,
_TestCxxAutoInitSimple,
_TestCxxClassDerived,
_TestCxxClassDerivedDerived,
_TestCxxNoAutoInit,
)
from tvm_ffi.testing.testing import requires_py313
def _ffi_init(obj: Any, *args: Any) -> None:
"""Look up __ffi_init__ from the TypeAttrColumn and call it via __init_handle_by_constructor__."""
type_index = type(obj).__tvm_ffi_type_info__.type_index
ffi_init = core._lookup_type_attr(type_index, "__ffi_init__")
assert ffi_init is not None, f"No __ffi_init__ TypeAttrColumn entry for {type(obj)}"
obj.__init_handle_by_constructor__(ffi_init, *args)
def _field_map(type_cls: type) -> dict[str, Any]:
return {field.name: field for field in getattr(type_cls, "__tvm_ffi_type_info__").fields}
def _method_metadata(type_cls: type, method_name: str) -> dict[str, Any]:
type_info = getattr(type_cls, "__tvm_ffi_type_info__")
for method in type_info.methods:
if method.name == method_name:
return method.metadata
raise AssertionError(f"Cannot find method metadata: {type_cls.__name__}.{method_name}")
class TestAutoInitMetadata:
def test_auto_init_method_marked(self) -> None:
type_info = getattr(_TestCxxAutoInit, "__tvm_ffi_type_info__")
ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
assert ffi_init is not None
# Auto-generated init is only in TypeAttrColumn, not in the method list
assert not any(m.name == "__ffi_init__" for m in type_info.methods)
def test_field_bitmask_init_kw_only_and_defaults(self) -> None:
fields = _field_map(_TestCxxAutoInit)
assert fields["a"].c_init is True
assert fields["b"].c_init is False
assert fields["b"].c_has_default is True
assert fields["c"].c_kw_only is True
assert fields["c"].c_init is True
assert fields["d"].c_has_default is True
def test_all_init_off_field_bitmask(self) -> None:
fields = _field_map(_TestCxxAutoInitAllInitOff)
assert fields["x"].c_init is False
assert fields["x"].c_has_default is True
assert fields["y"].c_init is False
assert fields["y"].c_has_default is True
assert fields["z"].c_init is False
assert fields["z"].c_has_default is False
def test_kw_only_defaults_field_bitmask(self) -> None:
fields = _field_map(_TestCxxAutoInitKwOnlyDefaults)
assert fields["p_default"].c_has_default is True
assert fields["k_required"].c_kw_only is True
assert fields["k_default"].c_kw_only is True
assert fields["k_default"].c_has_default is True
assert fields["hidden"].c_init is False
assert fields["hidden"].c_has_default is True
class TestAutoInitSignature:
def test_auto_init_signature_layout(self) -> None:
sig = inspect.signature(_TestCxxAutoInit.__init__)
assert tuple(sig.parameters) == ("self", "a", "d", "c")
assert sig.parameters["a"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters["d"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters["c"].kind == inspect.Parameter.KEYWORD_ONLY
def test_auto_init_signature_required_vs_default(self) -> None:
sig = inspect.signature(_TestCxxAutoInit.__init__)
assert sig.parameters["a"].default is inspect.Parameter.empty
assert sig.parameters["c"].default is inspect.Parameter.empty
assert sig.parameters["d"].default is not inspect.Parameter.empty
def test_simple_signature(self) -> None:
sig = inspect.signature(_TestCxxAutoInitSimple.__init__)
assert tuple(sig.parameters) == ("self", "x", "y")
assert all(
p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
for p in (sig.parameters["x"], sig.parameters["y"])
)
def test_all_init_off_signature_is_no_arg(self) -> None:
sig = inspect.signature(_TestCxxAutoInitAllInitOff.__init__)
assert tuple(sig.parameters) == ("self",)
def test_kw_only_defaults_signature_layout(self) -> None:
sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
assert tuple(sig.parameters) == (
"self",
"p_required",
"p_default",
"k_required",
"k_default",
)
assert sig.parameters["p_required"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters["p_default"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters["k_required"].kind == inspect.Parameter.KEYWORD_ONLY
assert sig.parameters["k_default"].kind == inspect.Parameter.KEYWORD_ONLY
assert sig.parameters["p_required"].default is inspect.Parameter.empty
assert sig.parameters["k_required"].default is inspect.Parameter.empty
assert sig.parameters["p_default"].default is not inspect.Parameter.empty
assert sig.parameters["k_default"].default is not inspect.Parameter.empty
def test_inheritance_signature_includes_parent_fields(self) -> None:
sig = inspect.signature(_TestCxxAutoInitChild.__init__)
# Required positional params must precede default ones in Python,
# so child_required comes before parent_default.
assert tuple(sig.parameters) == (
"self",
"parent_required",
"child_required",
"parent_default",
"child_kw_only",
)
assert sig.parameters["child_kw_only"].kind == inspect.Parameter.KEYWORD_ONLY
assert sig.parameters["parent_default"].default is not inspect.Parameter.empty
def test_init_false_field_not_in_signature(self) -> None:
"""B is init=False and should not appear in signature."""
sig = inspect.signature(_TestCxxAutoInit.__init__)
assert "b" not in sig.parameters
def test_hidden_field_not_in_signature(self) -> None:
"""Hidden is init=False and should not appear in signature."""
sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
assert "hidden" not in sig.parameters
def test_kw_only_params_after_positional(self) -> None:
"""Keyword-only params should come after positional in the signature."""
sig = inspect.signature(_TestCxxAutoInit.__init__)
params = list(sig.parameters.values())
saw_kw_only = False
for p in params[1:]: # skip self
if p.kind == inspect.Parameter.KEYWORD_ONLY:
saw_kw_only = True
elif saw_kw_only:
assert p.kind == inspect.Parameter.KEYWORD_ONLY, (
f"Parameter {p.name} is {p.kind} after a KEYWORD_ONLY param"
)
def test_child_signature_required_before_optional(self) -> None:
"""The child signature should have all required positional before optional."""
sig = inspect.signature(_TestCxxAutoInitChild.__init__)
params = list(sig.parameters.values())[1:] # skip self
positional = [p for p in params if p.kind != inspect.Parameter.KEYWORD_ONLY]
saw_default = False
for p in positional:
if p.default is not inspect.Parameter.empty:
saw_default = True
elif saw_default:
pytest.fail(f"Required param '{p.name}' appears after a default param")
class TestAutoInitConstruction:
def test_auto_init_minimal_required(self) -> None:
obj = _TestCxxAutoInit(1, c=3)
assert obj.a == 1
assert obj.b == 42
assert obj.c == 3
assert obj.d == 99
def test_auto_init_all_keyword_arguments(self) -> None:
obj = _TestCxxAutoInit(a=10, c=30, d=20)
assert obj.a == 10
assert obj.b == 42
assert obj.c == 30
assert obj.d == 20
def test_auto_init_keyword_order_irrelevant(self) -> None:
obj = _TestCxxAutoInit(c=7, d=8, a=9)
assert obj.a == 9
assert obj.c == 7
assert obj.d == 8
assert obj.b == 42
def test_auto_init_second_positional_maps_to_d(self) -> None:
obj = _TestCxxAutoInit(1, 2, c=3)
assert obj.a == 1
assert obj.d == 2
assert obj.c == 3
assert obj.b == 42
def test_mutate_fields_after_construction(self) -> None:
obj = _TestCxxAutoInit(1, c=2)
obj.b = 100
obj.c = 999
assert obj.b == 100
assert obj.c == 999
class TestAutoInitErrors:
def test_missing_required_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(c=3) # ty: ignore[missing-argument]
def test_missing_required_kw_only(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1) # ty: ignore[missing-argument]
def test_kw_only_rejects_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, 2, 3) # ty: ignore[missing-argument, too-many-positional-arguments]
def test_duplicate_argument_detection(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, 2, c=3, d=4) # ty: ignore[parameter-already-assigned]
def test_unexpected_keyword(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, c=2, nope=3) # ty: ignore[unknown-argument]
def test_init_false_field_rejected_in_python_init(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, b=2, c=3) # ty: ignore[unknown-argument]
def test_type_mismatch_for_required_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit("x", c=2) # ty: ignore[invalid-argument-type]
def test_type_mismatch_for_kw_only(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, c="x") # ty: ignore[invalid-argument-type]
def test_type_mismatch_for_defaultable_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInit(1, d="x", c=3) # ty: ignore[invalid-argument-type]
def test_init_false_field_rejected_via_dict_unpacking(self) -> None:
"""B is init=False, rejected even when passed via **dict."""
with pytest.raises(TypeError):
_TestCxxAutoInit(**{"a": 1, "c": 2, "b": 3})
def test_positional_and_keyword_same_field(self) -> None:
"""Providing a field both positionally and as keyword should error."""
with pytest.raises(TypeError):
_TestCxxAutoInit(1, a=2, c=3) # ty: ignore[parameter-already-assigned]
def test_none_for_required_integer_field(self) -> None:
"""Passing None where an int64_t is expected should raise TypeError."""
with pytest.raises(TypeError):
_TestCxxAutoInitSimple(None, 1) # ty: ignore[invalid-argument-type]
def test_none_for_keyword_integer_field(self) -> None:
"""Passing None as keyword where int64_t is expected."""
with pytest.raises(TypeError):
_TestCxxAutoInitSimple(x=None, y=1) # ty: ignore[invalid-argument-type]
class TestAutoInitLowLevelFfiInit:
def test_low_level_kwargs_protocol(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
_ffi_init(obj, core.KWARGS, "a", 1, "c", 3)
assert obj.a == 1
assert obj.b == 42
assert obj.c == 3
assert obj.d == 99
def test_low_level_kwargs_even_pairs_required(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, "a", 1, "c")
def test_low_level_kwargs_duplicate_name(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, "a", 1, "a", 2, "c", 3)
def test_low_level_kwargs_unknown_name(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, "a", 1, "unknown", 2, "c", 3)
def test_low_level_kwargs_key_must_be_string(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, 1, 2, "a", 3, "c", 4)
def test_low_level_positional_too_many(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, 1, 2, 3, 4)
def test_low_level_missing_required(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, 1)
def test_low_level_kw_only_field_rejected_positionally(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, 1, 2)
def test_low_level_init_false_field_rejected(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, "a", 1, "b", 2, "c", 3)
def test_low_level_kwargs_all_init_fields_explicit(self) -> None:
"""Providing all init=True fields via KWARGS."""
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
_ffi_init(obj, core.KWARGS, "a", 10, "c", 30, "d", 40)
assert obj.a == 10
assert obj.b == 42 # init=False, default
assert obj.c == 30
assert obj.d == 40
def test_low_level_kwargs_empty_string_key(self) -> None:
"""Empty string as keyword name should be rejected as unknown."""
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError, match="unexpected keyword"):
_ffi_init(obj, core.KWARGS, "", 1, "a", 2, "c", 3)
def test_low_level_kwargs_odd_kv_count(self) -> None:
"""Odd number of key-value args after KWARGS sentinel."""
obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
with pytest.raises(TypeError):
_ffi_init(obj, core.KWARGS, "x")
def test_low_level_positional_only_simple(self) -> None:
"""Positional-only mode (no KWARGS sentinel)."""
obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
_ffi_init(obj, 10, 20)
assert obj.x == 10
assert obj.y == 20
def test_low_level_zero_args_missing_required(self) -> None:
"""Zero args for a type that requires them."""
obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
with pytest.raises(TypeError, match="missing required"):
_ffi_init(obj)
def test_low_level_kwargs_sentinel_only_no_kv_pairs(self) -> None:
"""KWARGS sentinel with zero key-value pairs; required fields still missing."""
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError, match="missing required"):
_ffi_init(obj, core.KWARGS)
def test_low_level_kwargs_positional_then_sentinel_no_kv(self) -> None:
"""Positional args followed by KWARGS sentinel but no key-value pairs."""
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
# a=1 positionally, sentinel, no KV pairs → c is still missing
with pytest.raises(TypeError, match="missing required"):
_ffi_init(obj, 1, core.KWARGS)
def test_low_level_child_positional_routing(self) -> None:
"""Verify the inheritance positional fix at the raw __ffi_init__ level.
After stable_partition, pos_indices should be:
[parent_required, child_required, parent_default]
So 2 positional args map to parent_required=1, child_required=2.
"""
obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
_ffi_init(obj, 1, 2, core.KWARGS, "child_kw_only", 3)
assert obj.parent_required == 1
assert obj.child_required == 2
assert obj.parent_default == 5 # default
assert obj.child_kw_only == 3
def test_low_level_child_all_three_positional(self) -> None:
"""Three positional args for child at the raw protocol level."""
obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
_ffi_init(obj, 1, 2, 3, core.KWARGS, "child_kw_only", 4)
assert obj.parent_required == 1
assert obj.child_required == 2
assert obj.parent_default == 3
assert obj.child_kw_only == 4
def test_low_level_child_too_many_positional(self) -> None:
"""Four positional args exceed the 3 positional slots for child."""
obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
with pytest.raises(TypeError, match="positional"):
_ffi_init(obj, 1, 2, 3, 4, core.KWARGS, "child_kw_only", 5)
class TestAutoInitSimple:
def test_simple_positional(self) -> None:
obj = _TestCxxAutoInitSimple(10, 20)
assert obj.x == 10
assert obj.y == 20
def test_simple_keyword(self) -> None:
obj = _TestCxxAutoInitSimple(x=10, y=20)
assert obj.x == 10
assert obj.y == 20
def test_simple_mixed(self) -> None:
obj = _TestCxxAutoInitSimple(10, y=20)
assert obj.x == 10
assert obj.y == 20
def test_simple_missing_required(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitSimple(x=10) # ty: ignore[missing-argument]
def test_simple_too_many_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitSimple(1, 2, 3) # ty: ignore[too-many-positional-arguments]
def test_simple_unexpected_keyword(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitSimple(x=1, y=2, z=3) # ty: ignore[unknown-argument]
def test_simple_low_level_kwargs(self) -> None:
obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
_ffi_init(obj, core.KWARGS, "x", 10, "y", 20)
assert obj.x == 10
assert obj.y == 20
def test_zero_values(self) -> None:
obj = _TestCxxAutoInitSimple(0, 0)
assert obj.x == 0
assert obj.y == 0
def test_negative_values(self) -> None:
obj = _TestCxxAutoInitSimple(-1, -9999999)
assert obj.x == -1
assert obj.y == -9999999
def test_large_values(self) -> None:
large = 2**62
obj = _TestCxxAutoInitSimple(large, -large)
assert obj.x == large
assert obj.y == -large
def test_float_for_integer_field(self) -> None:
"""Passing float where int64_t is expected - should truncate or error."""
try:
obj = _TestCxxAutoInitSimple(1.5, 2) # ty: ignore[invalid-argument-type]
assert isinstance(obj.x, int)
except TypeError:
pass # Also acceptable
def test_bool_for_integer_field(self) -> None:
"""Booleans are valid ints in Python but should work correctly."""
obj = _TestCxxAutoInitSimple(True, False)
assert obj.x == 1
assert obj.y == 0
class TestAutoInitAllInitOff:
def test_no_arg_constructor(self) -> None:
obj = _TestCxxAutoInitAllInitOff()
assert obj.x == 7
assert obj.y == 9
assert obj.z == 1234
def test_rejects_positional_args(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitAllInitOff(1) # ty: ignore[too-many-positional-arguments]
def test_rejects_keyword_args(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitAllInitOff(x=1) # ty: ignore[unknown-argument]
def test_low_level_empty_init(self) -> None:
obj = _TestCxxAutoInitAllInitOff.__new__(_TestCxxAutoInitAllInitOff)
_ffi_init(obj)
assert obj.x == 7
assert obj.y == 9
assert obj.z == 1234
def test_mutate_fields(self) -> None:
obj = _TestCxxAutoInitAllInitOff()
obj.x = 101
obj.y = 202
obj.z = 303
assert (obj.x, obj.y, obj.z) == (101, 202, 303)
def test_empty_kwargs_dict_star(self) -> None:
"""Passing **{} should be fine (empty kwargs)."""
obj = _TestCxxAutoInitAllInitOff(**{})
assert obj.x == 7
def test_low_level_rejects_positional(self) -> None:
obj = _TestCxxAutoInitAllInitOff.__new__(_TestCxxAutoInitAllInitOff)
with pytest.raises(TypeError):
_ffi_init(obj, 1)
class TestAutoInitKwOnlyDefaults:
def test_minimal_required(self) -> None:
obj = _TestCxxAutoInitKwOnlyDefaults(1, k_required=2)
assert obj.p_required == 1
assert obj.p_default == 11
assert obj.k_required == 2
assert obj.k_default == 22
assert obj.hidden == 33
def test_override_defaults(self) -> None:
obj = _TestCxxAutoInitKwOnlyDefaults(p_required=1, p_default=4, k_required=5, k_default=6)
assert obj.p_required == 1
assert obj.p_default == 4
assert obj.k_required == 5
assert obj.k_default == 6
assert obj.hidden == 33
def test_missing_required_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitKwOnlyDefaults(k_required=2) # ty: ignore[missing-argument]
def test_missing_required_kw_only(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitKwOnlyDefaults(1) # ty: ignore[missing-argument]
def test_kw_only_rejects_positional(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitKwOnlyDefaults(1, 2, 3) # ty: ignore[missing-argument, too-many-positional-arguments]
def test_hidden_init_false_field_not_accepted(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitKwOnlyDefaults(1, k_required=2, hidden=4) # ty: ignore[unknown-argument]
def test_type_mismatch(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitKwOnlyDefaults("x", k_required=2) # ty: ignore[invalid-argument-type]
def test_low_level_kwargs_call(self) -> None:
obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
_ffi_init(obj, core.KWARGS, "p_required", 1, "k_required", 2)
assert obj.p_required == 1
assert obj.p_default == 11
assert obj.k_required == 2
assert obj.k_default == 22
assert obj.hidden == 33
def test_kw_only_via_dict_unpacking(self) -> None:
"""Verify kw_only fields work via **dict."""
kwargs = {"k_required": 100, "k_default": 200}
obj = _TestCxxAutoInitKwOnlyDefaults(1, **kwargs)
assert obj.p_required == 1
assert obj.p_default == 11 # default
assert obj.k_required == 100
assert obj.k_default == 200
assert obj.hidden == 33 # init=False default
class TestAutoInitInheritance:
def test_parent_constructor(self) -> None:
obj = _TestCxxAutoInitParent(10)
assert obj.parent_required == 10
assert obj.parent_default == 5
def test_parent_all_keyword(self) -> None:
obj = _TestCxxAutoInitParent(parent_required=10, parent_default=20)
assert obj.parent_required == 10
assert obj.parent_default == 20
def test_parent_positional_then_keyword(self) -> None:
obj = _TestCxxAutoInitParent(10, parent_default=20)
assert obj.parent_required == 10
assert obj.parent_default == 20
def test_child_constructor_uses_parent_and_child_fields(self) -> None:
obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, child_kw_only=3)
assert obj.parent_required == 1
assert obj.parent_default == 5
assert obj.child_required == 2
assert obj.child_kw_only == 3
def test_child_all_keyword_with_parent_default_override(self) -> None:
# fmt: off
obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, parent_default=99, child_kw_only=3)
# fmt: on
assert obj.parent_required == 1
assert obj.child_required == 2
assert obj.parent_default == 99
assert obj.child_kw_only == 3
def test_child_missing_parent_required(self) -> None:
with pytest.raises(TypeError):
_TestCxxAutoInitChild(child_required=2, child_kw_only=3) # ty: ignore[missing-argument]
def test_child_two_positional_args_routes_correctly(self) -> None:
"""Calling Child(1, 2, child_kw_only=3) should set parent_required=1, child_required=2.
The Python signature is:
(self, parent_required, child_required, parent_default=..., *, child_kw_only)
So positional arg 0 = parent_required, positional arg 1 = child_required.
"""
obj = _TestCxxAutoInitChild(1, 2, child_kw_only=3)
assert obj.parent_required == 1
assert obj.child_required == 2
assert obj.parent_default == 5 # should use the default
assert obj.child_kw_only == 3
def test_child_three_positional_args_no_silent_swap(self) -> None:
"""Calling Child(1, 2, 3, child_kw_only=4) should map correctly.
Python signature order: parent_required=1, child_required=2, parent_default=3
"""
obj = _TestCxxAutoInitChild(1, 2, 3, child_kw_only=4)
assert obj.parent_required == 1
assert obj.child_required == 2
assert obj.parent_default == 3
assert obj.child_kw_only == 4
def test_child_one_positional_rest_keyword(self) -> None:
"""Mix of positional and keyword to verify correct mapping."""
obj = _TestCxxAutoInitChild(10, child_required=20, parent_default=30, child_kw_only=40)
assert obj.parent_required == 10
assert obj.child_required == 20
assert obj.parent_default == 30
assert obj.child_kw_only == 40
class TestAutoInitCopyBehavior:
"""Test copy/deepcopy/replace interplay with auto-init objects."""
def test_shallow_copy(self) -> None:
obj = _TestCxxAutoInitSimple(10, 20)
obj_copy = copy.copy(obj)
assert obj_copy.x == 10
assert obj_copy.y == 20
assert not obj.same_as(obj_copy)
def test_deepcopy(self) -> None:
obj = _TestCxxAutoInitSimple(10, 20)
obj_copy = copy.deepcopy(obj)
assert obj_copy.x == 10
assert obj_copy.y == 20
assert not obj.same_as(obj_copy)
@requires_py313
def test_replace(self) -> None:
obj = _TestCxxAutoInit(1, c=3)
replaced = copy.replace(obj, a=100, c=300) # type: ignore[attr-defined]
assert replaced.a == 100
assert replaced.b == 42
assert replaced.c == 300
assert replaced.d == 99
def test_copy_preserves_init_false_field(self) -> None:
"""After construction, mutating the init=False field and copying."""
obj = _TestCxxAutoInit(1, c=3)
assert obj.b == 42
obj.b = 999
assert obj.b == 999
obj_copy = copy.copy(obj)
assert obj_copy.b == 999
def test_copy_preserves_default_override(self) -> None:
"""Override a default field, then copy should preserve the override."""
obj = _TestCxxAutoInit(1, c=3, d=55)
obj_copy = copy.copy(obj)
assert obj_copy.d == 55
def test_deepcopy_all_init_off(self) -> None:
"""Deepcopy of an object with all fields init=False."""
obj = _TestCxxAutoInitAllInitOff()
obj.x = 111
obj.y = 222
obj.z = 333
obj_copy = copy.deepcopy(obj)
assert obj_copy.x == 111
assert obj_copy.y == 222
assert obj_copy.z == 333
assert not obj.same_as(obj_copy)
@requires_py313
def test_replace_kw_only_defaults(self) -> None:
obj = _TestCxxAutoInitKwOnlyDefaults(1, k_required=2)
replaced = copy.replace(obj, k_required=99, p_default=88) # type: ignore[attr-defined]
assert replaced.p_required == 1
assert replaced.p_default == 88
assert replaced.k_required == 99
assert replaced.k_default == 22
assert replaced.hidden == 33
class TestAutoInitReinitialization:
"""Test what happens when __ffi_init__ is called multiple times."""
def test_reinit_changes_handle(self) -> None:
"""Calling __ffi_init__ again should create a new underlying object."""
obj = _TestCxxAutoInit(1, c=3)
original_handle = obj.__chandle__()
assert obj.a == 1
_ffi_init(obj, core.KWARGS, "a", 100, "c", 300)
assert obj.a == 100
assert obj.c == 300
assert obj.__chandle__() != original_handle
def test_reinit_resets_init_false_field(self) -> None:
"""Re-initialization should reset init=False fields to defaults."""
obj = _TestCxxAutoInit(1, c=3)
obj.b = 999
assert obj.b == 999
_ffi_init(obj, core.KWARGS, "a", 2, "c", 4)
assert obj.b == 42 # reset to default
class TestAutoInitTypeChecks:
"""Verify isinstance relationships for auto-init objects."""
def test_parent_isinstance(self) -> None:
obj = _TestCxxAutoInitParent(1)
assert isinstance(obj, _TestCxxAutoInitParent)
assert isinstance(obj, core.Object)
def test_child_isinstance_parent(self) -> None:
obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, child_kw_only=3)
assert isinstance(obj, _TestCxxAutoInitChild)
assert isinstance(obj, _TestCxxAutoInitParent)
assert isinstance(obj, core.Object)
def test_parent_not_isinstance_child(self) -> None:
"""A parent object should not pass isinstance check for a child class."""
obj = _TestCxxAutoInitParent(1)
assert not isinstance(obj, _TestCxxAutoInitChild)
class TestAutoInitInstanceIsolation:
"""Verify that multiple instances don't share mutable state."""
def test_separate_instances_are_independent(self) -> None:
a = _TestCxxAutoInitSimple(1, 2)
b = _TestCxxAutoInitSimple(3, 4)
assert a.x == 1
assert b.x == 3
assert not a.same_as(b)
def test_mutating_one_instance_doesnt_affect_another(self) -> None:
a = _TestCxxAutoInit(1, c=3)
b = _TestCxxAutoInit(1, c=3)
assert a.b == 42
assert b.b == 42
a.b = 999
assert a.b == 999
assert b.b == 42
def test_all_init_off_instances_are_independent(self) -> None:
a = _TestCxxAutoInitAllInitOff()
b = _TestCxxAutoInitAllInitOff()
a.x = 100
assert b.x == 7
class TestAutoInitReinitInitOffNoDefault:
"""Test reinit behavior for init=False fields with and without reflection defaults."""
def test_reinit_init_false_with_default_resets(self) -> None:
"""Fields b (init=False, default=42) should reset to default on reinit."""
obj = _TestCxxAutoInit(1, c=3)
obj.b = 999
_ffi_init(obj, core.KWARGS, "a", 2, "c", 4)
assert obj.b == 42
def test_reinit_init_false_without_reflection_default(self) -> None:
"""Field z has init=False AND no reflection default (c_has_default=False).
On reinit, z should get whatever the C++ creator sets (1234).
"""
obj = _TestCxxAutoInitAllInitOff()
assert obj.z == 1234
obj.z = 9999
assert obj.z == 9999
# Reinit via low-level call
_ffi_init(obj)
# z has no reflection default, so creator's C++ default (1234) is used
assert obj.z == 1234
class TestAutoInitErrorMessages:
"""Verify that error messages name the correct field after pos_indices reordering."""
def test_missing_child_required_names_correct_field(self) -> None:
"""When child_required is missing, error should mention 'child_required'."""
with pytest.raises(TypeError, match="child_required"):
_TestCxxAutoInitChild(parent_required=1, child_kw_only=3) # ty: ignore[missing-argument]
def test_missing_parent_required_names_correct_field(self) -> None:
"""When parent_required is missing, error should mention 'parent_required'."""
with pytest.raises(TypeError, match="parent_required"):
_TestCxxAutoInitChild(child_required=2, child_kw_only=3) # ty: ignore[missing-argument]
def test_missing_kw_only_names_correct_field(self) -> None:
"""When child_kw_only is missing, error should mention 'child_kw_only'."""
with pytest.raises(TypeError, match="child_kw_only"):
_TestCxxAutoInitChild(parent_required=1, child_required=2) # ty: ignore[missing-argument]
def test_too_many_positional_error_message(self) -> None:
"""Error for too many positional args should mention the correct count."""
with pytest.raises(TypeError, match="3 positional"):
_TestCxxAutoInitChild(1, 2, 3, 4, child_kw_only=5) # ty: ignore[too-many-positional-arguments]
def test_unexpected_keyword_error_message(self) -> None:
"""Error for unknown keyword should mention the keyword name."""
with pytest.raises(TypeError, match="bogus"):
_TestCxxAutoInit(1, c=2, bogus=3) # ty: ignore[unknown-argument]
def test_duplicate_arg_error_message(self) -> None:
"""Error for duplicate argument should mention the field name."""
with pytest.raises(TypeError, match=r"multiple values.*a"):
_TestCxxAutoInit(1, c=2, a=3) # ty: ignore[parameter-already-assigned]
class TestAutoInitLowLevelKwOnlyDefaults:
"""Low-level KWARGS protocol tests for the KwOnlyDefaults type."""
def test_low_level_positional_plus_kwargs_kw_only(self) -> None:
"""Positional arg for p_required, then KWARGS for kw_only fields."""
obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
_ffi_init(obj, 1, core.KWARGS, "k_required", 2)
assert obj.p_required == 1
assert obj.p_default == 11
assert obj.k_required == 2
assert obj.k_default == 22
assert obj.hidden == 33
def test_low_level_two_positional_plus_kwargs(self) -> None:
"""Two positional args (p_required, p_default) then KWARGS for kw_only."""
obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
_ffi_init(obj, 1, 2, core.KWARGS, "k_required", 3, "k_default", 4)
assert obj.p_required == 1
assert obj.p_default == 2
assert obj.k_required == 3
assert obj.k_default == 4
assert obj.hidden == 33
def test_low_level_all_via_kwargs(self) -> None:
"""All init=True fields via KWARGS, no positional."""
obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
_ffi_init(
obj, core.KWARGS, "p_required", 10, "p_default", 20, "k_required", 30, "k_default", 40
)
assert obj.p_required == 10
assert obj.p_default == 20
assert obj.k_required == 30
assert obj.k_default == 40
assert obj.hidden == 33
class TestClassLevelInitFalse:
"""init(false) passed to ObjectDef constructor suppresses __ffi_init__."""
def test_no_ffi_init_method(self) -> None:
type_index = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__").type_index
ffi_init = core._lookup_type_attr(type_index, "__ffi_init__")
assert ffi_init is None
def test_has_fields(self) -> None:
type_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
field_names = [f.name for f in type_info.fields]
assert field_names == ["x", "y"]
def test_direct_construction_raises(self) -> None:
with pytest.raises(TypeError, match="cannot be constructed directly"):
_TestCxxNoAutoInit(1, 2) # ty: ignore[too-many-positional-arguments]
def test_has_shallow_copy(self) -> None:
type_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
# __ffi_shallow_copy__ is now TypeAttrColumn only, not a method
fn = core._lookup_type_attr(type_info.type_index, "__ffi_shallow_copy__")
assert fn is not None
# ###########################################################################
# kw_only error message regression tests
# ###########################################################################
class TestKwOnlyErrorMessages:
"""Verify that error messages distinguish keyword-only from positional."""
def test_missing_kw_only_error_says_keyword_only(self) -> None:
"""Missing required kw_only field should say 'keyword-only'."""
with pytest.raises(TypeError, match="keyword-only"):
_TestCxxAutoInit(1) # ty: ignore[missing-argument]
def test_missing_positional_error_does_not_say_keyword_only(self) -> None:
"""Missing required positional field should NOT say 'keyword-only'."""
with pytest.raises(TypeError, match="missing required argument") as exc_info:
_TestCxxAutoInit(c=3) # ty: ignore[missing-argument]
assert "keyword-only" not in str(exc_info.value)
def test_child_missing_kw_only_error_says_keyword_only(self) -> None:
"""Inherited kw_only field should produce 'keyword-only' error."""
with pytest.raises(TypeError, match="keyword-only"):
_TestCxxAutoInitChild(parent_required=1, child_required=2) # ty: ignore[missing-argument]
def test_kw_only_defaults_missing_error(self) -> None:
"""KwOnlyDefaults type: missing k_required should say 'keyword-only'."""
with pytest.raises(TypeError, match="keyword-only"):
_TestCxxAutoInitKwOnlyDefaults(1) # ty: ignore[missing-argument]
def test_low_level_kw_only_error_says_keyword_only(self) -> None:
"""Low-level __ffi_init__ also gives 'keyword-only' error."""
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
with pytest.raises(TypeError, match="keyword-only"):
_ffi_init(obj, 1)
# ###########################################################################
# __ffi_init__ TypeMethod registration regression tests
#
# These tests verify that def(init<>) in C++ registers __ffi_init__ as a
# TypeMethod (not just TypeAttrColumn), preserving type_schema metadata.
# They also verify the __ffi_init__ instance method wrapper and the MISSING
# sentinel used for default parameter values.
# See commit 2987899a for the original fix.
# ###########################################################################
class TestFfiInitAsTypeMethod:
"""Verify that classes using ``def(init<>)`` expose __ffi_init__ as a TypeMethod."""
@pytest.mark.parametrize("cls", [TestIntPair, TestCompare, TestHash])
def test_explicit_init_has_ffi_init_type_method(self, cls: type) -> None:
type_info = cls.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
method_names = {m.name for m in type_info.methods}
assert "__ffi_init__" in method_names
def test_auto_init_does_not_have_ffi_init_type_method(self) -> None:
type_info = _TestCxxAutoInit.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
method_names = {m.name for m in type_info.methods}
assert "__ffi_init__" not in method_names
@pytest.mark.parametrize("cls", [TestIntPair, TestCompare, TestHash])
def test_ffi_init_type_method_has_func(self, cls: type) -> None:
type_info = cls.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
for method in type_info.methods:
if method.name == "__ffi_init__":
assert method.func is not None
break
else:
pytest.fail(f"__ffi_init__ not found in {cls.__name__} methods") # ty: ignore[invalid-argument-type]
class TestFfiInitAsInstanceMethod:
"""Verify that __ffi_init__ is available as a callable method on registered classes."""
def test_explicit_init_class_has_ffi_init_attr(self) -> None:
assert hasattr(TestIntPair, "__ffi_init__")
def test_auto_init_class_has_ffi_init_attr(self) -> None:
assert hasattr(_TestCxxAutoInit, "__ffi_init__")
def test_ffi_init_constructs_explicit_init_object(self) -> None:
obj = TestIntPair.__new__(TestIntPair)
obj.__ffi_init__(10, 20) # ty: ignore[unresolved-attribute]
assert obj.a == 10
assert obj.b == 20
assert obj.sum() == 30
def test_ffi_init_constructs_auto_init_object(self) -> None:
obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
obj.__ffi_init__(7, 8) # ty: ignore[unresolved-attribute]
assert obj.x == 7
assert obj.y == 8
def test_ffi_init_with_kwargs_protocol(self) -> None:
obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
obj.__ffi_init__(a=1, c=3) # ty: ignore[unresolved-attribute]
assert obj.a == 1
assert obj.b == 42
assert obj.c == 3
assert obj.d == 99
class TestFfiInitTypeOwnerGuard:
"""Verify the type-owner guard prevents subclass misuse of __ffi_init__."""
def test_same_type_ffi_init_succeeds(self) -> None:
obj = TestIntPair.__new__(TestIntPair)
TestIntPair.__ffi_init__(obj, 1, 2) # ty: ignore[unresolved-attribute]
assert obj.a == 1
assert obj.b == 2
def test_subclass_ffi_init_raises_type_error(self) -> None:
obj = _TestCxxClassDerivedDerived.__new__(_TestCxxClassDerivedDerived)
with pytest.raises(TypeError, match="not supported"):
_TestCxxClassDerived.__ffi_init__(obj, 1, 2, 3.0) # ty: ignore[unresolved-attribute]
class TestMissingSentinelDefaults:
"""Verify that auto-generated __init__ signatures use core.MISSING for defaults."""
def test_default_params_use_core_missing(self) -> None:
sig = inspect.signature(_TestCxxAutoInit.__init__)
d_param = sig.parameters["d"]
assert d_param.default is not inspect.Parameter.empty
assert d_param.default is core.MISSING
def test_kw_only_default_uses_core_missing(self) -> None:
sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
assert sig.parameters["k_default"].default is core.MISSING
assert sig.parameters["p_default"].default is core.MISSING
def test_required_params_have_no_default(self) -> None:
sig = inspect.signature(_TestCxxAutoInit.__init__)
assert sig.parameters["a"].default is inspect.Parameter.empty
assert sig.parameters["c"].default is inspect.Parameter.empty
def test_missing_not_sent_to_ffi(self) -> None:
"""MISSING sentinel values should be stripped, not forwarded to C++."""
obj = _TestCxxAutoInitKwOnlyDefaults(p_required=1, k_required=2)
assert obj.p_default == 11
assert obj.k_default == 22
assert obj.hidden == 33
def test_derived_class_defaults(self) -> None:
obj = _TestCxxClassDerived(v_i64=1, v_i32=2, v_f64=3.0)
assert obj.v_f32 == 8.0
class TestFfiInitDualRegistration:
"""Verify __ffi_init__ is available from both TypeMethod and TypeAttrColumn."""
@pytest.mark.parametrize("cls", [TestIntPair, TestCompare])
def test_explicit_init_in_type_attr_column(self, cls: type) -> None:
type_info = cls.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
assert ffi_init is not None
def test_auto_init_in_type_attr_column(self) -> None:
type_info = _TestCxxAutoInit.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
assert ffi_init is not None
def test_explicit_init_method_and_attr_produce_same_result(self) -> None:
type_info = TestIntPair.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
method_func = None
for method in type_info.methods:
if method.name == "__ffi_init__":
method_func = method.func
break
assert method_func is not None
attr_func = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
assert attr_func is not None
obj1 = TestIntPair.__new__(TestIntPair)
obj1.__init_handle_by_constructor__(method_func, 10, 20)
obj2 = TestIntPair.__new__(TestIntPair)
obj2.__init_handle_by_constructor__(attr_func, 10, 20)
assert obj1.a == obj2.a == 10
assert obj1.b == obj2.b == 20
# ###########################################################################
# Python 3.14 annotation regression (PEP 749)
#
# Python 3.14 stores annotations lazily via __annotate_func__ instead of
# directly in cls.__dict__["__annotations__"]. This broke @py_class field
# discovery when it used cls.__dict__.get("__annotations__", {}).
# ###########################################################################
@py_class("testing.PyClassSimple")
class _PyClassSimple(core.Object):
x: int
y: int
@py_class("testing.PyClassWithDefault")
class _PyClassWithDefault(core.Object):
a: int
b: int = 42
class TestPyClassAnnotationDiscovery:
"""Regression: @py_class must discover fields on Python 3.14+ (PEP 749)."""
def test_fields_registered(self) -> None:
ti: core.TypeInfo = _PyClassSimple.__tvm_ffi_type_info__ # type: ignore[unresolved-attribute]
names = [f.name for f in ti.fields]
assert names == ["x", "y"]
def test_construct_kwargs(self) -> None:
obj = _PyClassSimple(x=1, y=2)
assert obj.x == 1
assert obj.y == 2
def test_construct_positional(self) -> None:
obj = _PyClassSimple(10, 20)
assert obj.x == 10
assert obj.y == 20
def test_default_field(self) -> None:
obj = _PyClassWithDefault(a=7)
assert obj.a == 7
assert obj.b == 42
def test_override_default(self) -> None:
obj = _PyClassWithDefault(a=1, b=2)
assert obj.a == 1
assert obj.b == 2