blob: f945b39dd111bf964e5471e621c0f2627bdaf18f [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: D102, UP006, UP035, UP045
"""Tests for __copy__, __deepcopy__, and __replace__ on FFI objects."""
from __future__ import annotations
import copy
import itertools
import pickle
from typing import Dict, List, Optional
import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi._ffi_api import DeepCopy
from tvm_ffi.core import Object
from tvm_ffi.dataclasses import py_class
_counter_pc = itertools.count()
def _unique_key_pc(base: str) -> str:
return f"testing.copy_pc.{base}_{next(_counter_pc)}"
# --------------------------------------------------------------------------- #
# __copy__
# --------------------------------------------------------------------------- #
class TestShallowCopy:
"""Tests for copy.copy() / __copy__."""
def test_basic_fields(self) -> None:
pair = tvm_ffi.testing.TestIntPair(1, 2)
pair_copy = copy.copy(pair)
assert pair_copy.a == 1
assert pair_copy.b == 2
def test_creates_new_object(self) -> None:
pair = tvm_ffi.testing.TestIntPair(3, 7)
pair_copy = copy.copy(pair)
assert not pair.same_as(pair_copy)
def test_mutable_fields(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=42, v_str="hello")
obj_copy = copy.copy(obj)
assert obj_copy.v_i64 == 42 # ty: ignore[unresolved-attribute]
assert obj_copy.v_str == "hello" # ty: ignore[unresolved-attribute]
assert obj_copy.v_f64 == 10.0 # ty: ignore[unresolved-attribute]
assert not obj.same_as(obj_copy)
def test_mutating_copy_does_not_affect_original(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=1, v_str="a")
obj_copy = copy.copy(obj)
obj_copy.v_i64 = 99 # ty: ignore[unresolved-attribute]
obj_copy.v_str = "z" # ty: ignore[unresolved-attribute]
assert obj.v_i64 == 1 # ty: ignore[unresolved-attribute]
assert obj.v_str == "a" # ty: ignore[unresolved-attribute]
def test_derived_type_preserves_type(self) -> None:
v_map = tvm_ffi.convert({"k": 1})
v_array = tvm_ffi.convert([1, 2])
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=5,
v_map=v_map,
v_array=v_array,
)
obj_copy = copy.copy(obj)
assert not obj.same_as(obj_copy)
assert isinstance(obj_copy, tvm_ffi.testing.TestObjectDerived)
assert obj_copy.v_i64 == 5
# shallow copy shares sub-objects
assert obj_copy.v_map.same_as(obj.v_map) # ty: ignore[unresolved-attribute]
assert obj_copy.v_array.same_as(obj.v_array) # ty: ignore[unresolved-attribute]
def test_auto_copy_for_cxx_class(self) -> None:
# _TestCxxClassBase is copy-constructible, so copy is auto-enabled
# Note: _TestCxxClassBase.__init__ adds 1 to v_i64 and 2 to v_i32
obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
obj_copy = copy.copy(obj)
assert obj_copy.v_i64 == 2
assert obj_copy.v_i32 == 4
assert not obj.same_as(obj_copy)
def test_non_copyable_type_raises(self) -> None:
obj = tvm_ffi.testing.TestNonCopyable(42)
with pytest.raises(TypeError, match="does not support copy"):
copy.copy(obj)
# --------------------------------------------------------------------------- #
# __deepcopy__
# --------------------------------------------------------------------------- #
class TestDeepCopy:
"""Tests for copy.deepcopy() / __deepcopy__."""
def test_basic_fields(self) -> None:
pair = tvm_ffi.testing.TestIntPair(5, 10)
pair_deep = copy.deepcopy(pair)
assert pair_deep.a == 5
assert pair_deep.b == 10
assert not pair.same_as(pair_deep)
def test_nested_objects_are_copied(self) -> None:
inner = tvm_ffi.testing.TestIntPair(1, 2)
v_array = tvm_ffi.convert([inner])
v_map = tvm_ffi.convert({"x": "y"})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=10,
v_map=v_map,
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
# top-level is a new object
assert not obj.same_as(obj_deep)
# nested TestIntPair should also be a new object
assert not obj.v_array[0].same_as(obj_deep.v_array[0]) # ty: ignore[unresolved-attribute]
# but values are preserved
assert obj_deep.v_array[0].a == 1 # ty: ignore[unresolved-attribute]
assert obj_deep.v_array[0].b == 2 # ty: ignore[unresolved-attribute]
def test_shared_references_preserved(self) -> None:
"""Two array slots pointing to the same object should still share after deepcopy."""
shared = tvm_ffi.testing.TestIntPair(7, 8)
v_array = tvm_ffi.convert([shared, shared])
v_map = tvm_ffi.convert({"a": "b"})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
assert obj.v_array[0].same_as(obj.v_array[1]) # ty: ignore[unresolved-attribute]
obj_deep = copy.deepcopy(obj)
# the copies should still share
assert obj_deep.v_array[0].same_as(obj_deep.v_array[1]) # ty: ignore[unresolved-attribute]
# but they must be distinct from the originals
assert not obj.v_array[0].same_as(obj_deep.v_array[0]) # ty: ignore[unresolved-attribute]
def test_shared_containers_preserved(self) -> None:
"""Two array slots pointing to the same container should still share after deepcopy."""
inner = tvm_ffi.convert([1, 2, 3])
v_array = tvm_ffi.convert([inner, inner])
v_map = tvm_ffi.convert({"a": "b"})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
assert obj.v_array[0].same_as(obj.v_array[1]) # ty: ignore[unresolved-attribute]
obj_deep = copy.deepcopy(obj)
# the copied containers should still share identity
assert obj_deep.v_array[0].same_as(obj_deep.v_array[1]) # ty: ignore[unresolved-attribute]
# but they must be distinct from the originals
assert not obj.v_array[0].same_as(obj_deep.v_array[0]) # ty: ignore[unresolved-attribute]
def test_original_untouched(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=42, v_str="original")
obj_deep = copy.deepcopy(obj)
obj_deep.v_i64 = 0 # ty: ignore[unresolved-attribute]
obj_deep.v_str = "modified" # ty: ignore[unresolved-attribute]
assert obj.v_i64 == 42 # ty: ignore[unresolved-attribute]
assert obj.v_str == "original" # ty: ignore[unresolved-attribute]
def test_self_referencing_cycle(self) -> None:
"""An object whose array field contains itself should deepcopy correctly."""
v_map = tvm_ffi.convert({"a": "b"})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=tvm_ffi.convert([]),
)
# Create self-reference: obj.v_array = [obj]
obj.v_array = tvm_ffi.convert([obj]) # ty: ignore[unresolved-attribute]
obj_deep = copy.deepcopy(obj)
assert not obj.same_as(obj_deep)
# The cycle should be preserved: copy -> copy.v_array[0] -> same copy
assert obj_deep.v_array[0].same_as(obj_deep) # ty: ignore[unresolved-attribute]
def test_mutual_reference_cycle(self) -> None:
"""Two objects referencing each other should deepcopy with cycle preserved."""
v_map = tvm_ffi.convert({"a": "b"})
obj_a = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=tvm_ffi.convert([]),
)
obj_b = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=2,
v_map=v_map,
v_array=tvm_ffi.convert([obj_a]),
)
# Close the cycle: A -> B and B -> A
obj_a.v_array = tvm_ffi.convert([obj_b]) # ty: ignore[unresolved-attribute]
deep_a = copy.deepcopy(obj_a)
assert not obj_a.same_as(deep_a)
# deep_a.v_array[0] is the copy of obj_b
deep_b = deep_a.v_array[0] # ty: ignore[unresolved-attribute]
assert not obj_b.same_as(deep_b)
# The cycle should be preserved: deep_a -> deep_b -> deep_a
assert deep_b.v_array[0].same_as(deep_a)
# Values are preserved
assert deep_a.v_i64 == 1 # ty: ignore[unresolved-attribute]
assert deep_b.v_i64 == 2
def test_array_root(self) -> None:
"""Deepcopy with a bare Array as root should create a new array."""
inner = tvm_ffi.testing.TestIntPair(1, 2)
arr = tvm_ffi.convert([inner, "hello", 42])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
# inner object is deep-copied
assert not arr[0].same_as(arr_deep[0])
assert arr_deep[0].a == 1
# primitives and strings preserved
assert arr_deep[1] == "hello"
assert arr_deep[2] == 42
def test_map_root(self) -> None:
"""Deepcopy with a bare Map as root should create a new map."""
inner = tvm_ffi.testing.TestIntPair(3, 4)
m = tvm_ffi.convert({"key": inner})
m_deep = copy.deepcopy(m)
assert not m.same_as(m_deep)
# inner object is deep-copied
assert not m["key"].same_as(m_deep["key"])
assert m_deep["key"].a == 3
def test_dict_root(self) -> None:
"""Deepcopy with a bare Dict as root should create a new dict."""
inner = tvm_ffi.testing.TestIntPair(3, 4)
d = tvm_ffi.Dict({"key": inner})
d_deep = copy.deepcopy(d)
assert not d.same_as(d_deep)
# inner object is deep-copied
assert not d["key"].same_as(d_deep["key"])
assert d_deep["key"].a == 3
def test_auto_deepcopy_for_cxx_class(self) -> None:
# _TestCxxClassBase is copy-constructible, so deepcopy is auto-enabled
# Note: _TestCxxClassBase.__init__ adds 1 to v_i64 and 2 to v_i32
obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
obj_deep = copy.deepcopy(obj)
assert obj_deep.v_i64 == 2
assert obj_deep.v_i32 == 4
assert not obj.same_as(obj_deep)
def test_non_copyable_type_raises(self) -> None:
obj = tvm_ffi.testing.TestNonCopyable(42)
with pytest.raises((TypeError, RuntimeError), match="not copy-constructible"):
copy.deepcopy(obj)
def test_long_string_in_array(self) -> None:
"""Strings exceeding inline threshold are heap-allocated objects.
deepcopy must treat them as immutable terminals, not call CopyObject.
"""
long_str = "a" * 100
arr = tvm_ffi.convert([long_str])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert arr_deep[0] == long_str
def test_long_string_in_object_field(self) -> None:
"""Heap-allocated string as a field value should survive deepcopy."""
long_str = "x" * 200
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=1, v_str=long_str)
obj_deep = copy.deepcopy(obj)
assert obj_deep.v_str == long_str # ty: ignore[unresolved-attribute]
def test_any_field_with_object(self) -> None:
"""Any-typed field containing an object must be recursively copied."""
inner = tvm_ffi.testing.TestIntPair(3, 4)
obj = tvm_ffi.testing.create_object("testing.TestDeepCopyEdges", v_any=inner, v_obj=inner)
obj_deep = copy.deepcopy(obj)
assert not obj.same_as(obj_deep)
assert not inner.same_as(obj_deep.v_any) # ty: ignore[unresolved-attribute]
assert obj_deep.v_any.a == 3 # ty: ignore[unresolved-attribute]
def test_any_field_with_array(self) -> None:
"""Any-typed field containing an Array must be recursively copied."""
inner_arr = tvm_ffi.convert([1, 2, 3])
obj = tvm_ffi.testing.create_object(
"testing.TestDeepCopyEdges", v_any=inner_arr, v_obj=inner_arr
)
obj_deep = copy.deepcopy(obj)
assert not inner_arr.same_as(obj_deep.v_any) # ty: ignore[unresolved-attribute]
assert list(obj_deep.v_any) == [1, 2, 3] # ty: ignore[unresolved-attribute]
def test_any_field_with_map(self) -> None:
"""Any-typed field containing a Map must be recursively copied."""
inner_map = tvm_ffi.convert({"k": "v"})
obj = tvm_ffi.testing.create_object(
"testing.TestDeepCopyEdges", v_any=inner_map, v_obj=inner_map
)
obj_deep = copy.deepcopy(obj)
assert not inner_map.same_as(obj_deep.v_any) # ty: ignore[unresolved-attribute]
assert obj_deep.v_any["k"] == "v" # ty: ignore[unresolved-attribute]
def test_objectref_field_with_array(self) -> None:
"""ObjectRef field holding runtime Array must go through Resolve."""
inner_arr = tvm_ffi.convert([10, 20])
obj = tvm_ffi.testing.create_object(
"testing.TestDeepCopyEdges", v_any=None, v_obj=inner_arr
)
obj_deep = copy.deepcopy(obj)
assert not inner_arr.same_as(obj_deep.v_obj) # ty: ignore[unresolved-attribute]
assert list(obj_deep.v_obj) == [10, 20] # ty: ignore[unresolved-attribute]
def test_objectref_field_with_map(self) -> None:
"""ObjectRef field holding runtime Map must go through Resolve."""
inner_map = tvm_ffi.convert({"a": 1})
obj = tvm_ffi.testing.create_object(
"testing.TestDeepCopyEdges", v_any=None, v_obj=inner_map
)
obj_deep = copy.deepcopy(obj)
assert not inner_map.same_as(obj_deep.v_obj) # ty: ignore[unresolved-attribute]
assert obj_deep.v_obj["a"] == 1 # ty: ignore[unresolved-attribute]
def test_any_field_sharing_preserved(self) -> None:
"""Shared references through Any and ObjectRef fields are preserved."""
shared = tvm_ffi.testing.TestIntPair(5, 6)
obj = tvm_ffi.testing.create_object("testing.TestDeepCopyEdges", v_any=shared, v_obj=shared)
obj_deep = copy.deepcopy(obj)
# Both fields should point to the same copied object
assert obj_deep.v_any.same_as(obj_deep.v_obj) # ty: ignore[unresolved-attribute]
assert not shared.same_as(obj_deep.v_any) # ty: ignore[unresolved-attribute]
# --------------------------------------------------------------------------- #
# Deep copy branch coverage (C++ dataclass.cc)
# --------------------------------------------------------------------------- #
_deep_copy = tvm_ffi.get_global_func("ffi.DeepCopy")
class TestDeepCopyBranches:
"""Branch-coverage tests targeting every code path in deep_copy.cc."""
# --- Run(): primitive passthrough (type_index < kStaticObjectBegin) ---
def test_primitive_int(self) -> None:
assert _deep_copy(42) == 42
def test_primitive_float(self) -> None:
result = _deep_copy(3.14)
assert abs(result - 3.14) < 1e-9
def test_primitive_str(self) -> None:
assert _deep_copy("hello") == "hello"
def test_primitive_none(self) -> None:
assert _deep_copy(None) is None
def test_primitive_bool(self) -> None:
assert _deep_copy(True) is True
assert _deep_copy(False) is False
# --- Resolve(): array with various element types ---
def test_array_all_ints(self) -> None:
"""All elements are primitives — Resolve() returns each as-is."""
arr = tvm_ffi.convert([1, 2, 3])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert list(arr_deep) == [1, 2, 3]
def test_array_all_strings(self) -> None:
arr = tvm_ffi.convert(["a", "bb", "ccc"])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert list(arr_deep) == ["a", "bb", "ccc"]
def test_array_with_none_elements(self) -> None:
arr = tvm_ffi.convert([None, 1, None])
arr_deep = copy.deepcopy(arr)
assert arr_deep[0] is None
assert arr_deep[1] == 1
assert arr_deep[2] is None
def test_array_mixed_primitive_types(self) -> None:
"""Array with int, float, str, bool, None — all primitives."""
arr = tvm_ffi.convert([42, 3.14, "hi", True, None])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert arr_deep[0] == 42
assert abs(arr_deep[1] - 3.14) < 1e-9
assert arr_deep[2] == "hi"
assert arr_deep[3] is True
assert arr_deep[4] is None
def test_array_mixed_with_objects_and_containers(self) -> None:
"""Array with int, str, None, object, nested array, nested map."""
inner_obj = tvm_ffi.testing.TestIntPair(1, 2)
inner_arr = tvm_ffi.convert([10, 20])
inner_map = tvm_ffi.convert({"k": "v"})
arr = tvm_ffi.convert([42, "hello", None, inner_obj, inner_arr, inner_map])
arr_deep = copy.deepcopy(arr)
# primitives pass through
assert arr_deep[0] == 42
assert arr_deep[1] == "hello"
assert arr_deep[2] is None
# object is deep-copied
assert not arr[3].same_as(arr_deep[3])
assert arr_deep[3].a == 1
# nested array is deep-copied
assert not arr[4].same_as(arr_deep[4])
assert list(arr_deep[4]) == [10, 20]
# nested map is deep-copied
assert not arr[5].same_as(arr_deep[5])
assert arr_deep[5]["k"] == "v"
def test_array_empty(self) -> None:
arr = tvm_ffi.convert([])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert len(arr_deep) == 0
def test_array_nested_arrays(self) -> None:
"""Array of arrays — Resolve() recurses into each nested array."""
a = tvm_ffi.convert([1, 2])
b = tvm_ffi.convert([3, 4])
outer = tvm_ffi.convert([a, b])
outer_deep = copy.deepcopy(outer)
assert not outer.same_as(outer_deep)
assert not outer[0].same_as(outer_deep[0])
assert not outer[1].same_as(outer_deep[1])
assert list(outer_deep[0]) == [1, 2]
assert list(outer_deep[1]) == [3, 4]
def test_array_nested_maps(self) -> None:
"""Array of maps."""
m = tvm_ffi.convert({"x": 1})
arr = tvm_ffi.convert([m])
arr_deep = copy.deepcopy(arr)
assert not arr[0].same_as(arr_deep[0])
assert arr_deep[0]["x"] == 1
# --- Resolve(): map with various key/value types ---
def test_map_primitive_keys_and_values(self) -> None:
m = tvm_ffi.convert({"a": 1, "b": 2, "c": 3})
m_deep = copy.deepcopy(m)
assert not m.same_as(m_deep)
assert m_deep["a"] == 1
assert m_deep["b"] == 2
assert m_deep["c"] == 3
def test_map_with_container_values(self) -> None:
inner_arr = tvm_ffi.convert([1, 2])
m = tvm_ffi.convert({"arr": inner_arr})
m_deep = copy.deepcopy(m)
assert not m["arr"].same_as(m_deep["arr"])
assert list(m_deep["arr"]) == [1, 2]
def test_map_with_none_values(self) -> None:
m = tvm_ffi.convert({"a": None, "b": 1})
m_deep = copy.deepcopy(m)
assert not m.same_as(m_deep)
assert m_deep["a"] is None
assert m_deep["b"] == 1
def test_map_empty(self) -> None:
m = tvm_ffi.convert({})
m_deep = copy.deepcopy(m)
assert not m.same_as(m_deep)
assert len(m_deep) == 0
# --- Resolve(): dict with various key/value types ---
def test_dict_primitive_keys_and_values(self) -> None:
d = tvm_ffi.Dict({"a": 1, "b": 2, "c": 3})
d_deep = copy.deepcopy(d)
assert not d.same_as(d_deep)
assert d_deep["a"] == 1
assert d_deep["b"] == 2
assert d_deep["c"] == 3
def test_dict_with_container_values(self) -> None:
inner_arr = tvm_ffi.convert([1, 2])
d = tvm_ffi.Dict({"arr": inner_arr})
d_deep = copy.deepcopy(d)
assert not d["arr"].same_as(d_deep["arr"])
assert list(d_deep["arr"]) == [1, 2]
def test_dict_with_none_values(self) -> None:
d = tvm_ffi.Dict({"a": None, "b": 1})
d_deep = copy.deepcopy(d)
assert not d.same_as(d_deep)
assert d_deep["a"] is None
assert d_deep["b"] == 1
def test_dict_empty(self) -> None:
d = tvm_ffi.Dict({})
d_deep = copy.deepcopy(d)
assert not d.same_as(d_deep)
assert len(d_deep) == 0
# --- Resolve(): copy_map_ hit (shared references across containers) ---
def test_shared_array_identity_in_outer_array(self) -> None:
"""Same array appears 3 times — all copies share identity."""
shared = tvm_ffi.convert([1, 2])
outer = tvm_ffi.convert([shared, shared, shared])
outer_deep = copy.deepcopy(outer)
assert outer_deep[0].same_as(outer_deep[1])
assert outer_deep[1].same_as(outer_deep[2])
assert not outer[0].same_as(outer_deep[0])
def test_shared_map_identity_in_outer_array(self) -> None:
shared = tvm_ffi.convert({"x": 1})
outer = tvm_ffi.convert([shared, shared])
outer_deep = copy.deepcopy(outer)
assert outer_deep[0].same_as(outer_deep[1])
assert not outer[0].same_as(outer_deep[0])
def test_shared_dict_identity_in_outer_array(self) -> None:
shared = tvm_ffi.Dict({"x": 1})
outer = tvm_ffi.convert([shared, shared])
outer_deep = copy.deepcopy(outer)
assert outer_deep[0].same_as(outer_deep[1])
assert not outer[0].same_as(outer_deep[0])
def test_shared_object_across_array_and_map(self) -> None:
"""Same object referenced from both v_array and v_map."""
pair = tvm_ffi.testing.TestIntPair(7, 8)
v_array = tvm_ffi.convert([pair])
v_map = tvm_ffi.convert({"p": pair})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
# both fields should refer to the same copied object
deep_from_arr = obj_deep.v_array[0] # ty: ignore[unresolved-attribute]
deep_from_map = obj_deep.v_map["p"] # ty: ignore[unresolved-attribute]
assert deep_from_arr.same_as(deep_from_map)
assert not pair.same_as(deep_from_arr)
# --- ResolveFields: container field with only primitives ---
# Resolve() always rebuilds the container, so setter is always called.
def test_field_array_only_primitives(self) -> None:
v_array = tvm_ffi.convert([1, 2, 3])
v_map = tvm_ffi.convert({"k": "v"})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
assert not obj.v_array.same_as(obj_deep.v_array) # ty: ignore[unresolved-attribute]
assert list(obj_deep.v_array) == [1, 2, 3] # ty: ignore[unresolved-attribute]
def test_field_map_only_primitives(self) -> None:
v_array = tvm_ffi.convert([])
v_map = tvm_ffi.convert({"x": 1, "y": 2})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
assert not obj.v_map.same_as(obj_deep.v_map) # ty: ignore[unresolved-attribute]
assert obj_deep.v_map["x"] == 1 # ty: ignore[unresolved-attribute]
assert obj_deep.v_map["y"] == 2 # ty: ignore[unresolved-attribute]
# --- ResolveFields: empty container fields ---
def test_field_empty_containers(self) -> None:
v_array = tvm_ffi.convert([])
v_map = tvm_ffi.convert({})
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=v_map,
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
assert not obj.v_array.same_as(obj_deep.v_array) # ty: ignore[unresolved-attribute]
assert not obj.v_map.same_as(obj_deep.v_map) # ty: ignore[unresolved-attribute]
assert len(obj_deep.v_array) == 0 # ty: ignore[unresolved-attribute]
assert len(obj_deep.v_map) == 0 # ty: ignore[unresolved-attribute]
# --- ResolveFields: shared container across multiple objects ---
def test_shared_container_field_across_objects(self) -> None:
"""Two objects share the same v_array — copy_map_ deduplicates."""
shared_arr = tvm_ffi.convert([1, 2, 3])
obj_a = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=tvm_ffi.convert({}),
v_array=shared_arr,
)
obj_b = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=2,
v_map=tvm_ffi.convert({}),
v_array=shared_arr,
)
outer = tvm_ffi.convert([obj_a, obj_b])
outer_deep = copy.deepcopy(outer)
deep_a = outer_deep[0]
deep_b = outer_deep[1]
# both should share the same deep-copied array
assert deep_a.v_array.same_as(deep_b.v_array)
assert not shared_arr.same_as(deep_a.v_array)
# --- CopyObject: unsupported type nested in container ---
def test_cxx_class_in_array(self) -> None:
# Note: _TestCxxClassBase.__init__ adds 1 to v_i64 and 2 to v_i32
obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
arr = tvm_ffi.convert([obj])
arr_deep = copy.deepcopy(arr)
assert not arr.same_as(arr_deep)
assert not arr[0].same_as(arr_deep[0])
assert arr_deep[0].v_i64 == 2
assert arr_deep[0].v_i32 == 4
def test_cxx_class_in_map_value(self) -> None:
# Note: _TestCxxClassBase.__init__ adds 1 to v_i64 and 2 to v_i32
obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
m = tvm_ffi.convert({"k": obj})
m_deep = copy.deepcopy(m)
assert not m.same_as(m_deep)
assert not m["k"].same_as(m_deep["k"])
assert m_deep["k"].v_i64 == 2
assert m_deep["k"].v_i32 == 4
def test_non_copyable_type_in_array(self) -> None:
obj = tvm_ffi.testing.TestNonCopyable(1)
arr = tvm_ffi.convert([obj])
with pytest.raises(RuntimeError, match="not copy-constructible"):
copy.deepcopy(arr)
def test_non_copyable_type_in_map_value(self) -> None:
obj = tvm_ffi.testing.TestNonCopyable(1)
m = tvm_ffi.convert({"k": obj})
with pytest.raises(RuntimeError, match="not copy-constructible"):
copy.deepcopy(m)
# --- Deeply nested structures ---
def test_deeply_nested_containers(self) -> None:
"""Array > Map > Array > object — all levels resolved."""
pair = tvm_ffi.testing.TestIntPair(9, 10)
inner_arr = tvm_ffi.convert([pair])
inner_map = tvm_ffi.convert({"items": inner_arr})
outer = tvm_ffi.convert([inner_map])
outer_deep = copy.deepcopy(outer)
deep_pair = outer_deep[0]["items"][0]
assert not pair.same_as(deep_pair)
assert deep_pair.a == 9
assert deep_pair.b == 10
def test_object_with_deeply_nested_field(self) -> None:
"""Object whose array field contains a map containing an object."""
pair = tvm_ffi.testing.TestIntPair(5, 6)
inner_map = tvm_ffi.convert({"pair": pair})
v_array = tvm_ffi.convert([inner_map])
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=tvm_ffi.convert({}),
v_array=v_array,
)
obj_deep = copy.deepcopy(obj)
deep_pair = obj_deep.v_array[0]["pair"] # ty: ignore[unresolved-attribute]
assert not pair.same_as(deep_pair)
assert deep_pair.a == 5
# --- Cycle preservation with immutable root containers ---
def test_cycle_list_root_map_backref_preserved(self) -> None:
"""Control case: List root with Map back-reference should preserve cycle."""
root_list = tvm_ffi.List()
m = tvm_ffi.Map({"list": root_list})
root_list.append(m)
deep_list = copy.deepcopy(root_list)
assert not root_list.same_as(deep_list)
assert deep_list[0]["list"].same_as(deep_list)
def test_cycle_map_root_list_backref_preserved(self) -> None:
"""Map root with List child pointing back should preserve cycle to root copy."""
l = tvm_ffi.List()
m = tvm_ffi.Map({"list": l})
l.append(m)
deep_map = copy.deepcopy(m)
assert not m.same_as(deep_map)
assert not l.same_as(deep_map["list"])
assert deep_map["list"][0].same_as(deep_map)
def test_cycle_array_root_list_backref_preserved(self) -> None:
"""Array root with List child pointing back should preserve cycle to root copy."""
l = tvm_ffi.List()
a = tvm_ffi.Array([l])
l.append(a)
deep_arr = copy.deepcopy(a)
assert not a.same_as(deep_arr)
assert not l.same_as(deep_arr[0])
assert deep_arr[0][0].same_as(deep_arr)
def test_cycle_array_root_dict_backref_preserved(self) -> None:
"""Array root with Dict child pointing back should preserve cycle to root copy."""
d = tvm_ffi.Dict()
a = tvm_ffi.Array([d])
d["self"] = a
deep_arr = copy.deepcopy(a)
assert not a.same_as(deep_arr)
assert not d.same_as(deep_arr[0])
assert deep_arr[0]["self"].same_as(deep_arr)
def test_cycle_map_root_dict_backref_preserved(self) -> None:
"""Map root with Dict child pointing back should preserve cycle to root copy."""
d = tvm_ffi.Dict()
m = tvm_ffi.Map({"dict": d})
d["self"] = m
deep_map = copy.deepcopy(m)
assert not m.same_as(deep_map)
assert not d.same_as(deep_map["dict"])
assert deep_map["dict"]["self"].same_as(deep_map)
def test_cycle_map_root_backref_identity_not_duplicated(self) -> None:
"""Back-references in a map-root cycle should point to the root copied map."""
shared_list = tvm_ffi.List()
m = tvm_ffi.Map({"l1": shared_list, "l2": shared_list})
shared_list.append(m)
deep_map = copy.deepcopy(m)
assert deep_map["l1"].same_as(deep_map["l2"])
assert deep_map["l1"][0].same_as(deep_map)
def test_cycle_map_root_list_key_backref_preserved(self) -> None:
"""Map-root cycles through keys should preserve back-reference to copied root."""
key_list = tvm_ffi.List()
m = tvm_ffi.Map({key_list: 1})
key_list.append(m)
deep_map = copy.deepcopy(m)
deep_key = next(iter(deep_map.keys()))
assert isinstance(deep_key, tvm_ffi.List)
assert deep_key[0].same_as(deep_map)
def test_cycle_map_root_dict_key_backref_preserved(self) -> None:
"""Map-root cycles through Dict keys should preserve back-reference to copied root."""
key_dict = tvm_ffi.Dict()
m = tvm_ffi.Map({key_dict: 1})
key_dict["self"] = m
deep_map = copy.deepcopy(m)
deep_key = next(iter(deep_map.keys()))
assert isinstance(deep_key, tvm_ffi.Dict)
assert deep_key["self"].same_as(deep_map)
def test_cycle_array_root_dict_contains_root_as_key(self) -> None:
"""Array root with Dict child using the root as key should fix key to copied root."""
d = tvm_ffi.Dict()
root = tvm_ffi.Array([d])
d[root] = 1
deep_root = copy.deepcopy(root)
deep_dict = deep_root[0]
deep_key = next(iter(deep_dict.keys()))
assert not root.same_as(deep_root)
assert deep_key.same_as(deep_root)
assert not deep_key.same_as(root)
def test_cycle_map_root_dict_contains_root_as_key(self) -> None:
"""Map root with Dict child using the root as key should fix key to copied root."""
d = tvm_ffi.Dict()
root = tvm_ffi.Map({"d": d})
d[root] = 1
deep_root = copy.deepcopy(root)
deep_dict = deep_root["d"]
deep_key = next(iter(deep_dict.keys()))
assert not root.same_as(deep_root)
assert deep_key.same_as(deep_root)
assert not deep_key.same_as(root)
# --- Python deepcopy protocol consistency for immutable Shape ---
def test_shape_root_python_deepcopy_matches_ffi_deepcopy(self) -> None:
"""copy.deepcopy(Shape) should be consistent with ffi.DeepCopy."""
deep_copy_fn = tvm_ffi.get_global_func("ffi.DeepCopy")
s = tvm_ffi.Shape((2, 3, 4))
ffi_copied = deep_copy_fn(s)
py_copied = copy.deepcopy(s)
assert py_copied == ffi_copied
assert isinstance(py_copied, type(s))
def test_shape_inside_python_container_deepcopy(self) -> None:
"""Python container deepcopy should handle Shape payloads."""
s = tvm_ffi.Shape((1, 2))
payload = [s, {"shape": s}]
copied = copy.deepcopy(payload)
assert copied[0] == s
assert copied[1]["shape"] == s # ty: ignore[invalid-argument-type]
# --- Cycle fixup: immutable container → reflected object back-reference ---
def test_cycle_array_root_object_backreference(self) -> None:
"""Array A → Object X, X.v_array = A. Deep copy from A."""
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=42,
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([]),
)
arr = tvm_ffi.Array([obj])
obj.v_array = arr # ty: ignore[unresolved-attribute]
arr_deep = _deep_copy(arr)
assert not arr.same_as(arr_deep)
obj_deep = arr_deep[0]
assert not obj.same_as(obj_deep)
assert obj_deep.v_i64 == 42
assert not obj_deep.v_array.same_as(arr)
assert obj_deep.v_array.same_as(arr_deep)
def test_cycle_map_root_object_backreference(self) -> None:
"""Map M → Object X, X.v_map = M. Deep copy from M."""
obj = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=7,
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([]),
)
m = tvm_ffi.Map({"key": obj})
obj.v_map = m # ty: ignore[unresolved-attribute]
m_deep = _deep_copy(m)
assert not m.same_as(m_deep)
obj_deep = m_deep["key"]
assert not obj.same_as(obj_deep)
assert obj_deep.v_i64 == 7
assert not obj_deep.v_map.same_as(m)
assert obj_deep.v_map.same_as(m_deep)
def test_cycle_nested_array_object_array(self) -> None:
"""Array → Object → Array → Object → back to root Array."""
inner = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=1,
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([]),
)
outer = tvm_ffi.testing.create_object(
"testing.TestObjectDerived",
v_i64=2,
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([inner]),
)
root_arr = tvm_ffi.Array([outer])
inner.v_array = root_arr # ty: ignore[unresolved-attribute]
root_deep = _deep_copy(root_arr)
assert not root_arr.same_as(root_deep)
outer_deep = root_deep[0]
inner_deep = outer_deep.v_array[0]
assert not inner_deep.v_array.same_as(root_arr)
assert inner_deep.v_array.same_as(root_deep)
# --------------------------------------------------------------------------- #
# __replace__
# --------------------------------------------------------------------------- #
class TestReplace:
"""Tests for __replace__."""
def test_replace_writable_fields(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=1, v_str="a")
obj2 = obj.__replace__(v_i64=99) # ty: ignore[unresolved-attribute]
assert obj2.v_i64 == 99
assert obj2.v_str == "a"
assert not obj.same_as(obj2)
def test_replace_multiple_fields(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=1, v_str="a")
obj2 = obj.__replace__(v_i64=42, v_str="world") # ty: ignore[unresolved-attribute]
assert obj2.v_i64 == 42
assert obj2.v_str == "world"
def test_replace_no_kwargs_is_copy(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=7, v_str="hi")
obj2 = obj.__replace__() # ty: ignore[unresolved-attribute]
assert obj2.v_i64 == 7
assert obj2.v_str == "hi"
assert not obj.same_as(obj2)
def test_original_unchanged(self) -> None:
obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=5, v_str="x")
obj.__replace__(v_i64=100) # ty: ignore[unresolved-attribute]
assert obj.v_i64 == 5 # ty: ignore[unresolved-attribute]
def test_replace_readonly_field(self) -> None:
# __replace__ uses the FFIProperty.set() escape hatch,
# so it works even on frozen / read-only fields.
pair = tvm_ffi.testing.TestIntPair(3, 4)
pair2 = pair.__replace__(a=10) # ty: ignore[unresolved-attribute]
assert pair2.a == 10
assert pair2.b == 4
assert pair.a == 3 # original unchanged
def test_auto_replace_for_cxx_class(self) -> None:
# _TestCxxClassBase is copy-constructible, so replace is auto-enabled
# Note: _TestCxxClassBase.__init__ adds 1 to v_i64 and 2 to v_i32
obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
obj2 = obj.__replace__(v_i64=99) # ty: ignore[unresolved-attribute]
assert obj2.v_i64 == 99
assert obj2.v_i32 == 4
assert not obj.same_as(obj2)
def test_non_copyable_type_raises(self) -> None:
obj = tvm_ffi.testing.TestNonCopyable(42)
with pytest.raises(TypeError, match="does not support copy"):
obj.__replace__() # ty: ignore[unresolved-attribute]
# --------------------------------------------------------------------------- #
# @py_class copy/deepcopy with rich field types
# --------------------------------------------------------------------------- #
class TestPyClassCopyRichFields:
"""copy.copy / copy.deepcopy with container and optional fields on @py_class."""
def test_shallow_copy_containers(self) -> None:
@py_class(_unique_key_pc("SCCont"))
class SCCont(Object):
x: int
items: List[int]
data: Dict[str, int]
obj = SCCont(x=42, items=[1, 2, 3], data={"a": 1})
obj2 = copy.copy(obj)
assert obj2.x == 42
assert len(obj2.items) == 3
assert obj2.data["a"] == 1
assert obj.items.same_as(obj2.items) # ty:ignore[unresolved-attribute]
assert obj.data.same_as(obj2.data) # ty:ignore[unresolved-attribute]
def test_deep_copy_containers(self) -> None:
@py_class(_unique_key_pc("DCCont"))
class DCCont(Object):
x: int
items: List[int]
data: Dict[str, int]
obj = DCCont(x=42, items=[1, 2, 3], data={"a": 1})
obj2 = copy.deepcopy(obj)
assert obj2.x == 42
assert len(obj2.items) == 3
assert obj2.data["a"] == 1
assert not obj.items.same_as(obj2.items) # ty:ignore[unresolved-attribute]
assert not obj.data.same_as(obj2.data) # ty:ignore[unresolved-attribute]
def test_shallow_copy_nested_containers(self) -> None:
@py_class(_unique_key_pc("SCNest"))
class SCNest(Object):
matrix: List[List[int]]
obj = SCNest(matrix=[[1, 2], [3, 4]])
obj2 = copy.copy(obj)
assert obj.matrix.same_as(obj2.matrix) # ty:ignore[unresolved-attribute]
def test_deep_copy_nested_containers(self) -> None:
@py_class(_unique_key_pc("DCNest"))
class DCNest(Object):
matrix: List[List[int]]
obj = DCNest(matrix=[[1, 2], [3, 4]])
obj2 = copy.deepcopy(obj)
assert obj2.matrix[0][0] == 1
assert obj2.matrix[1][1] == 4
assert not obj.matrix.same_as(obj2.matrix) # ty:ignore[unresolved-attribute]
def test_deep_copy_mutation_independent(self) -> None:
@py_class(_unique_key_pc("DCMutInd"))
class DCMutInd(Object):
x: int
items: List[int]
obj = DCMutInd(x=1, items=[10, 20])
obj2 = copy.deepcopy(obj)
obj2.x = 99
assert obj.x == 1
obj2.items[0] = 999
assert obj.items[0] == 10
def test_shallow_copy_optional_fields(self) -> None:
@py_class(_unique_key_pc("SCOpt"))
class SCOpt(Object):
x: Optional[int]
items: Optional[List[int]]
obj = SCOpt(x=42, items=[1, 2])
obj2 = copy.copy(obj)
assert obj2.x == 42
assert len(obj2.items) == 2 # ty:ignore[invalid-argument-type]
def test_deep_copy_with_none_optional(self) -> None:
@py_class(_unique_key_pc("DCOptNone"))
class DCOptNone(Object):
x: Optional[int]
items: Optional[List[int]]
obj = DCOptNone(x=None, items=None)
obj2 = copy.deepcopy(obj)
assert obj2.x is None
assert obj2.items is None
def test_replace_with_containers(self) -> None:
@py_class(_unique_key_pc("ReplCont"))
class ReplCont(Object):
x: int
items: List[int]
obj = ReplCont(x=1, items=[1, 2, 3])
obj2 = obj.__replace__(x=99) # ty:ignore[unresolved-attribute]
assert obj2.x == 99
assert tuple(obj2.items) == (1, 2, 3)
assert obj.x == 1
# --------------------------------------------------------------------------- #
# DeepCopy FFI with @py_class containers
# --------------------------------------------------------------------------- #
class TestPyClassDeepCopyContainers:
"""DeepCopy FFI function with @py_class container fields."""
def test_deep_copy_list_field(self) -> None:
@py_class(_unique_key_pc("DCList"))
class DCList(Object):
items: List[int]
obj = DCList(items=[1, 2, 3])
obj2 = DeepCopy(obj)
assert tuple(obj2.items) == (1, 2, 3)
assert not obj.items.same_as(obj2.items) # ty:ignore[unresolved-attribute]
def test_deep_copy_dict_field(self) -> None:
@py_class(_unique_key_pc("DCDict"))
class DCDict(Object):
data: Dict[str, int]
obj = DCDict(data={"a": 1, "b": 2})
obj2 = DeepCopy(obj)
assert obj2.data["a"] == 1
assert not obj.data.same_as(obj2.data) # ty:ignore[unresolved-attribute]
def test_deep_copy_nested(self) -> None:
@py_class(_unique_key_pc("DCNested"))
class DCNested(Object):
matrix: List[List[int]]
obj = DCNested(matrix=[[1, 2], [3, 4]])
obj2 = DeepCopy(obj)
assert obj2.matrix[0][0] == 1
assert not obj.matrix.same_as(obj2.matrix) # ty:ignore[unresolved-attribute]
def test_deep_copy_optional_none(self) -> None:
@py_class(_unique_key_pc("DCOptN"))
class DCOptN(Object):
items: Optional[List[int]]
obj = DCOptN(items=None)
assert DeepCopy(obj).items is None
def test_deep_copy_optional_value(self) -> None:
@py_class(_unique_key_pc("DCOptV"))
class DCOptV(Object):
items: Optional[List[int]]
obj = DCOptV(items=[1, 2, 3])
obj2 = DeepCopy(obj)
assert tuple(obj2.items) == (1, 2, 3)
assert not obj.items.same_as(obj2.items) # ty:ignore[unresolved-attribute]
# --------------------------------------------------------------------------- #
# Copy of @py_class with custom __init__
# --------------------------------------------------------------------------- #
class TestPyClassCopyCustomInit:
"""Copy of @py_class with init=False and custom __init__."""
def _make_cls(self) -> type:
@py_class(_unique_key_pc("CopyCI"), init=False)
class CopyCI(Object):
a: int
b: str
def __init__(self, *, b: str, a: int) -> None:
self.a = a
self.b = b
return CopyCI
def test_shallow_copy_custom_init(self) -> None:
CopyCI = self._make_cls()
src = CopyCI(a=1, b="hello")
dst = copy.copy(src)
assert not src.same_as(dst)
assert dst.a == 1
assert dst.b == "hello"
def test_deep_copy_custom_init(self) -> None:
CopyCI = self._make_cls()
src = CopyCI(a=1, b="hello")
dst = copy.deepcopy(src)
assert not src.same_as(dst)
assert dst.a == 1
assert dst.b == "hello"
# --------------------------------------------------------------------------- #
# Pickle roundtrip for @py_class
# --------------------------------------------------------------------------- #
# Pickle requires classes to be importable at module level.
@py_class(_unique_key_pc("PickleBasic"))
class _PickleBasic(Object):
a: int
b: float
c: str
d: bool
@py_class(_unique_key_pc("PickleOptV"))
class _PickleOptV(Object):
a: Optional[int]
b: Optional[str]
@py_class(_unique_key_pc("PickleCont"))
class _PickleCont(Object):
items: List[int]
data: Dict[str, int]
@py_class(_unique_key_pc("PickleCI"), init=False)
class _PickleCI(Object):
a: int
b: str
def __init__(self, *, b: str, a: int) -> None:
self.a = a
self.b = b
class TestPyClassPickleRoundtrip:
"""Pickle serialization/deserialization for @py_class objects."""
def test_pickle_basic_fields(self) -> None:
obj = _PickleBasic(a=1, b=2.0, c="hello", d=True)
obj2 = pickle.loads(pickle.dumps(obj))
assert obj2.a == 1
assert obj2.b == 2.0
assert obj2.c == "hello"
assert obj2.d is True
def test_pickle_optional_with_values(self) -> None:
obj = _PickleOptV(a=42, b="world")
obj2 = pickle.loads(pickle.dumps(obj))
assert obj2.a == 42
assert obj2.b == "world"
def test_pickle_optional_with_none(self) -> None:
obj = _PickleOptV(a=None, b=None)
obj2 = pickle.loads(pickle.dumps(obj))
assert obj2.a is None
assert obj2.b is None
def test_pickle_container_fields(self) -> None:
obj = _PickleCont(items=[1, 2, 3], data={"a": 1, "b": 2})
obj2 = pickle.loads(pickle.dumps(obj))
assert tuple(obj2.items) == (1, 2, 3)
assert obj2.data["a"] == 1
def test_pickle_custom_init(self) -> None:
obj = _PickleCI(a=1, b="hello")
obj2 = pickle.loads(pickle.dumps(obj))
assert obj2.a == 1
assert obj2.b == "hello"