mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
2987d93ba7
commit_hash:cf5070a543788fa57136adb1a4a7ea42f4490329
338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""Tests for pydivkit.core.serialization module.
|
|
|
|
Covers dump(), _cast_value_type sub-functions, _make_exclude_fields,
|
|
_unpack_optional_type, and _merge_types.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import enum
|
|
from typing import Any, Mapping, Optional, Sequence, Union
|
|
|
|
import pytest
|
|
|
|
from pydivkit.core import BaseEntity, Expr, Field
|
|
from pydivkit.core.serialization import (
|
|
_cast_entity_or_coerce,
|
|
_cast_mapping,
|
|
_cast_sequence,
|
|
_cast_union,
|
|
_cast_value_type,
|
|
_make_exclude_fields,
|
|
_merge_types,
|
|
_unpack_optional_type,
|
|
dump,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# dump()
|
|
# ---------------------------------------------------------------------------
|
|
class TestDump:
|
|
def test_string_passthrough(self) -> None:
|
|
assert dump("hello") == "hello"
|
|
|
|
def test_bytes_passthrough(self) -> None:
|
|
assert dump(b"data") == b"data"
|
|
|
|
def test_int_passthrough(self) -> None:
|
|
assert dump(42) == 42
|
|
|
|
def test_sequence_recurse(self) -> None:
|
|
assert dump([1, 2, 3]) == [1, 2, 3]
|
|
|
|
def test_mapping_recurse_values(self) -> None:
|
|
"""Regression test: mapping values are recursively dumped."""
|
|
assert dump({"a": 1, "b": 2}) == {"a": 1, "b": 2}
|
|
|
|
def test_mapping_with_expr_values(self) -> None:
|
|
"""Mapping values containing Expr are serialized."""
|
|
expr = Expr("@{var}")
|
|
result = dump({"key": expr})
|
|
assert result == {"key": "@{var}"}
|
|
|
|
def test_mapping_with_entity_values(self) -> None:
|
|
"""Mapping values containing BaseEntity are serialized."""
|
|
|
|
class Inner(BaseEntity):
|
|
val: int = Field(default=1)
|
|
|
|
result = dump({"child": Inner(val=5)})
|
|
assert result == {"child": {"val": 5}}
|
|
|
|
def test_mapping_with_enum_values(self) -> None:
|
|
"""Mapping values containing enums are serialized."""
|
|
|
|
class Color(enum.Enum):
|
|
RED = "red"
|
|
|
|
result = dump({"color": Color.RED})
|
|
assert result == {"color": "red"}
|
|
|
|
def test_expr_to_string(self) -> None:
|
|
assert dump(Expr("@{x}")) == "@{x}"
|
|
|
|
def test_entity_calls_dict(self) -> None:
|
|
class E(BaseEntity):
|
|
name: str = Field(default="hi")
|
|
|
|
assert dump(E()) == {"name": "hi"}
|
|
|
|
def test_enum_value(self) -> None:
|
|
class Dir(enum.Enum):
|
|
UP = "up"
|
|
DOWN = "down"
|
|
|
|
assert dump(Dir.UP) == "up"
|
|
|
|
def test_nested_sequence_of_entities(self) -> None:
|
|
class Item(BaseEntity):
|
|
x: int = Field(default=0)
|
|
|
|
result = dump([Item(x=1), Item(x=2)])
|
|
assert result == [{"x": 1}, {"x": 2}]
|
|
|
|
def test_deeply_nested_structure(self) -> None:
|
|
"""dump() handles deeply nested list-of-mapping combinations."""
|
|
|
|
class Leaf(BaseEntity):
|
|
v: int = Field(default=0)
|
|
|
|
data = [{"items": [Leaf(v=1), Leaf(v=2)]}]
|
|
result = dump(data)
|
|
assert result == [{"items": [{"v": 1}, {"v": 2}]}]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _cast_sequence
|
|
# ---------------------------------------------------------------------------
|
|
class TestCastSequence:
|
|
def test_list_of_int(self) -> None:
|
|
result = _cast_sequence([1, 2, 3], Sequence[int])
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_list_with_coercion(self) -> None:
|
|
result = _cast_sequence([1, 2], Sequence[float])
|
|
assert result == [1.0, 2.0]
|
|
assert all(isinstance(v, float) for v in result)
|
|
|
|
def test_non_sequence_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="has wrong type"):
|
|
_cast_sequence(42, Sequence[int])
|
|
|
|
def test_empty_list(self) -> None:
|
|
assert _cast_sequence([], Sequence[str]) == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _cast_mapping
|
|
# ---------------------------------------------------------------------------
|
|
class TestCastMapping:
|
|
def test_simple_mapping(self) -> None:
|
|
result = _cast_mapping({"a": 1}, Mapping[str, int])
|
|
assert result == {"a": 1}
|
|
|
|
def test_mapping_value_coercion(self) -> None:
|
|
result = _cast_mapping({"a": 1}, Mapping[str, float])
|
|
assert result == {"a": 1.0}
|
|
assert isinstance(result["a"], float)
|
|
|
|
def test_non_mapping_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="has wrong type"):
|
|
_cast_mapping([1, 2], Mapping[str, int])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _cast_union
|
|
# ---------------------------------------------------------------------------
|
|
class TestCastUnion:
|
|
def test_none_in_optional(self) -> None:
|
|
result = _cast_union(None, Optional[int])
|
|
assert result is None
|
|
|
|
def test_exact_type_match(self) -> None:
|
|
result = _cast_union(42, Union[int, str])
|
|
assert result == 42
|
|
|
|
def test_fallback_coercion(self) -> None:
|
|
result = _cast_union(42, Union[float, str])
|
|
assert isinstance(result, float)
|
|
assert result == 42.0
|
|
|
|
def test_no_match_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="has wrong type"):
|
|
_cast_union([], Union[int, float])
|
|
|
|
def test_dict_dispatch_with_type_field(self) -> None:
|
|
class Alpha(BaseEntity):
|
|
type: str = Field(name="type", default="alpha")
|
|
val: int = Field(default=0)
|
|
|
|
class Beta(BaseEntity):
|
|
type: str = Field(name="type", default="beta")
|
|
val: int = Field(default=0)
|
|
|
|
result = _cast_union({"type": "alpha", "val": 5}, Union[Alpha, Beta])
|
|
assert isinstance(result, Alpha)
|
|
assert result.val == 5
|
|
|
|
def test_dict_without_type_field_raises(self) -> None:
|
|
class Gamma(BaseEntity):
|
|
type: str = Field(name="type", default="gamma")
|
|
val: int = Field(default=0)
|
|
|
|
class Delta(BaseEntity):
|
|
type: str = Field(name="type", default="delta")
|
|
val: int = Field(default=0)
|
|
|
|
with pytest.raises(ValueError, match="does not have field"):
|
|
_cast_union({"val": 1}, Union[Gamma, Delta])
|
|
|
|
def test_dict_unknown_type_raises(self) -> None:
|
|
class Eps(BaseEntity):
|
|
type: str = Field(name="type", default="eps")
|
|
val: int = Field(default=0)
|
|
|
|
with pytest.raises(ValueError, match="does not contain"):
|
|
_cast_union({"type": "unknown"}, Union[Eps, int])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _cast_entity_or_coerce
|
|
# ---------------------------------------------------------------------------
|
|
class TestCastEntityOrCoerce:
|
|
def test_isinstance_passthrough(self) -> None:
|
|
class E(BaseEntity):
|
|
val: int = Field(default=0)
|
|
|
|
entity = E(val=1)
|
|
assert _cast_entity_or_coerce(entity, E) is entity
|
|
|
|
def test_wrong_entity_type_raises(self) -> None:
|
|
class A(BaseEntity):
|
|
val: int = Field(default=0)
|
|
|
|
class B(BaseEntity):
|
|
name: str = Field(default="x")
|
|
|
|
with pytest.raises(ValueError, match="has wrong type"):
|
|
_cast_entity_or_coerce(A(), B)
|
|
|
|
def test_dict_to_entity(self) -> None:
|
|
class E(BaseEntity):
|
|
val: int = Field(default=0)
|
|
|
|
result = _cast_entity_or_coerce({"val": 5}, E)
|
|
assert isinstance(result, E)
|
|
assert result.val == 5
|
|
|
|
def test_type_coercion(self) -> None:
|
|
result = _cast_entity_or_coerce(42, float)
|
|
assert isinstance(result, float)
|
|
assert result == 42.0
|
|
|
|
def test_none_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="has wrong type"):
|
|
_cast_entity_or_coerce(None, int)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _cast_value_type (dispatcher)
|
|
# ---------------------------------------------------------------------------
|
|
class TestCastValueType:
|
|
def test_field_value_raises(self) -> None:
|
|
from pydivkit.core.fields import _Field
|
|
|
|
with pytest.raises(ValueError, match="cannot be of type"):
|
|
_cast_value_type(_Field(), int)
|
|
|
|
def test_any_passthrough(self) -> None:
|
|
obj = object()
|
|
assert _cast_value_type(obj, Any) is obj
|
|
|
|
def test_exact_type_fast_path(self) -> None:
|
|
assert _cast_value_type(42, int) == 42
|
|
|
|
def test_sequence_dispatch(self) -> None:
|
|
result = _cast_value_type([1, 2], Sequence[int])
|
|
assert result == [1, 2]
|
|
|
|
def test_mapping_dispatch(self) -> None:
|
|
result = _cast_value_type({"a": 1}, Mapping[str, int])
|
|
assert result == {"a": 1}
|
|
|
|
def test_union_dispatch(self) -> None:
|
|
result = _cast_value_type(42, Union[int, str])
|
|
assert result == 42
|
|
|
|
def test_entity_coerce(self) -> None:
|
|
result = _cast_value_type(42, float)
|
|
assert isinstance(result, float)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _make_exclude_fields
|
|
# ---------------------------------------------------------------------------
|
|
class TestMakeExcludeFields:
|
|
def test_none_returns_empty(self) -> None:
|
|
result = _make_exclude_fields(None)
|
|
assert dict(result) == {}
|
|
|
|
def test_empty_list_returns_empty(self) -> None:
|
|
result = _make_exclude_fields([])
|
|
assert dict(result) == {}
|
|
|
|
def test_simple_fields(self) -> None:
|
|
result = _make_exclude_fields(["a", "b"])
|
|
assert dict(result) == {"a": True, "b": True}
|
|
|
|
def test_nested_fields(self) -> None:
|
|
result = _make_exclude_fields(["a.b"])
|
|
assert "a" in result
|
|
assert dict(result["a"]) == {"b": True}
|
|
|
|
def test_mixed_fields(self) -> None:
|
|
result = _make_exclude_fields(["a", "b.c"])
|
|
assert result["a"] is True
|
|
assert dict(result["b"]) == {"c": True}
|
|
|
|
def test_empty_field_name_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="cannot be empty"):
|
|
_make_exclude_fields([""])
|
|
|
|
def test_deeply_nested(self) -> None:
|
|
result = _make_exclude_fields(["a.b.c"])
|
|
inner = result["a"]
|
|
assert dict(inner["b"]) == {"c": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _unpack_optional_type
|
|
# ---------------------------------------------------------------------------
|
|
class TestUnpackOptionalType:
|
|
def test_non_union_unchanged(self) -> None:
|
|
assert _unpack_optional_type(int) is int
|
|
|
|
def test_optional_unwraps(self) -> None:
|
|
result = _unpack_optional_type(Optional[int])
|
|
assert result is int
|
|
|
|
def test_union_without_none_unchanged(self) -> None:
|
|
result = _unpack_optional_type(Union[int, str])
|
|
assert result == Union[int, str]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _merge_types
|
|
# ---------------------------------------------------------------------------
|
|
class TestMergeTypes:
|
|
def test_same_types(self) -> None:
|
|
assert _merge_types(int, int) is int
|
|
|
|
def test_optional_merge(self) -> None:
|
|
assert _merge_types(Optional[int], int) is int
|
|
|
|
def test_incompatible_raises(self) -> None:
|
|
with pytest.raises(TypeError, match="Incompatible"):
|
|
_merge_types(int, str)
|