Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions python/cocoindex/tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import datetime
from dataclasses import dataclass
import pytest
from cocoindex.convert import to_engine_value, make_engine_value_converter
from cocoindex.typing import encode_enriched_type
from cocoindex.convert import to_engine_value
from cocoindex.convert import make_engine_value_converter

@dataclass
class Order:
Expand Down Expand Up @@ -33,13 +34,13 @@ class NestedStruct:
orders: list[Order]
count: int = 0

def build_converter(py_type, target_type=None):
def build_engine_value_converter(engine_type_in_py, python_type=None):
"""
Helper to build a converter for the given Python type.
If target_type is not specified, uses py_type as the target.
Helper to build a converter for the given engine-side type (as represented in Python).
If python_type is not specified, uses engine_type_in_py as the target.
"""
engine_type = encode_enriched_type(py_type)["type"]
return make_engine_value_converter([], engine_type, target_type or py_type)
engine_type = encode_enriched_type(engine_type_in_py)["type"]
return make_engine_value_converter([], engine_type, python_type or engine_type_in_py)

def test_to_engine_value_basic_types():
assert to_engine_value(123) == 123
Expand Down Expand Up @@ -90,18 +91,18 @@ def test_to_engine_value_none():
assert to_engine_value(None) is None

def test_make_engine_value_converter_basic_types():
for py_type, value in [
for engine_type_in_py, value in [
(int, 42),
(float, 3.14),
(str, "hello"),
(bool, True),
# (type(None), None), # Removed unsupported NoneType
]:
converter = build_converter(py_type)
converter = build_engine_value_converter(engine_type_in_py)
assert converter(value) == value

def test_make_engine_value_converter_struct():
converter = build_converter(Order)
converter = build_engine_value_converter(Order)
# All fields match
engine_val = ["O123", "mixed nuts", 25.0, "default_extra"]
assert converter(engine_val) == Order("O123", "mixed nuts", 25.0, "default_extra")
Expand All @@ -121,25 +122,25 @@ def test_make_engine_value_converter_struct():
def test_make_engine_value_converter_struct_field_order():
# Engine fields in different order
# Use encode_enriched_type to avoid manual mistakes
converter = build_converter(Order)
converter = build_engine_value_converter(Order)
# Provide all fields in the correct order
engine_val = ["O123", "mixed nuts", 25.0, "default_extra"]
assert converter(engine_val) == Order("O123", "mixed nuts", 25.0, "default_extra")

def test_make_engine_value_converter_collections():
# List of structs
converter = build_converter(list[Order])
converter = build_engine_value_converter(list[Order])
engine_val = [
["O1", "item1", 10.0, "default_extra"],
["O2", "item2", 20.0, "default_extra"]
]
assert converter(engine_val) == [Order("O1", "item1", 10.0, "default_extra"), Order("O2", "item2", 20.0, "default_extra")]
# Struct with list field
converter = build_converter(Customer)
converter = build_engine_value_converter(Customer)
engine_val = ["Alice", ["O1", "item1", 10.0, "default_extra"], [["vip"], ["premium"]]]
assert converter(engine_val) == Customer("Alice", Order("O1", "item1", 10.0, "default_extra"), [Tag("vip"), Tag("premium")])
# Struct with struct field
converter = build_converter(NestedStruct)
converter = build_engine_value_converter(NestedStruct)
engine_val = [
["Alice", ["O1", "item1", 10.0, "default_extra"], [["vip"]]],
[["O1", "item1", 10.0, "default_extra"], ["O2", "item2", 20.0, "default_extra"]],
Expand All @@ -153,9 +154,66 @@ def test_make_engine_value_converter_collections():

def test_make_engine_value_converter_defaults_and_missing_fields():
# Missing optional field in engine value
converter = build_converter(Customer)
converter = build_engine_value_converter(Customer)
engine_val = ["Alice", ["O1", "item1", 10.0, "default_extra"], None] # tags explicitly None
assert converter(engine_val) == Customer("Alice", Order("O1", "item1", 10.0, "default_extra"), None)
# Extra field in engine value (should ignore)
engine_val = ["Alice", ["O1", "item1", 10.0, "default_extra"], [["vip"]], "extra"]
assert converter(engine_val) == Customer("Alice", Order("O1", "item1", 10.0, "default_extra"), [Tag("vip")])

def test_engine_python_schema_field_order():
"""
Engine and Python dataclasses have the same fields but different order.
Converter should map by field name, not order.
"""
@dataclass
class EngineOrder:
id: str
name: str
price: float
@dataclass
class PythonOrder:
name: str
id: str
price: float
extra: str = "default"
converter = build_engine_value_converter(EngineOrder, PythonOrder)
engine_val = ["O123", "mixed nuts", 25.0] # matches EngineOrder order
assert converter(engine_val) == PythonOrder("mixed nuts", "O123", 25.0, "default")

def test_engine_python_schema_extra_field():
"""
Python dataclass has an extra field not present in engine schema.
Converter should fill with default value.
"""
@dataclass
class EngineOrder:
id: str
name: str
@dataclass
class PythonOrder:
id: str
name: str
price: float = 0.0
converter = build_engine_value_converter(EngineOrder, PythonOrder)
engine_val = ["O123", "mixed nuts"]
assert converter(engine_val) == PythonOrder("O123", "mixed nuts", 0.0)

def test_engine_python_schema_missing_field():
"""
Engine dataclass has a field missing in Python dataclass.
Converter should ignore the missing field.
"""
from dataclasses import dataclass
@dataclass
class EngineOrder:
id: str
name: str
price: float
@dataclass
class PythonOrder:
id: str
name: str
converter = build_engine_value_converter(EngineOrder, PythonOrder)
engine_val = ["O123", "mixed nuts", 25.0]
assert converter(engine_val) == PythonOrder("O123", "mixed nuts")