blob: 3a557eabf62e97fdb4f8d8160cac9ede81f2084d [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.
from __future__ import annotations
import dataclasses
import sys
from typing import Any
import pytest
from tvm_ffi.utils.unpack_dataclass import (
_extract_dataclass_to_tuple_schema,
_validate_dataclass_to_tuple_schema,
unpack_dataclass_to_tuple,
)
# Module-level dataclass definitions for schema extraction tests.
# Must be at module level so typing.get_type_hints() can resolve
# cross-references with PEP 563 (from __future__ import annotations).
@dataclasses.dataclass
class _ExtractConfig:
x: int
y: int
@dataclasses.dataclass
class _ExtractNested:
value: int
cfg: _ExtractConfig
@dataclasses.dataclass
class _ExtractWithAny:
data: Any
scale: int
@dataclasses.dataclass
class _ExtractWithList:
items: list[_ExtractConfig]
scale: int
@dataclasses.dataclass
class _ExtractWithLeafList:
values: list[int]
name: str
@dataclasses.dataclass
class _ExtractWithDict:
mapping: dict[str, _ExtractConfig]
count: int
@dataclasses.dataclass
class _ExtractWithLeafDict:
mapping: dict[str, int]
count: int
@dataclasses.dataclass
class _ExtractWithTuple:
pair: tuple[_ExtractConfig, int]
flag: bool
@dataclasses.dataclass
class _ExtractWithOptional:
value: int | None
name: str
@dataclasses.dataclass
class _ExtractWithLeafListInt:
items: list[int]
scale: int
def test_unpack_dataclass_to_tuple() -> None:
"""Test unpack_dataclass_to_tuple JIT-compiled unpacking."""
@dataclasses.dataclass
class Config:
x: int
y: int
@dataclasses.dataclass
class Nested:
value: int
cfg: Config
@dataclasses.dataclass
class Deep:
nested: Nested
flag: bool
# Flat dataclass
assert unpack_dataclass_to_tuple(Config(x=1, y=2)) == (1, 2)
# Nested dataclass (auto-recurses based on type annotation)
assert unpack_dataclass_to_tuple(Nested(value=5, cfg=Config(x=10, y=20))) == (5, (10, 20))
# Deep nesting
assert unpack_dataclass_to_tuple(
Deep(nested=Nested(value=5, cfg=Config(x=10, y=20)), flag=True)
) == ((5, (10, 20)), True)
# Leaf passthrough
assert unpack_dataclass_to_tuple(42) == 42
assert unpack_dataclass_to_tuple("hello") == "hello"
assert unpack_dataclass_to_tuple(None) is None
# List recursion: list of dataclasses -> tuple of tuples
assert unpack_dataclass_to_tuple([Config(x=1, y=2), Config(x=3, y=4)]) == [(1, 2), (3, 4)]
# Tuple recursion
assert unpack_dataclass_to_tuple((Config(x=1, y=2), 5)) == ((1, 2), 5)
# Dict recursion (recurses values)
assert unpack_dataclass_to_tuple({"a": Config(x=1, y=2), "b": 3}) == {"a": (1, 2), "b": 3}
# Leaf values are NOT copied (no deep copy)
class Holder:
pass
@dataclasses.dataclass
class WithObj:
obj: Any
val: int
h = Holder()
result = unpack_dataclass_to_tuple(WithObj(obj=h, val=1))
assert result == (h, 1)
assert result[0] is h # same object reference, no copy
# Dynamic dispatch: Any-typed field receives a dataclass at runtime
@dataclasses.dataclass
class WithAnyField:
data: Any
scale: int
# data is Any -> schema marks it as "unpack" -> __dispatch called at runtime
# When data is a dataclass, it should be recursively unpacked
result = unpack_dataclass_to_tuple(WithAnyField(data=Config(x=1, y=2), scale=3))
assert result == ((1, 2), 3)
# When data is a plain value, passthrough
result = unpack_dataclass_to_tuple(WithAnyField(data=42, scale=3))
assert result == (42, 3)
# When data is a list of dataclasses, recurse each element
result = unpack_dataclass_to_tuple(
WithAnyField(data=[Config(x=1, y=2), Config(x=3, y=4)], scale=5)
)
assert result == ([(1, 2), (3, 4)], 5)
# When data is a nested dataclass
result = unpack_dataclass_to_tuple(
WithAnyField(data=Nested(value=10, cfg=Config(x=1, y=2)), scale=5)
)
assert result == ((10, (1, 2)), 5)
# When data is a dict with dataclass values
result = unpack_dataclass_to_tuple(
WithAnyField(data={"a": Config(x=1, y=2), "b": Config(x=3, y=4)}, scale=5)
)
assert result == ({"a": (1, 2), "b": (3, 4)}, 5)
# Self-referential dataclass (linked list): should not infinite recurse
@dataclasses.dataclass
class Node:
value: int
next: Node | None
# Build a short linked list
node = Node(value=1, next=Node(value=2, next=None))
result = unpack_dataclass_to_tuple(node)
# The 'next' field is typed as Node|None, which on 3.10+ resolves to
# a UnionType. The self-reference is caught by memo -> UNPACK -> dynamic dispatch.
# Dynamic dispatch recursively unpacks the nested Node.
assert result == (1, (2, None))
def test_validate_dataclass_to_tuple_schema() -> None:
"""Test internal schema validation."""
# Valid schemas
_validate_dataclass_to_tuple_schema({"x": None, "y": None})
_validate_dataclass_to_tuple_schema({"cfg": {"x": None, "y": None}, "scale": None})
# Invalid: not a dict
with pytest.raises(TypeError, match="must be a dict"):
_validate_dataclass_to_tuple_schema([1, 2]) # type: ignore[arg-type]
# Invalid: non-string key
with pytest.raises(TypeError, match="must be a string"):
_validate_dataclass_to_tuple_schema({123: None})
# Invalid: not a valid identifier
with pytest.raises(ValueError, match="not a valid Python identifier"):
_validate_dataclass_to_tuple_schema({"not-valid": None})
# Invalid: Python keyword
with pytest.raises(ValueError, match="is a Python keyword"):
_validate_dataclass_to_tuple_schema({"class": None})
# Invalid: nested schema with bad key
with pytest.raises(ValueError, match="not a valid Python identifier"):
_validate_dataclass_to_tuple_schema({"cfg": {"x": None, "y!": None}})
@pytest.mark.xfail(
sys.version_info < (3, 10),
reason="list[X]/dict[X,Y]/int|None not evaluable by get_type_hints on Python < 3.10",
)
def test_extract_dataclass_to_tuple_schema() -> None:
"""Test schema extraction from dataclass types."""
# Flat: all known leaf types -> None
schema = _extract_dataclass_to_tuple_schema(_ExtractConfig)
assert schema == {"x": None, "y": None}
# Nested: known dataclass field -> nested schema
schema = _extract_dataclass_to_tuple_schema(_ExtractNested)
assert schema == {"value": None, "cfg": {"x": None, "y": None}}
# Any field -> "unpack" (dynamic dispatch)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithAny)
assert schema == {"data": "unpack", "scale": None}
# list[Config] -> "unpack" (container with dataclass element)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithList)
assert schema == {"items": "unpack", "scale": None}
# list[int] -> "unpack" (list must be converted to tuple per contract)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithLeafList)
assert schema == {"values": "unpack", "name": None}
# list[int] standalone field also gets UNPACK
schema = _extract_dataclass_to_tuple_schema(_ExtractWithLeafListInt)
assert schema == {"items": "unpack", "scale": None}
# dict[str, Config] -> "unpack" (container with dataclass value type)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithDict)
assert schema == {"mapping": "unpack", "count": None}
# dict[str, int] -> None (container with only known leaf types)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithLeafDict)
assert schema == {"mapping": None, "count": None}
# tuple[Config, int] -> "unpack" (tuple containing a dataclass)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithTuple)
assert schema == {"pair": "unpack", "flag": None}
# Optional[int] (int | None) -> None (Union of known leaves)
schema = _extract_dataclass_to_tuple_schema(_ExtractWithOptional)
assert schema == {"value": None, "name": None}
# Non-dataclass raises
with pytest.raises(TypeError, match="Expected a dataclass class"):
_extract_dataclass_to_tuple_schema(int)
# Locally-defined classes with built-in type annotations resolve fine
@dataclasses.dataclass
class LocalConfig:
x: int
y: int
schema = _extract_dataclass_to_tuple_schema(LocalConfig)
assert schema == {"x": None, "y": None}
# Locally-defined classes referencing other local classes can't be resolved
# by get_type_hints — all fields fall back to UNPACK
@dataclasses.dataclass
class LocalNested:
val: int
cfg: LocalConfig
schema = _extract_dataclass_to_tuple_schema(LocalNested)
# get_type_hints fails for the class -> all fields become "unpack"
assert schema == {"val": "unpack", "cfg": "unpack"}