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