blob: 8de633716f89ffb22e3f4e76cc6b489f62778d88 [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 :func:`tvm_ffi.dataclasses.is_dataclass`, :func:`fields`, :func:`replace`."""
from __future__ import annotations
import collections
import dataclasses as _dc
import itertools
import typing
from typing import List, Optional
import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi.core import MISSING, Object
from tvm_ffi.dataclasses import (
KW_ONLY,
Field,
asdict,
astuple,
field,
fields,
is_dataclass,
py_class,
replace,
)
_counter = itertools.count()
def _k(base: str) -> str:
return f"testing.compat.{base}_{next(_counter)}"
# ---------------------------------------------------------------------------
# is_dataclass
# ---------------------------------------------------------------------------
class TestIsDataclass:
def test_c_class_instance(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
assert is_dataclass(p) is True
def test_c_class_type(self) -> None:
assert is_dataclass(tvm_ffi.testing.TestIntPair) is True
def test_py_class_instance(self) -> None:
@py_class(_k("PC"))
class PC(Object):
x: int
assert is_dataclass(PC(x=1)) is True
def test_py_class_type(self) -> None:
@py_class(_k("PCT"))
class PCT(Object):
x: int
assert is_dataclass(PCT) is True
def test_non_dataclass_types(self) -> None:
assert is_dataclass(42) is False
assert is_dataclass("hi") is False
assert is_dataclass(object()) is False
assert is_dataclass(int) is False
def test_plain_object_subclass(self) -> None:
"""Object subclass without @py_class / @c_class shouldn't qualify."""
class Plain(Object):
pass
assert is_dataclass(Plain) is False
def test_stdlib_dataclass_not_recognised(self) -> None:
"""``common.is_dataclass`` only recognises FFI types."""
@_dc.dataclass
class D:
x: int
assert is_dataclass(D) is False
assert is_dataclass(D(1)) is False
# ---------------------------------------------------------------------------
# fields
# ---------------------------------------------------------------------------
class TestFields:
def test_returns_tuple_of_field(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
result = fields(p)
assert isinstance(result, tuple)
assert all(isinstance(f, Field) for f in result)
def test_c_class_basic(self) -> None:
fs = fields(tvm_ffi.testing.TestIntPair)
assert [f.name for f in fs] == ["a", "b"]
assert all(f.type is int for f in fs)
# TestIntPair registers no defaults; the Field descriptors reflect that.
assert all(f.default is MISSING for f in fs)
def test_c_class_default_value_from_cxx(self) -> None:
"""``fields()`` exposes ``refl::default_value(...)`` from C++.
``_TestCxxClassDerived.v_f32`` has ``refl::default_value(8.0f)``.
"""
fs = fields(tvm_ffi.testing._TestCxxClassDerived)
by_name = {f.name: f for f in fs}
assert by_name["v_f32"].default == pytest.approx(8.0)
assert by_name["v_f32"].default_factory is MISSING
# v_f64 has no C++ default.
assert by_name["v_f64"].default is MISSING
def test_c_class_instance_and_type_equivalent(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
assert fields(p) == fields(tvm_ffi.testing.TestIntPair)
def test_py_class_with_default(self) -> None:
@py_class(_k("PCDef"))
class PCDef(Object):
x: int
y: str = field(default="hi")
fs = fields(PCDef)
names = [f.name for f in fs]
assert names == ["x", "y"]
(fx, fy) = fs
assert fx.default is MISSING
assert fx.default_factory is MISSING
assert fy.default == "hi"
assert fy.default_factory is MISSING
def test_py_class_with_default_factory(self) -> None:
@py_class(_k("PCFact"))
class PCFact(Object):
items: List[int] = field(default_factory=list)
(f,) = fields(PCFact)
assert f.default is MISSING
assert f.default_factory is list
def test_py_class_optional_type_resolved(self) -> None:
@py_class(_k("PCOpt"))
class PCOpt(Object):
v: Optional[int] = field(default=None)
(f,) = fields(PCOpt)
# Depending on the Python version, get_type_hints may preserve
# Optional[int] or normalize it to Union[int, None]; inspect the
# type's args instead of relying on str() formatting.
args = typing.get_args(f.type)
assert int in args and type(None) in args
assert f.default is None
def test_inheritance_parent_first(self) -> None:
@py_class(_k("PCP"))
class P(Object):
x: int
y: str = field(default="p")
@py_class(_k("PCC"))
class C(P):
z: float = field(default=1.5)
assert [f.name for f in fields(C)] == ["x", "y", "z"]
assert fields(C)[2].default == 1.5
def test_c_class_inheritance(self) -> None:
"""@c_class derived types also walk the parent chain."""
fs = fields(tvm_ffi.testing._TestCxxClassDerived)
# Parent v_i64/v_i32 come before child v_f64/v_f32.
assert [f.name for f in fs] == ["v_i64", "v_i32", "v_f64", "v_f32"]
def test_non_dataclass_raises(self) -> None:
with pytest.raises(TypeError, match="c_class or py_class"):
fields(42)
def test_field_attribute_access(self) -> None:
"""Returned Field descriptors expose ``name`` / ``type`` / ``default``."""
(fa, _) = fields(tvm_ffi.testing.TestIntPair)
assert fa.name == "a"
assert fa.type is int
assert fa.default is MISSING
# ---------------------------------------------------------------------------
# replace
# ---------------------------------------------------------------------------
class TestReplace:
def test_c_class_single_field(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
p2 = replace(p, a=10)
assert (p2.a, p2.b) == (10, 2)
assert (p.a, p.b) == (1, 2) # original unchanged
assert not p.same_as(p2)
def test_c_class_multiple_fields(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
p2 = replace(p, a=7, b=8)
assert (p2.a, p2.b) == (7, 8)
def test_c_class_no_changes_is_copy(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
p2 = replace(p)
assert (p2.a, p2.b) == (1, 2)
assert not p.same_as(p2)
def test_py_class_basic(self) -> None:
@py_class(_k("PCR"))
class PCR(Object):
x: int = field(default=5)
y: str = field(default="hi")
obj = PCR()
obj2 = replace(obj, x=99)
assert obj2.x == 99
assert obj2.y == "hi"
assert obj.x == 5 # original unchanged
def test_py_class_inherited_fields(self) -> None:
@py_class(_k("PCRParent"))
class P(Object):
x: int
@py_class(_k("PCRChild"))
class C(P):
y: str
obj = C(x=1, y="a")
obj2 = replace(obj, x=99)
assert obj2.x == 99
assert obj2.y == "a"
def test_replace_frozen_field(self) -> None:
"""replace() goes through __replace__, which uses the frozen-bypass setter."""
# TestIntPair.a and .b are read-only (frozen c_class fields).
p = tvm_ffi.testing.TestIntPair(3, 4)
p2 = replace(p, a=10)
assert p2.a == 10
assert p.a == 3 # still read-only, original untouched
# ---------------------------------------------------------------------------
# asdict
# ---------------------------------------------------------------------------
class TestAsdict:
def test_c_class_basic(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
assert asdict(p) == {"a": 1, "b": 2}
def test_py_class_basic(self) -> None:
@py_class(_k("PCAsdict"))
class PC(Object):
x: int
y: str
assert asdict(PC(x=1, y="hi")) == {"x": 1, "y": "hi"}
def test_py_class_nested(self) -> None:
@py_class(_k("PCInner"))
class Inner(Object):
x: int
@py_class(_k("PCOuter"))
class Outer(Object):
a: Inner
b: Inner
out = Outer(a=Inner(x=1), b=Inner(x=2))
assert asdict(out) == {"a": {"x": 1}, "b": {"x": 2}}
def test_py_class_inheritance(self) -> None:
@py_class(_k("PCParent"))
class P(Object):
x: int
@py_class(_k("PCChild"))
class C(P):
y: str
assert asdict(C(x=1, y="a")) == {"x": 1, "y": "a"}
def test_ffi_array_recurses_to_list(self) -> None:
@py_class(_k("PCWithArray"))
class PC(Object):
xs: tvm_ffi.Array
pc = PC(xs=tvm_ffi.Array([1, 2, 3]))
result = asdict(pc)
assert result == {"xs": [1, 2, 3]}
assert type(result["xs"]) is list
def test_ffi_list_recurses_to_list(self) -> None:
@py_class(_k("PCWithList"))
class PC(Object):
xs: tvm_ffi.List
pc = PC(xs=tvm_ffi.List([1, 2, 3]))
result = asdict(pc)
assert result == {"xs": [1, 2, 3]}
assert type(result["xs"]) is list
def test_ffi_map_recurses_to_dict(self) -> None:
@py_class(_k("PCWithMap"))
class PC(Object):
m: tvm_ffi.Map
pc = PC(m=tvm_ffi.Map({"a": 1, "b": 2}))
result = asdict(pc)
assert result == {"m": {"a": 1, "b": 2}}
assert type(result["m"]) is dict
def test_ffi_dict_recurses_to_dict(self) -> None:
@py_class(_k("PCWithDict"))
class PC(Object):
d: tvm_ffi.Dict
pc = PC(d=tvm_ffi.Dict({"a": 1, "b": 2}))
result = asdict(pc)
assert result == {"d": {"a": 1, "b": 2}}
assert type(result["d"]) is dict
def test_ffi_array_of_dataclasses(self) -> None:
@py_class(_k("PCItem"))
class Item(Object):
v: int
@py_class(_k("PCBox"))
class Box(Object):
items: tvm_ffi.Array
box = Box(items=tvm_ffi.Array([Item(v=1), Item(v=2)]))
assert asdict(box) == {"items": [{"v": 1}, {"v": 2}]}
def test_ffi_map_of_dataclasses(self) -> None:
@py_class(_k("PCItem2"))
class Item(Object):
v: int
@py_class(_k("PCBox2"))
class Box(Object):
items: tvm_ffi.Map
box = Box(items=tvm_ffi.Map({"a": Item(v=1), "b": Item(v=2)}))
assert asdict(box) == {"items": {"a": {"v": 1}, "b": {"v": 2}}}
def test_dict_factory(self) -> None:
@py_class(_k("PCDF"))
class PC(Object):
x: int
y: int
result = asdict(PC(x=1, y=2), dict_factory=collections.OrderedDict)
assert isinstance(result, collections.OrderedDict)
assert list(result.items()) == [("x", 1), ("y", 2)]
def test_dict_factory_recurses(self) -> None:
@py_class(_k("PCDFI"))
class Inner(Object):
v: int
@py_class(_k("PCDFO"))
class Outer(Object):
a: Inner
result = asdict(Outer(a=Inner(v=5)), dict_factory=collections.OrderedDict)
assert isinstance(result, collections.OrderedDict)
assert isinstance(result["a"], collections.OrderedDict)
def test_default_factory_list_independent(self) -> None:
"""Result must be a fresh ``list``, not aliased to any internal state."""
@py_class(_k("PCInd"))
class PC(Object):
xs: tvm_ffi.Array
pc = PC(xs=tvm_ffi.Array([1, 2]))
d = asdict(pc)
d["xs"].append(99)
# Mutating the result must not affect the original.
assert list(pc.xs) == [1, 2]
def test_type_raises(self) -> None:
with pytest.raises(TypeError, match="c_class / py_class instances"):
asdict(tvm_ffi.testing.TestIntPair) # passing type, not instance
def test_non_dataclass_raises(self) -> None:
with pytest.raises(TypeError, match="c_class / py_class instances"):
asdict(42)
with pytest.raises(TypeError, match="c_class / py_class instances"):
asdict([1, 2, 3])
def test_defaultdict_preserved(self) -> None:
"""``defaultdict`` round-trips with its ``default_factory`` intact.
Exercises the ``_asdict_inner`` defaultdict branch directly, since
FFI ``Any`` field storage converts a stored ``defaultdict`` into
an FFI ``Map`` on readback. Mirrors stdlib
``dataclasses._asdict_inner``'s check on ``type(obj)``.
"""
from tvm_ffi.dataclasses.common import _asdict_inner # noqa: PLC0415
dd = collections.defaultdict(list)
dd["a"].append(1)
dd["b"].append(2)
result = _asdict_inner(dd, dict)
assert type(result) is collections.defaultdict
assert result.default_factory is list
assert dict(result) == {"a": [1], "b": [2]}
# ---------------------------------------------------------------------------
# astuple
# ---------------------------------------------------------------------------
class TestAstuple:
def test_c_class_basic(self) -> None:
p = tvm_ffi.testing.TestIntPair(1, 2)
assert astuple(p) == (1, 2)
def test_py_class_basic(self) -> None:
@py_class(_k("PCAstuple"))
class PC(Object):
x: int
y: str
assert astuple(PC(x=1, y="hi")) == (1, "hi")
def test_py_class_nested(self) -> None:
@py_class(_k("PCInnerT"))
class Inner(Object):
x: int
@py_class(_k("PCOuterT"))
class Outer(Object):
a: Inner
b: Inner
out = Outer(a=Inner(x=1), b=Inner(x=2))
assert astuple(out) == ((1,), (2,))
def test_py_class_inheritance(self) -> None:
@py_class(_k("PCParentT"))
class P(Object):
x: int
@py_class(_k("PCChildT"))
class C(P):
y: str
assert astuple(C(x=1, y="a")) == (1, "a")
def test_ffi_array_recurses_to_list(self) -> None:
@py_class(_k("PCArrT"))
class PC(Object):
xs: tvm_ffi.Array
pc = PC(xs=tvm_ffi.Array([1, 2, 3]))
result = astuple(pc)
assert result == ([1, 2, 3],)
assert type(result[0]) is list
def test_ffi_map_recurses_to_dict(self) -> None:
@py_class(_k("PCMapT"))
class PC(Object):
m: tvm_ffi.Map
pc = PC(m=tvm_ffi.Map({"a": 1}))
result = astuple(pc)
assert result == ({"a": 1},)
assert type(result[0]) is dict
def test_tuple_factory(self) -> None:
@py_class(_k("PCTF"))
class PC(Object):
x: int
y: int
assert astuple(PC(x=1, y=2), tuple_factory=list) == [1, 2]
def test_tuple_factory_recurses(self) -> None:
@py_class(_k("PCTFI"))
class Inner(Object):
v: int
@py_class(_k("PCTFO"))
class Outer(Object):
a: Inner
result = astuple(Outer(a=Inner(v=5)), tuple_factory=list)
assert result == [[5]]
def test_type_raises(self) -> None:
with pytest.raises(TypeError, match="c_class / py_class instances"):
astuple(tvm_ffi.testing.TestIntPair)
def test_non_dataclass_raises(self) -> None:
with pytest.raises(TypeError, match="c_class / py_class instances"):
astuple(42)
with pytest.raises(TypeError, match="c_class / py_class instances"):
astuple([1, 2, 3])
def test_defaultdict_preserved(self) -> None:
"""``defaultdict`` round-trips with its ``default_factory`` intact."""
from tvm_ffi.dataclasses.common import _astuple_inner # noqa: PLC0415
dd = collections.defaultdict(list)
dd["a"].append(1)
dd["b"].append(2)
result = _astuple_inner(dd, tuple)
assert type(result) is collections.defaultdict
assert result.default_factory is list
assert dict(result) == {"a": [1], "b": [2]}
# ---------------------------------------------------------------------------
# __match_args__
# ---------------------------------------------------------------------------
def _match_args(cls: type) -> object:
"""Read ``__match_args__`` without tripping static attribute checks."""
return getattr(cls, "__match_args__")
class TestMatchArgs:
def test_py_class_basic(self) -> None:
@py_class(_k("MAPy"))
class PC(Object):
x: int
y: str
assert _match_args(PC) == ("x", "y")
def test_py_class_init_false_excluded(self) -> None:
@py_class(_k("MAInitFalse"))
class PC(Object):
x: int
y: int = field(default=0, init=False)
assert _match_args(PC) == ("x",)
def test_py_class_kw_only_field_excluded(self) -> None:
@py_class(_k("MAKwField"))
class PC(Object):
x: int
y: int = field(kw_only=True)
assert _match_args(PC) == ("x",)
def test_py_class_kw_only_decorator_excludes_all(self) -> None:
@py_class(_k("MAKwDeco"), kw_only=True)
class PC(Object):
x: int
y: int
assert _match_args(PC) == ()
def test_py_class_kw_only_sentinel(self) -> None:
@py_class(_k("MAKwSent"))
class PC(Object):
x: int
_: KW_ONLY
y: int
z: int
assert _match_args(PC) == ("x",)
def test_py_class_inheritance_parent_first(self) -> None:
@py_class(_k("MAParent"))
class P(Object):
x: int
y: str
@py_class(_k("MAChild"))
class C(P):
z: float
assert _match_args(P) == ("x", "y")
assert _match_args(C) == ("x", "y", "z")
def test_py_class_opt_out(self) -> None:
@py_class(_k("MAOptOut"), match_args=False)
class PC(Object):
x: int
y: int
assert "__match_args__" not in PC.__dict__
def test_py_class_user_defined_preserved(self) -> None:
@py_class(_k("MAUser"))
class PC(Object):
x: int
y: int
__match_args__ = ("y",)
assert _match_args(PC) == ("y",)
def test_c_class_basic(self) -> None:
assert _match_args(tvm_ffi.testing.TestIntPair) == ("a", "b")
def test_c_class_inheritance_parent_first(self) -> None:
assert _match_args(tvm_ffi.testing._TestCxxClassDerived) == (
"v_i64",
"v_i32",
"v_f64",
"v_f32",
)
def test_tuple_type(self) -> None:
@py_class(_k("MATupT"))
class PC(Object):
x: int
assert isinstance(_match_args(PC), tuple)