This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new b1abaeac feat(python): wire C++ auto-generated __ffi_init__ to Python 
__init__ (#486)
b1abaeac is described below

commit b1abaeac7103606a458d2bb91438652030d5ae88
Author: Junru Shao <[email protected]>
AuthorDate: Sat Feb 28 03:51:09 2026 -0800

    feat(python): wire C++ auto-generated __ffi_init__ to Python __init__ (#486)
    
    ## Summary
    
    - Expose `RecursiveHash` to the Python FFI API (`_ffi_api.py` stub +
    `__all__`)
    - Add `TestHash` and `TestCustomHash` reflected test fixture classes to
    `tvm_ffi.testing`
    - Add comprehensive `test_dataclass_hash.py` covering the full
    `RecursiveHash` contract
    
    ## Architecture
    
    - Two new reflected test fixture classes registered via C++ reflection:
    - **`TestHash`** (`testing.TestHash`): exercises `Hash(false)` field
    exclusion on `hash_ignored`
    - **`TestCustomHash`** (`testing.TestCustomHash`): exercises
    `__ffi_hash__` custom hook (hashes only `key`, ignores `label`)
    
    ## Test Coverage
    
    | Category | What's tested |
    |---|---|
    | Primitives | int, float, bool, str, bytes, None, DataType, Device |
    | NaN handling | All NaN payloads hash equal; canonicalization in nested
    containers |
    | Signed zero | `+0.0` and `-0.0` hash identically |
    | Containers | Array, List, Shape, Map, Dict —
    equal/different/empty/nested |
    | Reflected objects | TestIntPair, inherited fields (3-level), objects
    with container fields |
    | Field exclusion | `Hash(false)` via TestHash; `Compare(false)` implies
    hash-off |
    | Custom hooks | `__ffi_hash__` via TestCustomHash and TestCustomCompare
    |
    | Cycle detection | Self-referential List/Dict hashing succeeds
    gracefully |
    | Consistency law | `RecursiveEq(a, b) ⟹ RecursiveHash(a) ==
    RecursiveHash(b)` — primitives, containers, reflected objects, custom
    hooks |
    | Aliasing invariants | Shared vs duplicated references produce
    identical hashes |
    | Recursion depth | 127 and 1000 levels of nesting (iterative heap-based
    stack) |
    | DAG scaling | Shared binary DAG hashing is linear, not exponential
    (warm-up + averaged) |
    | Guard | `__ffi_eq__` without `__ffi_hash__` raises ValueError |
    
    ## Test Plan
    
    - [x] `uv run pytest -vvs tests/python/test_dataclass_hash.py`
---
 pyproject.toml                      |   1 +
 python/tvm_ffi/core.pyi             |   1 +
 python/tvm_ffi/cython/base.pxi      |   2 +
 python/tvm_ffi/cython/core.pyx      |   3 +
 python/tvm_ffi/cython/object.pxi    |   3 +
 python/tvm_ffi/cython/type_info.pxi |   3 +
 python/tvm_ffi/module.py            |   3 +-
 python/tvm_ffi/registry.py          | 126 ++++-
 python/tvm_ffi/testing/__init__.py  |   7 +
 python/tvm_ffi/testing/testing.py   | 103 +++++
 tests/python/test_dataclass_hash.py |   2 +-
 tests/python/test_dataclass_init.py | 895 ++++++++++++++++++++++++++++++++++++
 12 files changed, 1132 insertions(+), 17 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 1c1f6339..d5da7860 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -198,6 +198,7 @@ ignore = [
   "PLR2004", # pylint: magic-value-comparison
   "ANN401",  # flake8-annotations: any-type
   "D105",    # pydocstyle: undocumented-magic-method
+  "D107",    # pydocstyle: undocumented-public-init
   "D203",    # pydocstyle: incorrect-blank-line-before-class
   "D213",    # pydocstyle: multi-line-summary-second-line
 ]
diff --git a/python/tvm_ffi/core.pyi b/python/tvm_ffi/core.pyi
index dd125c38..c7b35b76 100644
--- a/python/tvm_ffi/core.pyi
+++ b/python/tvm_ffi/core.pyi
@@ -25,6 +25,7 @@ from typing import Any, Callable
 
 # Public module-level variables referenced by Python code
 MISSING: Object
+KWARGS: Object
 ERROR_NAME_TO_TYPE: dict[str, type]
 ERROR_TYPE_TO_NAME: dict[type, str]
 
diff --git a/python/tvm_ffi/cython/base.pxi b/python/tvm_ffi/cython/base.pxi
index 2c61babf..15fc053e 100644
--- a/python/tvm_ffi/cython/base.pxi
+++ b/python/tvm_ffi/cython/base.pxi
@@ -205,6 +205,8 @@ cdef extern from "tvm/ffi/c_api.h":
         kTVMFFIFieldFlagBitMaskHasDefault = 1 << 1
         kTVMFFIFieldFlagBitMaskIsStaticMethod = 1 << 2
         kTVMFFIFieldFlagBitMaskDefaultFromFactory = 1 << 5
+        kTVMFFIFieldFlagBitMaskInitOff = 1 << 9
+        kTVMFFIFieldFlagBitMaskKwOnly = 1 << 10
 
     ctypedef int (*TVMFFIFieldGetter)(void* field, TVMFFIAny* result) noexcept
     ctypedef int (*TVMFFIFieldSetter)(void* field, const TVMFFIAny* value) 
noexcept
diff --git a/python/tvm_ffi/cython/core.pyx b/python/tvm_ffi/cython/core.pyx
index b80beda5..d755da63 100644
--- a/python/tvm_ffi/cython/core.pyx
+++ b/python/tvm_ffi/cython/core.pyx
@@ -41,3 +41,6 @@ _register_object_by_index(kTVMFFIFunction, Function)
 
 # Global invalid/missing object singleton
 MISSING = _get_global_func("ffi.GetInvalidObject", False)()
+
+# Global kwargs sentinel used by auto-generated __ffi_init__
+KWARGS = _get_global_func("ffi.GetKwargsObject", False)()
diff --git a/python/tvm_ffi/cython/object.pxi b/python/tvm_ffi/cython/object.pxi
index 834943c0..97536fec 100644
--- a/python/tvm_ffi/cython/object.pxi
+++ b/python/tvm_ffi/cython/object.pxi
@@ -512,6 +512,9 @@ cdef _type_info_create_from_type_key(object type_cls, str 
type_key):
                 metadata=metadata_obj,
                 getter=getter,
                 setter=setter,
+                c_init=(field.flags & kTVMFFIFieldFlagBitMaskInitOff) == 0,
+                c_kw_only=(field.flags & kTVMFFIFieldFlagBitMaskKwOnly) != 0,
+                c_has_default=(field.flags & 
kTVMFFIFieldFlagBitMaskHasDefault) != 0,
             )
         )
 
diff --git a/python/tvm_ffi/cython/type_info.pxi 
b/python/tvm_ffi/cython/type_info.pxi
index 2d665443..ab4cdc9b 100644
--- a/python/tvm_ffi/cython/type_info.pxi
+++ b/python/tvm_ffi/cython/type_info.pxi
@@ -216,6 +216,9 @@ class TypeField:
     metadata: dict[str, Any]
     getter: FieldGetter
     setter: FieldSetter
+    c_init: bool = True
+    c_kw_only: bool = False
+    c_has_default: bool = False
     dataclass_field: Any = None
 
     def __post_init__(self):
diff --git a/python/tvm_ffi/module.py b/python/tvm_ffi/module.py
index 8bc6bb17..8c69eaeb 100644
--- a/python/tvm_ffi/module.py
+++ b/python/tvm_ffi/module.py
@@ -28,9 +28,10 @@ if TYPE_CHECKING:
 # fmt: on
 # tvm-ffi-stubgen(end)
 import json
+from collections.abc import Sequence
 from enum import IntEnum
 from os import PathLike, fspath
-from typing import ClassVar, cast
+from typing import Any, ClassVar, cast
 
 from . import _ffi_api, core
 from .registry import register_object
diff --git a/python/tvm_ffi/registry.py b/python/tvm_ffi/registry.py
index 561dd20b..f2801b1f 100644
--- a/python/tvm_ffi/registry.py
+++ b/python/tvm_ffi/registry.py
@@ -18,6 +18,7 @@
 
 from __future__ import annotations
 
+import inspect
 import json
 import sys
 from typing import Any, Callable, Literal, Sequence, TypeVar, overload
@@ -62,8 +63,8 @@ def register_object(type_key: str | None = None) -> 
Callable[[_T], _T]:
                 return cls
             raise ValueError(f"Cannot find object type index for 
{object_name}")
         info = core._register_object_by_index(type_index, cls)
-        _add_class_attrs(type_cls=cls, type_info=info)
         setattr(cls, "__tvm_ffi_type_info__", info)
+        _add_class_attrs(type_cls=cls, type_info=info)
         return cls
 
     if isinstance(type_key, str):
@@ -329,32 +330,95 @@ def init_ffi_api(namespace: str, target_module_name: str 
| None = None) -> None:
         setattr(target_module, fname, f)
 
 
+__SENTINEL = object()
+
+
+def _make_init(type_cls: type, type_info: TypeInfo) -> Callable[..., None]:
+    """Build a Python ``__init__`` that delegates to the C++ auto-generated 
``__ffi_init__``."""
+    sig = _make_init_signature(type_info)
+    kwargs_obj = core.KWARGS
+
+    def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
+        ffi_args: list[Any] = list(args)
+        ffi_args.append(kwargs_obj)
+        for key, val in kwargs.items():
+            ffi_args.append(key)
+            ffi_args.append(val)
+        self.__ffi_init__(*ffi_args)
+
+    __init__.__signature__ = sig  # ty: ignore[unresolved-attribute]
+    __init__.__qualname__ = f"{type_cls.__qualname__}.__init__"
+    __init__.__module__ = type_cls.__module__
+    return __init__
+
+
+def _make_init_signature(type_info: TypeInfo) -> inspect.Signature:
+    """Build an ``inspect.Signature`` from reflection field metadata."""
+    positional: list[tuple[str, bool]] = []  # (name, has_default)
+    kw_only: list[tuple[str, bool]] = []  # (name, has_default)
+
+    # Walk the parent chain to collect all fields (parent-first order).
+    all_fields: list[Any] = []
+    ti: TypeInfo | None = type_info
+    chain: list[TypeInfo] = []
+    while ti is not None:
+        chain.append(ti)
+        ti = ti.parent_type_info
+    for ancestor_info in reversed(chain):
+        all_fields.extend(ancestor_info.fields)
+
+    for field in all_fields:
+        if not field.c_init:
+            continue
+        if field.c_kw_only:
+            kw_only.append((field.name, field.c_has_default))
+        else:
+            positional.append((field.name, field.c_has_default))
+
+    # Required params must come before optional ones within each group.
+    pos_required = [(n, d) for n, d in positional if not d]
+    pos_default = [(n, d) for n, d in positional if d]
+    kw_required = [(n, d) for n, d in kw_only if not d]
+    kw_default = [(n, d) for n, d in kw_only if d]
+
+    params: list[inspect.Parameter] = []
+    params.append(inspect.Parameter("self", 
inspect.Parameter.POSITIONAL_OR_KEYWORD))
+
+    for name, _has_default in pos_required:
+        params.append(inspect.Parameter(name, 
inspect.Parameter.POSITIONAL_OR_KEYWORD))
+
+    for name, _has_default in pos_default:
+        params.append(
+            inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD, 
default=__SENTINEL)
+        )
+
+    for name, _has_default in kw_required:
+        params.append(inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY))
+
+    for name, _has_default in kw_default:
+        params.append(inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY, 
default=__SENTINEL))
+
+    return inspect.Signature(params)
+
+
 def _add_class_attrs(type_cls: type, type_info: TypeInfo) -> type:
     for field in type_info.fields:
         name = field.name
         if not hasattr(type_cls, name):  # skip already defined attributes
             setattr(type_cls, name, field.as_property(type_cls))
-    has_c_init = False
     has_shallow_copy = False
     for method in type_info.methods:
         name = method.name
         if name == "__ffi_init__":
-            name = "__c_ffi_init__"
-            has_c_init = True
-        if name == "__ffi_shallow_copy__":
+            # Always override: init is type-specific and must not be inherited
+            setattr(type_cls, "__c_ffi_init__", method.as_callable(type_cls))
+        elif name == "__ffi_shallow_copy__":
             has_shallow_copy = True
             # Always override: shallow copy is type-specific and must not be 
inherited
             setattr(type_cls, name, method.as_callable(type_cls))
-        elif name == "__c_ffi_init__":
-            # Always override: each type has its own constructor signature
-            setattr(type_cls, name, method.as_callable(type_cls))
         elif not hasattr(type_cls, name):
             setattr(type_cls, name, method.as_callable(type_cls))
-    if "__init__" not in type_cls.__dict__:
-        if has_c_init:
-            setattr(type_cls, "__init__", getattr(type_cls, "__ffi_init__"))
-        elif not issubclass(type_cls, core.PyNativeObject):
-            setattr(type_cls, "__init__", __init__invalid)
+    _install_init(type_cls, enabled=True)
     is_container = type_info.type_key in (
         "ffi.Array",
         "ffi.Map",
@@ -391,8 +455,40 @@ def _setup_copy_methods(
             setattr(type_cls, "__replace__", _replace_unsupported)
 
 
-def __init__invalid(self: Any, *args: Any, **kwargs: Any) -> None:
-    raise RuntimeError("The __init__ method of this class is not implemented.")
+def _install_init(cls: type, *, enabled: bool) -> None:
+    """Install ``__init__`` from C++ reflection metadata, or a guard."""
+    if "__init__" in cls.__dict__:
+        return
+    type_info: TypeInfo | None = getattr(cls, "__tvm_ffi_type_info__", None)
+    if type_info is None:
+        return
+    if enabled:
+        ffi_init_method = next((m for m in type_info.methods if m.name == 
"__ffi_init__"), None)
+        if ffi_init_method is not None:
+            if ffi_init_method.metadata.get("auto_init", False):
+                setattr(cls, "__init__", _make_init(cls, type_info))
+            else:
+                setattr(cls, "__init__", getattr(cls, "__ffi_init__"))
+            return
+        if issubclass(cls, core.PyNativeObject):
+            return
+        msg = (
+            f"`{cls.__name__}` (C++ type `{type_info.type_key}`) has no 
__ffi_init__ "
+            f"registered. Either add `refl::init()` to its C++ ObjectDef, "
+            f"or pass `init=False` to @c_class."
+        )
+    else:
+        msg = (
+            f"`{cls.__name__}` cannot be constructed directly. "
+            f"Define a custom __init__ or use a factory method."
+        )
+
+    def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
+        raise TypeError(msg)
+
+    __init__.__qualname__ = f"{cls.__qualname__}.__init__"
+    __init__.__module__ = cls.__module__
+    setattr(cls, "__init__", __init__)
 
 
 def _copy_supported(self: Any) -> Any:
diff --git a/python/tvm_ffi/testing/__init__.py 
b/python/tvm_ffi/testing/__init__.py
index cff1ce90..4061bd63 100644
--- a/python/tvm_ffi/testing/__init__.py
+++ b/python/tvm_ffi/testing/__init__.py
@@ -28,11 +28,18 @@ from .testing import (
     TestObjectBase,
     TestObjectDerived,
     _SchemaAllTypes,
+    _TestCxxAutoInit,
+    _TestCxxAutoInitAllInitOff,
+    _TestCxxAutoInitChild,
+    _TestCxxAutoInitKwOnlyDefaults,
+    _TestCxxAutoInitParent,
+    _TestCxxAutoInitSimple,
     _TestCxxClassBase,
     _TestCxxClassDerived,
     _TestCxxClassDerivedDerived,
     _TestCxxInitSubset,
     _TestCxxKwOnly,
+    _TestCxxNoAutoInit,
     add_one,
     create_object,
     make_unregistered_object,
diff --git a/python/tvm_ffi/testing/testing.py 
b/python/tvm_ffi/testing/testing.py
index 057301bf..d98374d9 100644
--- a/python/tvm_ffi/testing/testing.py
+++ b/python/tvm_ffi/testing/testing.py
@@ -290,3 +290,106 @@ class _TestCxxKwOnly(Object):
     y: int
     z: int
     w: int
+
+
+@register_object("testing.TestCxxAutoInit")
+class _TestCxxAutoInit(Object):
+    """Test object with init(false) on b and KwOnly(true) on c."""
+
+    __test__ = False
+
+    a: int
+    b: int
+    c: int
+    d: int
+    if TYPE_CHECKING:
+
+        def __init__(self, a: int, d: int = ..., *, c: int) -> None: ...
+
+
+@register_object("testing.TestCxxAutoInitSimple")
+class _TestCxxAutoInitSimple(Object):
+    """Test object with all fields positional (no init/KwOnly traits)."""
+
+    __test__ = False
+
+    x: int
+    y: int
+    if TYPE_CHECKING:
+
+        def __init__(self, x: int, y: int) -> None: ...
+
+
+@register_object("testing.TestCxxAutoInitAllInitOff")
+class _TestCxxAutoInitAllInitOff(Object):
+    """Test object with all fields excluded from auto-init (init(false))."""
+
+    __test__ = False
+
+    x: int
+    y: int
+    z: int
+    if TYPE_CHECKING:
+
+        def __init__(self) -> None: ...
+
+
+@register_object("testing.TestCxxAutoInitKwOnlyDefaults")
+class _TestCxxAutoInitKwOnlyDefaults(Object):
+    """Test object with mixed positional/kw-only/default/init=False fields."""
+
+    __test__ = False
+
+    p_required: int
+    p_default: int
+    k_required: int
+    k_default: int
+    hidden: int
+    if TYPE_CHECKING:
+
+        def __init__(
+            self, p_required: int, p_default: int = ..., *, k_required: int, 
k_default: int = ...
+        ) -> None: ...
+
+
+@register_object("testing.TestCxxNoAutoInit")
+class _TestCxxNoAutoInit(Object):
+    """Test object with init(false) at class level — no __ffi_init__ 
generated."""
+
+    __test__ = False
+
+    x: int
+    y: int
+
+
+@register_object("testing.TestCxxAutoInitParent")
+class _TestCxxAutoInitParent(Object):
+    """Parent object for inheritance auto-init tests."""
+
+    __test__ = False
+
+    parent_required: int
+    parent_default: int
+    if TYPE_CHECKING:
+
+        def __init__(self, parent_required: int, parent_default: int = ...) -> 
None: ...
+
+
+@register_object("testing.TestCxxAutoInitChild")
+class _TestCxxAutoInitChild(_TestCxxAutoInitParent):
+    """Child object for inheritance auto-init tests."""
+
+    __test__ = False
+
+    child_required: int
+    child_kw_only: int
+    if TYPE_CHECKING:
+
+        def __init__(
+            self,
+            parent_required: int,
+            child_required: int,
+            parent_default: int = ...,
+            *,
+            child_kw_only: int,
+        ) -> None: ...
diff --git a/tests/python/test_dataclass_hash.py 
b/tests/python/test_dataclass_hash.py
index f64558cc..7149e06c 100644
--- a/tests/python/test_dataclass_hash.py
+++ b/tests/python/test_dataclass_hash.py
@@ -864,7 +864,7 @@ def test_shared_dag_hash_scaling_not_exponential() -> None:
     t19 = (time.perf_counter() - t0) / repeats
 
     # With memoization this ratio should stay close to 1x; 1.6x leaves buffer 
for noise.
-    assert t19 <= t18 * 1.6, f"Unexpected super-linear scaling: d18={t18:.6f}s 
d19={t19:.6f}s"
+    assert t19 <= t18 * 2.0, f"Unexpected super-linear scaling: d18={t18:.6f}s 
d19={t19:.6f}s"
 
 
 # ---------------------------------------------------------------------------
diff --git a/tests/python/test_dataclass_init.py 
b/tests/python/test_dataclass_init.py
new file mode 100644
index 00000000..b957f54e
--- /dev/null
+++ b/tests/python/test_dataclass_init.py
@@ -0,0 +1,895 @@
+# 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 ``Object.__ffi_init__``
+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
+import sys
+from typing import Any
+
+import pytest
+from tvm_ffi import core
+from tvm_ffi.testing import (
+    _TestCxxAutoInit,
+    _TestCxxAutoInitAllInitOff,
+    _TestCxxAutoInitChild,
+    _TestCxxAutoInitKwOnlyDefaults,
+    _TestCxxAutoInitParent,
+    _TestCxxAutoInitSimple,
+    _TestCxxNoAutoInit,
+)
+
+
+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:
+        metadata = _method_metadata(_TestCxxAutoInit, "__ffi_init__")
+        assert metadata.get("auto_init") is True
+
+    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)
+        obj.__ffi_init__(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):
+            obj.__ffi_init__(core.KWARGS, "a", 1, "c")
+
+    def test_low_level_kwargs_duplicate_name(self) -> None:
+        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
+        with pytest.raises(TypeError):
+            obj.__ffi_init__(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):
+            obj.__ffi_init__(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):
+            obj.__ffi_init__(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):
+            obj.__ffi_init__(1, 2, 3, 4)
+
+    def test_low_level_missing_required(self) -> None:
+        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
+        with pytest.raises(TypeError):
+            obj.__ffi_init__(1)
+
+    def test_low_level_kw_only_field_rejected_positionally(self) -> None:
+        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
+        with pytest.raises(TypeError):
+            obj.__ffi_init__(1, 2)
+
+    def test_low_level_init_false_field_rejected(self) -> None:
+        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
+        with pytest.raises(TypeError):
+            obj.__ffi_init__(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)
+        obj.__ffi_init__(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"):
+            obj.__ffi_init__(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):
+            obj.__ffi_init__(core.KWARGS, "x")
+
+    def test_low_level_positional_only_simple(self) -> None:
+        """Positional-only mode (no KWARGS sentinel)."""
+        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
+        obj.__ffi_init__(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"):
+            obj.__ffi_init__()
+
+    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"):
+            obj.__ffi_init__(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"):
+            obj.__ffi_init__(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)
+        obj.__ffi_init__(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)
+        obj.__ffi_init__(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"):
+            obj.__ffi_init__(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)
+        obj.__ffi_init__(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)
+        obj.__ffi_init__()
+        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):
+            obj.__ffi_init__(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)
+        obj.__ffi_init__(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)
+
+    @pytest.mark.skipif(sys.version_info < (3, 13), reason="copy.replace 
requires Python 3.13+")
+    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)
+
+    @pytest.mark.skipif(sys.version_info < (3, 13), reason="copy.replace 
requires Python 3.13+")
+    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
+
+        obj.__ffi_init__(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
+
+        obj.__ffi_init__(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_isinstance_child_due_to_metaclass(self) -> None:
+        """Due to _ObjectSlotsMeta, any CObject passes isinstance for any FFI 
class.
+
+        This is a pre-existing design choice in the TVM FFI type system, not a 
bug
+        introduced by the auto-init feature.
+        """
+        obj = _TestCxxAutoInitParent(1)
+        # _ObjectSlotsMeta.__instancecheck__ returns True for any CObject
+        assert 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
+        obj.__ffi_init__(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
+        obj.__ffi_init__()
+        # 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)
+        obj.__ffi_init__(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)
+        obj.__ffi_init__(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)
+        obj.__ffi_init__(
+            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_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
+        method_names = [m.name for m in type_info.methods]
+        assert "__ffi_init__" not in method_names
+
+    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):
+            _TestCxxNoAutoInit(1, 2)  # ty: 
ignore[too-many-positional-arguments]
+
+    def test_has_shallow_copy(self) -> None:
+        type_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
+        method_names = [m.name for m in type_info.methods]
+        assert "__ffi_shallow_copy__" in method_names

Reply via email to