This is an automated email from the ASF dual-hosted git repository.
junrushao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 02e9928 fix(py_class): remove __ffi_init_inplace__ to fix memory leak
(#551)
02e9928 is described below
commit 02e9928a356c2d448e17d9fbdd37738cddceb0a1
Author: Junru Shao <[email protected]>
AuthorDate: Tue Apr 14 22:12:26 2026 -0700
fix(py_class): remove __ffi_init_inplace__ to fix memory leak (#551)
## Summary
- **Fix memory leak** in `@py_class`: the custom `__new__` override
allocated a spurious C++ object via `__ffi_new__` whenever a py_class
instance was returned from C++, which was then orphaned when
`make_ret_object` overwrote `chandle` with the actual return value.
- **Remove `__ffi_init_inplace__`** entirely — both the C++
`MakeInitInplace` function, its registration, and the `kInitInplace`
constant.
- **Unify `c_class` and `py_class`** auto-generated `__init__` to both
use `self.__init_handle_by_constructor__(ffi_init, *args)`, differing
only in guard logic: py_class uses a `chandle != 0` guard (no-op when
already allocated), c_class uses a `type_info` identity guard
(`TypeError` from subclass).
- **Remove the custom `__new__` override** — no more spurious allocation
on the `make_ret` path.
- **Wrap user-defined `__init__`** on py_class with a `chandle == 0`
guard that pre-allocates via `__ffi_new__` before user code runs; use
`functools.wraps` to preserve metadata.
### Files changed
| File | Change |
|------|--------|
| `python/tvm_ffi/_dunder.py` | Core redesign: removed `inplace` param,
added `py_class_mode`, removed `__new__` override, added user-init
wrapping with `functools.wraps` |
| `python/tvm_ffi/registry.py` | Removed `inplace=False` kwarg from
`_make_init` call |
| `include/tvm/ffi/reflection/accessor.h` | Removed `kInitInplace`
constant |
| `include/tvm/ffi/reflection/registry.h` | Updated comments (removed
`__ffi_init_inplace__` references) |
| `src/ffi/extra/dataclass.cc` | Removed `MakeInitInplace()`, its
registration, `EnsureTypeAttrColumn` call |
| `python/tvm_ffi/dataclasses/py_class.py` | Updated comment |
| `tests/python/test_dataclass_init.py` | Removed `TestInitInplace`
class (5 tests) |
| `tests/python/test_dataclass_py_class.py` | Added 8 new tests in
`TestPyClassNoLeak` |
## Test plan
- [x] All 27 pre-commit linters pass
- [x] All 2103 Python tests pass (2095 existing + 8 new)
- [x] All 349 C++ tests pass
- [x] Memory leak validation: 20k construct+deepcopy cycles with no leak
---
include/tvm/ffi/reflection/accessor.h | 13 ---
include/tvm/ffi/reflection/registry.h | 4 +-
python/tvm_ffi/_dunder.py | 88 +++++++++++----------
python/tvm_ffi/dataclasses/py_class.py | 2 +-
python/tvm_ffi/registry.py | 1 -
src/ffi/extra/dataclass.cc | 24 +-----
tests/python/test_dataclass_init.py | 59 --------------
tests/python/test_dataclass_py_class.py | 135 +++++++++++++++++++++++++++++++-
8 files changed, 185 insertions(+), 141 deletions(-)
diff --git a/include/tvm/ffi/reflection/accessor.h
b/include/tvm/ffi/reflection/accessor.h
index 84988c1..a403417 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -340,19 +340,6 @@ inline constexpr const char* kNew = "__ffi_new__";
* Keyword arguments are packed as ``[KWARGS, key0, val0, key1, val1, ...]``.
*/
inline constexpr const char* kInit = "__ffi_init__";
-/*!
- * \brief In-place init on a pre-allocated object (no allocation).
- *
- * Used by ``@py_class`` auto-generated ``__init__``: ``__new__`` has already
- * allocated the object via ``kNew``, so this function only sets fields.
- * The first argument is ``self`` (the pre-allocated object).
- *
- * Signature: ``(self: TSelf, *args, **kwargs) -> void``, where ``TSelf`` is a
subclass of
- * ObjectRef.
- *
- * Keyword arguments are packed as ``[KWARGS, key0, val0, key1, val1, ...]``.
- */
-inline constexpr const char* kInitInplace = "__ffi_init_inplace__";
/*!
* \brief Convert ``AnyView`` to a specific reflected ``TSelf`` type.
*
diff --git a/include/tvm/ffi/reflection/registry.h
b/include/tvm/ffi/reflection/registry.h
index 08570f8..3e715fe 100644
--- a/include/tvm/ffi/reflection/registry.h
+++ b/include/tvm/ffi/reflection/registry.h
@@ -726,7 +726,7 @@ class ObjectDef : public ReflectionDefBase {
TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterAttr(type_index_, &attr,
&fn_any));
}
// Step 2. Register `__ffi_new__` <== info->metadata->creator
- // Also, `__ffi_init__` and `__ffi_init_inplace__` if no explicit init is
defined.
+ // Also, `__ffi_init__` if no explicit init is defined.
if (info->metadata != nullptr && info->metadata->creator != nullptr) {
Function fn = Function::FromTyped(
[creator = info->metadata->creator]() -> ObjectRef {
@@ -739,7 +739,7 @@ class ObjectDef : public ReflectionDefBase {
TVMFFIByteArray attr = AsByteArray(type_attr::kNew);
TVMFFIAny fn_any = AnyView(fn).CopyToTVMFFIAny();
TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterAttr(type_index_, &attr,
&fn_any));
- // Step 3. Register `__ffi_init__` and `__ffi_init_inplace__` if no
explicit init is defined.
+ // Step 3. Register `__ffi_init__` if no explicit init is defined.
// Use Function::GetGlobal to look up the registration function, which
lives in
// dataclass.cc and may not be loaded yet for early (builtin) types.
if (!has_explicit_init_) {
diff --git a/python/tvm_ffi/_dunder.py b/python/tvm_ffi/_dunder.py
index af3cc98..46f0e1b 100644
--- a/python/tvm_ffi/_dunder.py
+++ b/python/tvm_ffi/_dunder.py
@@ -31,24 +31,29 @@ if TYPE_CHECKING:
def _make_init(
type_cls: type,
type_info: TypeInfo,
- inplace: bool,
ffi_init: Function,
+ py_class_mode: bool = False,
) -> Callable[..., None]:
- """Build ``__init__`` that delegates to a C++ init function.
+ """Build ``__init__`` that delegates to ``__ffi_init__``.
+
+ Both ``@c_class`` and ``@py_class`` use the same constructor-call path
+ (``self.__init_handle_by_constructor__(ffi_init, *args)``). The only
+ difference is how ``super().__init__()`` from a subclass is handled:
+
+ * **c_class** — raises ``TypeError`` (subclass must define its own init).
+ * **py_class** — silently skips when the C++ handle is already set, so
+ ``super().__init__()`` is a harmless no-op.
Parameters
----------
type_cls
- The class to build an __init__ for. Used for signature and error
messages.
+ The class to build an __init__ for.
type_info
- The TypeInfo for *type_cls*, used to build the signature and for type
checks.
- inplace : bool
- If True (py_class), *ffi_init* is ``__ffi_init_inplace__`` — called as
- ``ffi_init(self, *args)`` on a pre-allocated object.
- If False (c_class), *ffi_init* is ``__ffi_init__`` — called via
- ``self.__init_handle_by_constructor__(ffi_init, *args)``.
+ The TypeInfo for *type_cls*.
ffi_init
- The C++ initialiser resolved from TypeAttrColumn at install time.
+ The C++ ``__ffi_init__`` resolved at install time.
+ py_class_mode
+ If True, use a ``chandle`` guard instead of a ``TypeError`` guard.
"""
sig = _make_init_signature(type_info)
@@ -56,17 +61,19 @@ def _make_init(
missing = core.MISSING
has_post_init = hasattr(type_cls, "__post_init__")
- if inplace:
+ if py_class_mode:
def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
- ffi_args: list[Any] = [self, *args]
+ if self.__chandle__() != 0:
+ return
+ ffi_args: list[Any] = list(args)
if kwargs:
ffi_args.append(kwargs_obj)
for key, val in kwargs.items():
if val is not missing:
ffi_args.append(key)
ffi_args.append(val)
- ffi_init(*ffi_args)
+ self.__init_handle_by_constructor__(ffi_init, *ffi_args)
if has_post_init:
self.__post_init__()
@@ -244,16 +251,15 @@ def _install_dataclass_dunders( # noqa: PLR0912, PLR0915
unsafe_hash
If True, install ``__hash__`` using ``RecursiveHash``.
py_class_mode
- If True, use C++ ``__ffi_init_inplace__`` for ``__init__``
- (object already allocated by ``__new__``).
+ If True, use a ``chandle`` guard for ``__init__`` so that
+ ``super().__init__()`` is a no-op, and wrap user-defined
+ ``__init__`` to allocate via ``__ffi_init__`` before user code.
"""
from . import _ffi_api # noqa: PLC0415
type_info: TypeInfo = cls.__tvm_ffi_type_info__ # type: ignore[assignment]
type_index: int = type_info.type_index
- ffi_new: Function | None = core._lookup_type_attr(type_index,
"__ffi_new__")
- ffi_init_inplace: Function | None = core._lookup_type_attr(type_index,
"__ffi_init_inplace__")
# Look up __ffi_init__ from TypeMethod (preferred) or TypeAttrColumn
(fallback).
ffi_init: Function | None = None
for method in type_info.methods:
@@ -262,19 +268,8 @@ def _install_dataclass_dunders( # noqa: PLR0912, PLR0915
break
if ffi_init is None:
ffi_init = core._lookup_type_attr(type_index, "__ffi_init__")
+ ffi_new: Function | None = core._lookup_type_attr(type_index,
"__ffi_new__")
ffi_shallow_copy: Function | None = core._lookup_type_attr(type_index,
"__ffi_shallow_copy__")
- pyobject_new = core.Object.__new__
-
- # __new__ (py_class only: allocate via __ffi_new__)
- if py_class_mode and "__new__" not in cls.__dict__:
- if ffi_new is not None:
-
- def __new__(klass: type, *args: Any, **kwargs: Any) -> Any:
- obj = pyobject_new(klass)
- obj.__init_handle_by_constructor__(ffi_new)
- return obj
-
- cls.__new__ = __new__ # type: ignore[attr-defined]
# __init__
# ┌─────────────────────┬──────────────────────┬──────────────────────┐
@@ -282,25 +277,16 @@ def _install_dataclass_dunders( # noqa: PLR0912, PLR0915
# ├─────────────────────┼──────────────────────┼──────────────────────┤
# │ c_class, init=True │ keep as-is │ _make_init │
# │ c_class, init=False │ keep as-is │ TypeError guard │
- # │ py_class, init=True │ keep as-is │ _make_init(inplace) │
- # │ py_class, init=False│ keep as-is │ TypeError guard │
+ # │ py_class, init=True │ wrap chandle guard │ _make_init(py_class) │
+ # │ py_class, init=False│ wrap chandle guard │ TypeError guard │
# └─────────────────────┴──────────────────────┴──────────────────────┘
if "__init__" not in cls.__dict__:
- if init and py_class_mode and ffi_init_inplace is not None:
- # py_class init=True: delegate to C++ __ffi_init_inplace__
- cls.__init__ = _make_init( # type: ignore[attr-defined]
- cls,
- type_info,
- ffi_init=ffi_init_inplace,
- inplace=True,
- )
- elif init and not py_class_mode and ffi_init is not None:
- # c_class init=True: delegate to __ffi_init__ via TypeAttrColumn
+ if init and ffi_init is not None:
cls.__init__ = _make_init( # type: ignore[attr-defined]
cls,
type_info,
ffi_init=ffi_init,
- inplace=False,
+ py_class_mode=py_class_mode,
)
elif not init:
# init=False, no user __init__: TypeError guard
@@ -315,6 +301,24 @@ def _install_dataclass_dunders( # noqa: PLR0912, PLR0915
__init___.__qualname__ = f"{cls.__qualname__}.__init__"
__init___.__module__ = cls.__module__
cls.__init__ = __init___ # type: ignore[attr-defined]
+ elif py_class_mode and ffi_new is not None:
+ # User-defined __init__: wrap with chandle guard so the C++ object
+ # is allocated (via __ffi_new__) before the user's code runs.
+ # We use __ffi_new__ (zero-arg allocator) rather than __ffi_init__
+ # because the user's __init__ signature may not match the field
+ # layout. super().__init__() from within the user's code becomes
+ # a no-op because chandle is already set.
+ import functools # noqa: PLC0415
+
+ user_init = cls.__dict__["__init__"]
+
+ @functools.wraps(user_init)
+ def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
+ if self.__chandle__() == 0:
+ self.__init_handle_by_constructor__(ffi_new)
+ user_init(self, *args, **kwargs)
+
+ cls.__init__ = __init__ # type: ignore[attr-defined]
# __repr__
if repr and "__repr__" not in cls.__dict__:
diff --git a/python/tvm_ffi/dataclasses/py_class.py
b/python/tvm_ffi/dataclasses/py_class.py
index a85965c..50c4510 100644
--- a/python/tvm_ffi/dataclasses/py_class.py
+++ b/python/tvm_ffi/dataclasses/py_class.py
@@ -422,7 +422,7 @@ _FFI_TYPE_ATTR_NAMES: frozenset[str] = frozenset(
#: TypeAttrColumn entries; all other names are registered as TypeMethod.
#:
#: System-managed names (``__ffi_new__``, ``__ffi_init__``,
-#: ``__ffi_init_inplace__``, ``__ffi_shallow_copy__``) are intentionally
+#: ``__ffi_shallow_copy__``) are intentionally
#: absent because the C++ runtime generates them.
_FFI_RECOGNIZED_METHODS: frozenset[str] = _FFI_TYPE_ATTR_NAMES
diff --git a/python/tvm_ffi/registry.py b/python/tvm_ffi/registry.py
index fb31bcf..0d0208a 100644
--- a/python/tvm_ffi/registry.py
+++ b/python/tvm_ffi/registry.py
@@ -381,7 +381,6 @@ def _install_init(cls: type, type_info: TypeInfo) -> None:
cls,
type_info,
ffi_init=ffi_init,
- inplace=False,
)
elif issubclass(cls, core.Object):
type_name = cls.__name__
diff --git a/src/ffi/extra/dataclass.cc b/src/ffi/extra/dataclass.cc
index 23e7728..2fcb702 100644
--- a/src/ffi/extra/dataclass.cc
+++ b/src/ffi/extra/dataclass.cc
@@ -1827,7 +1827,7 @@ Function MakeFieldSetter(int32_t field_type_index,
int64_t type_converter_int,
}
// ============================================================================
-// Shared helpers for MakeInit / MakeInitInplace
+// Shared helpers for MakeInit
// ============================================================================
/*!
@@ -1992,39 +1992,22 @@ Function MakeInit(int32_t type_index) {
});
}
-Function MakeInitInplace(int32_t type_index) {
- auto info = BuildAutoInitInfo(type_index);
- return Function::FromPacked([info](PackedArgs args, Any* rv) {
- TVM_FFI_ICHECK(args.size() >= 1)
- << "__ffi_init_inplace__ requires at least one argument (self)";
- ObjectRef self = args[0].cast<ObjectRef>();
- Object* obj = const_cast<Object*>(self.get());
- const auto raw_args = reinterpret_cast<const TVMFFIAny*>(args.data());
- BindFieldArgs(obj, *info, raw_args + 1, args.size() - 1);
- });
-}
-
void RegisterFFIInit(int32_t type_index) {
namespace refl = ::tvm::ffi::reflection;
Function auto_init_fn = MakeInit(type_index);
TVMFFIByteArray attr_name = refl::AsByteArray(refl::type_attr::kInit);
TVMFFIAny attr_value = AnyView(auto_init_fn).CopyToTVMFFIAny();
TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterAttr(type_index, &attr_name,
&attr_value));
-
- Function init_inplace_fn = MakeInitInplace(type_index);
- TVMFFIByteArray ip_attr_name =
refl::AsByteArray(refl::type_attr::kInitInplace);
- TVMFFIAny ip_attr_value = AnyView(init_inplace_fn).CopyToTVMFFIAny();
- TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterAttr(type_index, &ip_attr_name,
&ip_attr_value));
}
/*!
* \brief Combined registration for Python-defined types:
- * ``__ffi_init__``, ``__ffi_init_inplace__``, ``__ffi_new__``,
``__ffi_shallow_copy__``
+ * ``__ffi_init__``, ``__ffi_new__``, ``__ffi_shallow_copy__``
*/
void PyClassRegisterTypeAttrColumns(int32_t type_index, int32_t total_size) {
namespace refl = ::tvm::ffi::reflection;
const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(type_index);
- // Step 1. Register `__ffi_init__` and `__ffi_init_inplace__`
+ // Step 1. Register `__ffi_init__`
RegisterFFIInit(type_index);
// Step 2. Register `__ffi_new__`
Function new_fn = Function::FromTyped([type_index, total_size]() ->
ObjectRef {
@@ -2148,7 +2131,6 @@ TVM_FFI_STATIC_INIT_BLOCK() {
refl::EnsureTypeAttrColumn(refl::type_attr::kCompare);
refl::EnsureTypeAttrColumn(refl::type_attr::kNew);
refl::EnsureTypeAttrColumn(refl::type_attr::kInit);
- refl::EnsureTypeAttrColumn(refl::type_attr::kInitInplace);
refl::GlobalDef()
.def("ffi._RegisterFFIInit", RegisterFFIInit)
.def("ffi.MakeFieldSetter", MakeFieldSetter)
diff --git a/tests/python/test_dataclass_init.py
b/tests/python/test_dataclass_init.py
index b39c73b..639c180 100644
--- a/tests/python/test_dataclass_init.py
+++ b/tests/python/test_dataclass_init.py
@@ -944,65 +944,6 @@ class TestKwOnlyErrorMessages:
_ffi_init(obj, 1)
-# ###########################################################################
-# __ffi_init_inplace__ protocol tests
-# ###########################################################################
-
-
-def _ffi_init_inplace(obj: Any, *args: Any) -> None:
- """Look up __ffi_init_inplace__ from TypeAttrColumn and call it
directly."""
- type_index = type(obj).__tvm_ffi_type_info__.type_index
- fn = core._lookup_type_attr(type_index, "__ffi_init_inplace__")
- assert fn is not None, f"No __ffi_init_inplace__ for {type(obj)}"
- fn(obj, *args)
-
-
-class TestInitInplace:
- """Test the __ffi_init_inplace__ TypeAttrColumn."""
-
- def test_inplace_exists(self) -> None:
- type_index = getattr(_TestCxxAutoInit,
"__tvm_ffi_type_info__").type_index
- fn = core._lookup_type_attr(type_index, "__ffi_init_inplace__")
- assert fn is not None
-
- def test_inplace_positional(self) -> None:
- obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
- obj.__init_handle_by_constructor__(
- core._lookup_type_attr(type(obj).__tvm_ffi_type_info__.type_index,
"__ffi_new__")
- )
- _ffi_init_inplace(obj, 10, core.KWARGS, "c", 30)
- assert obj.a == 10
- assert obj.b == 42 # default
- assert obj.c == 30
- assert obj.d == 99 # default
-
- def test_inplace_all_kwargs(self) -> None:
- obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
- obj.__init_handle_by_constructor__(
- core._lookup_type_attr(type(obj).__tvm_ffi_type_info__.type_index,
"__ffi_new__")
- )
- _ffi_init_inplace(obj, core.KWARGS, "a", 1, "c", 2, "d", 3)
- assert obj.a == 1
- assert obj.c == 2
- assert obj.d == 3
-
- def test_inplace_kw_only_rejects_positional(self) -> None:
- obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
- obj.__init_handle_by_constructor__(
- core._lookup_type_attr(type(obj).__tvm_ffi_type_info__.type_index,
"__ffi_new__")
- )
- with pytest.raises(TypeError):
- _ffi_init_inplace(obj, 1, 2)
-
- def test_inplace_missing_kw_only_error(self) -> None:
- obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
- obj.__init_handle_by_constructor__(
- core._lookup_type_attr(type(obj).__tvm_ffi_type_info__.type_index,
"__ffi_new__")
- )
- with pytest.raises(TypeError, match="keyword-only"):
- _ffi_init_inplace(obj, 1)
-
-
# ###########################################################################
# __ffi_init__ TypeMethod registration regression tests
#
diff --git a/tests/python/test_dataclass_py_class.py
b/tests/python/test_dataclass_py_class.py
index 4078bf5..0b9d7db 100644
--- a/tests/python/test_dataclass_py_class.py
+++ b/tests/python/test_dataclass_py_class.py
@@ -5046,12 +5046,12 @@ class TestDtypeDeviceFields:
# ###########################################################################
-# kw_only regression tests (py_class via __ffi_init_inplace__)
+# kw_only regression tests (py_class via __ffi_init__)
# ###########################################################################
class TestPyClassKwOnlyRegression:
- """Regression tests ensuring kw_only enforcement via C++
__ffi_init_inplace__."""
+ """Regression tests ensuring kw_only enforcement via C++ __ffi_init__."""
def test_missing_kw_only_error_says_keyword_only(self) -> None:
"""Missing required kw_only field produces 'keyword-only' in the
error."""
@@ -5163,3 +5163,134 @@ class TestPyClassKwOnlyRegression:
with pytest.raises(TypeError, match="keyword-only"):
KwChild(1) # ty: ignore[missing-argument]
+
+
+# ###########################################################################
+# No-leak / no-spurious-allocation tests for py_class
+# ###########################################################################
+
+
+class TestPyClassNoLeak:
+ """Verify that the removal of custom __new__ eliminates the memory leak.
+
+ The original bug: ``@py_class`` installed a custom ``__new__`` that called
+ ``__ffi_new__`` to pre-allocate a C++ object. When a py_class object was
+ returned from C++ through ``make_ret_object``, ``cls.__new__(cls)``
+ triggered that custom ``__new__``, allocating a spurious C++ object whose
+ refcount was never decremented.
+ """
+
+ def test_no_custom_new_on_py_class(self) -> None:
+ """py_class must NOT install a custom __new__."""
+
+ @py_class(_unique_key("NoNew"))
+ class NoNew(Object):
+ x: int
+
+ assert "__new__" not in NoNew.__dict__
+
+ def test_no_custom_new_with_user_init(self) -> None:
+ """py_class with user-defined __init__ must NOT install a custom
__new__."""
+
+ @py_class(_unique_key("NoNewUI"), init=False)
+ class NoNewUI(Object):
+ x: int
+
+ def __init__(self, x: int) -> None:
+ self.x = x
+
+ assert "__new__" not in NoNewUI.__dict__
+
+ def test_make_ret_no_spurious_alloc(self) -> None:
+ """Objects returned from C++ (via DeepCopy) must not trigger spurious
allocation."""
+
+ @py_class(_unique_key("RetTest"))
+ class RetTest(Object):
+ x: int
+
+ obj = RetTest(42)
+ # DeepCopy returns through make_ret_object
+ obj2 = DeepCopy(obj)
+ assert obj2.x == 42
+ assert not obj.same_as(obj2)
+
+ def test_repeated_roundtrip_no_leak(self) -> None:
+ """Repeated construct + DeepCopy cycles must not leak."""
+
+ @py_class(_unique_key("RoundTrip"))
+ class RoundTrip(Object):
+ x: int
+ y: str = "default"
+
+ gc.collect()
+ for i in range(1000):
+ o = RoundTrip(i, str(i))
+ o2 = DeepCopy(o)
+ del o, o2
+ gc.collect()
+
+ def test_user_init_wraps_metadata(self) -> None:
+ """Wrapped user __init__ preserves docstring and __wrapped__."""
+
+ @py_class(_unique_key("WrapMeta"), init=False)
+ class WrapMeta(Object):
+ x: int
+
+ def __init__(self, x: int) -> None:
+ """Initialize WrapMeta."""
+ self.x = x
+
+ assert WrapMeta.__init__.__doc__ == "Initialize WrapMeta."
+ assert hasattr(WrapMeta.__init__, "__wrapped__")
+
+ def test_chandle_guard_skips_on_make_ret(self) -> None:
+ """Auto-generated __init__ with chandle guard: make_ret never calls
__init__."""
+
+ @py_class(_unique_key("ChandleGuard"))
+ class ChandleGuard(Object):
+ x: int
+
+ obj = ChandleGuard(7)
+ obj2 = DeepCopy(obj)
+ # If __init__ were called by make_ret, it would fail (no args)
+ # or overwrite fields. Verify the value survived intact.
+ assert obj2.x == 7
+
+ def test_super_init_noop_after_ffi_init(self) -> None:
+ """super().__init__() is a no-op when chandle is already set."""
+
+ @py_class(_unique_key("SuperBase"))
+ class SuperBase(Object):
+ pass
+
+ call_log: list[str] = []
+
+ @py_class(_unique_key("SuperChild"), init=False)
+ class SuperChild(SuperBase):
+ x: int
+
+ def __init__(self, x: int) -> None:
+ call_log.append("before_super")
+ super().__init__()
+ call_log.append("after_super")
+ self.x = x
+
+ obj = SuperChild(42)
+ assert obj.x == 42
+ assert call_log == ["before_super", "after_super"]
+
+ def test_user_init_mismatched_signature(self) -> None:
+ """User __init__ whose args don't match field layout still works."""
+
+ @py_class(_unique_key("Mismatch"), init=False)
+ class Mismatch(Object):
+ value: int
+ ref: Optional[Object]
+
+ def __init__(self, val: int) -> None:
+ self.value = val
+ self.ref = None
+
+ obj = Mismatch(99)
+ assert obj.value == 99
+ assert obj.ref is None