Files
divkit/json-builder/python/tests/test_core_entities.py
T
p-mosein 2987d93ba7 pydivkit. migrate to uv+hatch, decompose entities.py, fix Python 3.14 compat
commit_hash:cf5070a543788fa57136adb1a4a7ea42f4490329
2026-02-10 16:34:59 +03:00

616 lines
20 KiB
Python

import enum
from typing import Any, Mapping, Optional, Sequence, Union
import pytest
from pydivkit.core import BaseDiv, BaseEntity, Expr, Field, Ref
# ---------------------------------------------------------------------------
# BaseEntity: construction and field assignment
# ---------------------------------------------------------------------------
class TestBaseEntityInit:
def test_create_simple_entity(self):
class SimpleEntity(BaseEntity):
name: str = Field()
entity = SimpleEntity(name="hello")
assert entity.name == "hello"
def test_field_with_default(self):
class DefaultEntity(BaseEntity):
color: str = Field(default="red")
entity = DefaultEntity()
assert entity.color == "red"
def test_optional_field_default_none(self):
"""Optional field defaults to None when not provided."""
class OptEntity(BaseEntity):
label: Optional[str] = Field()
entity = OptEntity()
assert entity.label is None
def test_sequence_field(self):
class SeqEntity(BaseEntity):
items: Sequence[int] = Field()
entity = SeqEntity(items=[1, 2, 3])
assert entity.items == [1, 2, 3]
def test_sequence_field_wrong_type_raises(self):
class SeqEntity2(BaseEntity):
items: Sequence[int] = Field()
with pytest.raises(ValueError, match="has wrong type"):
SeqEntity2(items=42)
def test_mapping_field(self):
class MapEntity(BaseEntity):
data: Mapping[str, int] = Field()
entity = MapEntity(data={"a": 1, "b": 2})
assert entity.data == {"a": 1, "b": 2}
def test_mapping_field_wrong_type_raises(self):
class MapEntity2(BaseEntity):
data: Mapping[str, int] = Field()
with pytest.raises(ValueError, match="has wrong type"):
MapEntity2(data=42)
def test_any_field_passthrough(self):
class AnyEntity(BaseEntity):
data: Any = Field()
value = {"key": [1, 2, 3]}
entity = AnyEntity(data=value)
assert entity.data is value
def test_union_field_exact_match(self):
class UnionEntity(BaseEntity):
val: Union[int, str] = Field()
entity = UnionEntity(val=42)
assert entity.val == 42
def test_union_field_no_match_raises(self):
class UnionEntity2(BaseEntity):
val: Union[int, float] = Field()
with pytest.raises(ValueError, match="has wrong type"):
UnionEntity2(val=[])
def test_type_coercion(self):
"""Values are coerced to field type (e.g. int -> float)."""
class CoerceEntity(BaseEntity):
val: float = Field()
entity = CoerceEntity(val=42)
assert isinstance(entity.val, float)
assert entity.val == 42.0
def test_unknown_attribute_raises(self):
class StrictEntity(BaseEntity):
val: int = Field(default=0)
entity = StrictEntity()
with pytest.raises(KeyError, match="has no attribute 'unknown'"):
entity.unknown = 42
def test_wrong_type_raises(self):
"""Passing an incompatible value type raises ValueError."""
class StrictTypeEntity(BaseEntity):
val: int = Field()
with pytest.raises(ValueError, match="has wrong type"):
StrictTypeEntity(val=[])
# ---------------------------------------------------------------------------
# BaseEntity: type field validation
# ---------------------------------------------------------------------------
class TestBaseEntityTypeField:
def test_type_field_wrong_value_raises(self):
class TypedEntity(BaseEntity):
type: str = Field(name="type", default="expected")
with pytest.raises(ValueError, match="has wrong value"):
TypedEntity(type="wrong")
def test_type_field_correct_value(self):
class TypedEntity2(BaseEntity):
type: str = Field(name="type", default="correct")
entity = TypedEntity2(type="correct")
assert entity.type == "correct"
# ---------------------------------------------------------------------------
# BaseEntity: dict() serialization
# ---------------------------------------------------------------------------
class TestBaseEntityDict:
def test_simple_dict(self):
class DictEntity(BaseEntity):
name: str = Field(default="default")
count: Optional[int] = Field()
entity = DictEntity(name="test", count=5)
assert entity.dict() == {"name": "test", "count": 5}
def test_dict_skips_none(self):
class NoneEntity(BaseEntity):
name: str = Field(default="x")
opt: Optional[int] = Field()
entity = NoneEntity(name="hello")
assert entity.dict() == {"name": "hello"}
def test_dict_with_nested_entity(self):
class Inner(BaseEntity):
x: int = Field(default=1)
class Outer(BaseEntity):
inner: Inner = Field()
entity = Outer(inner=Inner(x=10))
assert entity.dict() == {"inner": {"x": 10}}
def test_dict_with_sequence(self):
class SeqDictEntity(BaseEntity):
items: Sequence[int] = Field()
entity = SeqDictEntity(items=[1, 2, 3])
assert entity.dict() == {"items": [1, 2, 3]}
def test_dict_with_mapping(self):
class MapDictEntity(BaseEntity):
data: Mapping[str, Any] = Field()
entity = MapDictEntity(data={"key": "value"})
assert entity.dict() == {"data": {"key": "value"}}
def test_dict_with_expr(self):
class ExprEntity(BaseEntity):
val: Union[str, Expr] = Field()
entity = ExprEntity(val=Expr("@{some_var}"))
assert entity.dict() == {"val": "@{some_var}"}
def test_dict_with_enum(self):
class Color(enum.Enum):
RED = "red"
GREEN = "green"
class EnumEntity(BaseEntity):
color: Color = Field(default=Color.RED)
entity = EnumEntity(color=Color.GREEN)
assert entity.dict() == {"color": "green"}
def test_dict_with_nested_entity_in_sequence(self):
class Item(BaseEntity):
x: int = Field(default=1)
class ListEntity(BaseEntity):
items: Sequence[Item] = Field()
entity = ListEntity(items=[Item(x=10)])
assert entity.dict() == {"items": [{"x": 10}]}
# ---------------------------------------------------------------------------
# BaseEntity: template_name
# ---------------------------------------------------------------------------
class TestBaseEntityTemplateName:
def test_default_template_name(self):
class NameEntity(BaseEntity):
val: int = Field(default=0)
expected = f"{NameEntity.__module__}.{NameEntity.__name__}"
assert NameEntity.template_name == expected
def test_custom_template_name(self):
class CustomNameEntity(BaseEntity):
__template_name__ = "custom.template"
val: int = Field(default=0)
assert CustomNameEntity.template_name == "custom.template"
# ---------------------------------------------------------------------------
# BaseEntity: related_templates
# ---------------------------------------------------------------------------
class TestBaseEntityRelatedTemplates:
def test_empty_for_plain_entity(self):
class PlainEntity(BaseEntity):
val: int = Field(default=0)
entity = PlainEntity()
assert entity.related_templates() == set()
# ---------------------------------------------------------------------------
# BaseEntity: union with injected types (dict -> entity cast)
# ---------------------------------------------------------------------------
class TestBaseEntityUnionInjectedTypes:
def test_union_field_dict_cast_to_entity(self):
"""Dict with 'type' field is cast to correct entity in a Union."""
class TypeAlpha(BaseEntity):
type: str = Field(name="type", default="alpha")
val: int = Field(default=0)
class TypeBeta(BaseEntity):
type: str = Field(name="type", default="beta")
name: str = Field(default="x")
class Container(BaseEntity):
child: Union[TypeAlpha, TypeBeta] = Field()
entity = Container(child={"type": "alpha", "val": 42})
assert isinstance(entity.child, TypeAlpha)
assert entity.child.val == 42
def test_union_field_dict_without_type_raises(self):
class TypeGamma(BaseEntity):
type: str = Field(name="type", default="gamma")
val: int = Field(default=0)
class TypeDelta(BaseEntity):
type: str = Field(name="type", default="delta")
val: int = Field(default=0)
class Container2(BaseEntity):
child: Union[TypeGamma, TypeDelta] = Field()
with pytest.raises(ValueError, match="does not have field type"):
Container2(child={"val": 42})
def test_union_field_dict_unknown_type_raises(self):
class TypeEpsilon(BaseEntity):
type: str = Field(name="type", default="epsilon")
val: int = Field(default=0)
class TypeZeta(BaseEntity):
type: str = Field(name="type", default="zeta")
val: int = Field(default=0)
class Container3(BaseEntity):
child: Union[TypeEpsilon, TypeZeta] = Field()
with pytest.raises(ValueError, match="does not contain type"):
Container3(child={"type": "unknown", "val": 1})
# ---------------------------------------------------------------------------
# BaseDiv: template building
# ---------------------------------------------------------------------------
class TestBaseDivTemplate:
def test_direct_subclass_is_not_template(self):
class RawDiv(BaseDiv):
name: str = Field(default="x")
with pytest.raises(TypeError, match="is not a template"):
RawDiv.template()
def test_template_with_tpl_values(self):
class CompBase(BaseDiv):
type: str = Field(name="type", default="component")
text: str = Field()
class MyComponent(CompBase):
text = "default_text"
tpl = MyComponent.template()
assert tpl["type"] == "component"
assert tpl["text"] == "default_text"
def test_template_with_ref_field(self):
class RefBase(BaseDiv):
type: str = Field(name="type", default="ref_comp")
label: str = Field()
class RefTemplate(RefBase):
custom_label: str = Field()
label = Ref(custom_label)
tpl = RefTemplate.template()
assert tpl["type"] == "ref_comp"
assert "$label" in tpl
assert tpl["$label"] == "custom_label"
def test_template_caching(self):
class CacheBase(BaseDiv):
type: str = Field(name="type", default="cache_test")
val: int = Field()
class CachedComponent(CacheBase):
val = 42
tpl1 = CachedComponent.template()
tpl2 = CachedComponent.template()
assert tpl1 is tpl2
def test_template_none_tpl_value_not_overridden(self):
"""Instantiating with None doesn't override template values."""
class TplBase(BaseDiv):
type: str = Field(name="type", default="tpl_base")
color: str = Field()
class TplChild(TplBase):
color = "red"
instance = TplChild()
assert instance.dict().get("type") is not None
def test_multiple_bases_raises(self):
with pytest.raises(TypeError, match="cannot be uniquely identified"):
class MultiBaseDiv(BaseDiv, BaseEntity):
pass
# ---------------------------------------------------------------------------
# BaseDiv: schema()
# ---------------------------------------------------------------------------
class TestBaseDivSchema:
def test_schema_returns_object_with_definitions(self):
class SchBase(BaseDiv):
type: str = Field(name="type", default="schema_test")
val: int = Field()
class SchChild(SchBase):
pass
schema = SchChild.schema()
assert schema["type"] == "object"
assert "definitions" in schema
assert isinstance(schema["definitions"], dict)
def test_schema_tpl_values_excluded(self):
class SchBase2(BaseDiv):
type: str = Field(name="type", default="sch_tpl")
color: str = Field()
size: int = Field()
class SchChild2(SchBase2):
color = "red"
schema = SchChild2.schema()
props = schema.get("properties", {})
assert "color" not in props
assert "size" in props
def test_schema_with_exclude_fields(self):
class SchBase3(BaseDiv):
type: str = Field(name="type", default="sch_excl")
a: str = Field()
b: str = Field()
class SchChild3(SchBase3):
pass
schema = SchChild3.schema(exclude_fields=["a"])
props = schema.get("properties", {})
assert "a" not in props
def test_schema_with_nested_exclude(self):
class SchBase4(BaseDiv):
type: str = Field(name="type", default="sch_nest")
a: str = Field()
b: str = Field()
class SchChild4(SchBase4):
pass
schema = SchChild4.schema(exclude_fields=["a.nested"])
assert "definitions" in schema
def test_schema_optional_not_required(self):
class SchBase5(BaseDiv):
type: str = Field(name="type", default="sch_opt")
required_field: int = Field()
optional_field: Optional[str] = Field()
class SchChild5(SchBase5):
pass
schema = SchChild5.schema()
required = schema.get("required", [])
assert "required_field" in required
assert "optional_field" not in required
def test_schema_int_field(self):
class IntBase(BaseDiv):
type: str = Field(name="type", default="int_sch")
count: int = Field()
class IntChild(IntBase):
pass
schema = IntChild.schema()
props = schema.get("properties", {})
assert props["count"]["type"] == "integer"
def test_schema_str_field(self):
class StrBase(BaseDiv):
type: str = Field(name="type", default="str_sch")
name: str = Field()
class StrChild(StrBase):
pass
schema = StrChild.schema()
props = schema.get("properties", {})
assert props["name"]["type"] == "string"
def test_schema_float_field(self):
class FloatBase(BaseDiv):
type: str = Field(name="type", default="float_sch")
rate: float = Field()
class FloatChild(FloatBase):
pass
schema = FloatChild.schema()
props = schema.get("properties", {})
assert props["rate"]["type"] == "number"
def test_schema_bool_field(self):
class BoolBase(BaseDiv):
type: str = Field(name="type", default="bool_sch")
flag: bool = Field()
class BoolChild(BoolBase):
pass
schema = BoolChild.schema()
props = schema.get("properties", {})
assert props["flag"]["type"] == "integer"
assert props["flag"]["format"] == "boolean"
def test_schema_sequence_field(self):
class SeqBase(BaseDiv):
type: str = Field(name="type", default="seq_sch")
items: Sequence[int] = Field()
class SeqChild(SeqBase):
pass
schema = SeqChild.schema()
props = schema.get("properties", {})
assert props["items"]["type"] == "array"
assert props["items"]["items"]["type"] == "integer"
def test_schema_mapping_field(self):
class MapBase(BaseDiv):
type: str = Field(name="type", default="map_sch")
data: Mapping[str, Any] = Field()
class MapChild(MapBase):
pass
schema = MapChild.schema()
props = schema.get("properties", {})
assert props["data"]["type"] == "object"
assert props["data"]["additionalProperties"] is True
def test_schema_union_field(self):
class UnionBase(BaseDiv):
type: str = Field(name="type", default="union_sch")
val: Union[int, str] = Field()
class UnionChild(UnionBase):
pass
schema = UnionChild.schema()
props = schema.get("properties", {})
assert "anyOf" in props["val"]
type_values = [a["type"] for a in props["val"]["anyOf"]]
assert "integer" in type_values
assert "string" in type_values
def test_schema_enum_field(self):
class Direction(enum.Enum):
LEFT = "left"
RIGHT = "right"
class EnumBase(BaseDiv):
type: str = Field(name="type", default="enum_sch")
dir: Direction = Field()
class EnumChild(EnumBase):
pass
schema = EnumChild.schema()
props = schema.get("properties", {})
assert props["dir"]["type"] == "string"
assert set(props["dir"]["enum"]) == {"left", "right"}
def test_schema_nested_entity_field(self):
class Nested(BaseEntity):
x: int = Field(default=0)
class NestBase(BaseDiv):
type: str = Field(name="type", default="nest_sch")
child: Nested = Field()
class NestChild(NestBase):
pass
schema = NestChild.schema()
props = schema.get("properties", {})
assert "$ref" in props["child"]
assert "Nested" in props["child"]["$ref"]
assert "Nested" in schema["definitions"]
def test_schema_field_with_description_and_constraints(self):
class DescBase(BaseDiv):
type: str = Field(name="type", default="desc_sch")
count: int = Field(
description="Item count",
ge=0,
le=100,
)
class DescChild(DescBase):
pass
schema = DescChild.schema()
props = schema.get("properties", {})
count_schema = props["count"]
assert count_schema.get("description") == "Item count"
assert count_schema.get("minimum") == 0
assert count_schema.get("maximum") == 100
def test_schema_type_field(self):
"""The 'type' field with a default generates enum schema."""
class TypeBase(BaseDiv):
type: str = Field(name="type", default="type_sch")
val: int = Field()
class TypeChild(TypeBase):
pass
schema = TypeChild.schema()
props = schema.get("properties", {})
type_prop = props["type"]
assert type_prop["type"] == "string"
assert "enum" in type_prop
# ---------------------------------------------------------------------------
# Integration with generated DivKit classes
# ---------------------------------------------------------------------------
class TestIntegration:
def test_make_div_with_template(self):
from pydivkit import DivContainer, DivText, make_div
class MyTemplate(DivContainer):
label: str = Field()
items = [DivText(text=Ref(label))]
result = make_div(MyTemplate(items=[], label="hello"))
assert "templates" in result
assert "card" in result
tpl_name = MyTemplate.template_name
assert tpl_name in result["templates"]
def test_schema_with_generated_classes(self):
from pydivkit import DivContainer, DivText
class TextTemplate(DivContainer):
label: str = Field()
items = [DivText(text=Ref(label))]
schema = TextTemplate.schema()
assert schema["type"] == "object"
assert "definitions" in schema