| # 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. |
| |
| import dataclasses |
| from dataclasses import dataclass |
| import datetime |
| import enum |
| from typing import Dict, Any, List, Set, Optional, Tuple |
| |
| import pytest |
| import typing |
| |
| import pyfory |
| from pyfory import Fory |
| from pyfory.error import TypeUnregisteredError |
| from pyfory.struct import DataClassSerializer, build_default_values_factory |
| from pyfory.types import TypeId |
| |
| |
| def ser_de(fory, obj): |
| binary = fory.serialize(obj) |
| return fory.deserialize(binary) |
| |
| |
| @dataclass |
| class SimpleObject: |
| f1: Optional[Dict[pyfory.int32, pyfory.float64]] = None |
| |
| |
| @dataclass |
| class ComplexObject: |
| f1: Optional[Any] = None |
| f2: Optional[Any] = None |
| f3: pyfory.int8 = 0 |
| f4: pyfory.int16 = 0 |
| f5: pyfory.int32 = 0 |
| f6: pyfory.int64 = 0 |
| f7: pyfory.float32 = 0 |
| f8: pyfory.float64 = 0 |
| f9: Optional[List[pyfory.int16]] = None |
| f10: Optional[Dict[pyfory.int32, pyfory.float64]] = None |
| |
| |
| def test_struct(): |
| fory = Fory(xlang=True, ref=True) |
| fory.register_type(SimpleObject, typename="SimpleObject") |
| fory.register_type(ComplexObject, typename="example.ComplexObject") |
| o = SimpleObject(f1={1: 1.0 / 3}) |
| assert ser_de(fory, o) == o |
| |
| o = ComplexObject( |
| f1="str", |
| f2={"k1": -1, "k2": [1, 2]}, |
| f3=2**7 - 1, |
| f4=2**15 - 1, |
| f5=2**31 - 1, |
| f6=2**63 - 1, |
| f7=1.0 / 2, |
| f8=2.0 / 3, |
| f9=[1, 2], |
| f10={1: 1.0 / 3, 100: 2 / 7.0}, |
| ) |
| assert ser_de(fory, o) == o |
| with pytest.raises(AssertionError): |
| assert ser_de(fory, ComplexObject(f7=1.0 / 3)) == ComplexObject(f7=1.0 / 3) |
| with pytest.raises(OverflowError): |
| assert ser_de(fory, ComplexObject(f3=2**8)) == ComplexObject(f3=2**8) |
| with pytest.raises(OverflowError): |
| assert ser_de(fory, ComplexObject(f4=2**16)) == ComplexObject(f4=2**16) |
| with pytest.raises(OverflowError): |
| assert ser_de(fory, ComplexObject(f5=2**32)) == ComplexObject(f5=2**32) |
| with pytest.raises(OverflowError): |
| assert ser_de(fory, ComplexObject(f6=2**64)) == ComplexObject(f6=2**64) |
| |
| |
| @dataclass |
| class SuperClass1: |
| f1: Optional[Any] = None |
| f2: pyfory.int8 = 0 |
| |
| |
| @dataclass |
| class ChildClass1(SuperClass1): |
| f3: Optional[Dict[str, pyfory.float64]] = None |
| |
| |
| def test_strict(): |
| fory = Fory(xlang=False, ref=True) |
| obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3}) |
| with pytest.raises(TypeUnregisteredError): |
| fory.serialize(obj) |
| |
| |
| def test_inheritance(): |
| type_hints = typing.get_type_hints(ChildClass1) |
| print(type_hints) |
| assert type_hints.keys() == {"f1", "f2", "f3"} |
| fory = Fory(xlang=False, ref=True, strict=False) |
| obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3}) |
| assert ser_de(fory, obj) == obj |
| assert type(fory.type_resolver.get_serializer(ChildClass1)) is pyfory.DataClassSerializer |
| |
| |
| @dataclass |
| class DataClassObject: |
| f_int: int |
| f_float: float |
| f_str: str |
| f_bool: bool |
| f_list: List[int] |
| f_dict: Dict[str, float] |
| f_any: Optional[Any] |
| f_complex: Optional[ComplexObject] = None |
| |
| @classmethod |
| def create(cls): |
| return cls( |
| f_int=42, |
| f_float=3.14159, |
| f_str="test_codegen", |
| f_bool=True, |
| f_list=[1, 2, 3], |
| f_dict={"key": 1.5}, |
| f_any="any_data", |
| f_complex=None, |
| ) |
| |
| |
| @dataclass |
| class BoolCoercionObject: |
| b: bool |
| |
| |
| @dataclass(frozen=True) |
| class TupleFieldObject: |
| bar: Tuple[str, int] |
| |
| |
| @dataclass(frozen=True) |
| class XlangTupleFieldObject: |
| bar: Tuple[str, int] |
| |
| |
| @dataclass(frozen=True) |
| class XlangNestedTupleObject: |
| tuple_field: Tuple[List[int], Dict[str, int]] |
| list_of_tuples: List[Tuple[str, int]] |
| map_of_tuples: Dict[str, Tuple[str, int]] |
| set_of_tuples: Set[Tuple[str, int]] |
| tuple_of_tuples: Tuple[Tuple[str, int], Tuple[str, int]] |
| |
| |
| def test_sort_fields(): |
| @dataclass |
| class TestClass: |
| f1: pyfory.int32 |
| f2: List[pyfory.int16] |
| f3: Dict[str, pyfory.float64] |
| f4: str |
| f5: pyfory.float32 |
| f6: bytes |
| f7: bool |
| f8: Any |
| f9: Dict[pyfory.int32, pyfory.float64] |
| f10: List[str] |
| f11: pyfory.int8 |
| f12: pyfory.int64 |
| f13: pyfory.float64 |
| f14: Set[pyfory.int32] |
| f15: datetime.datetime |
| |
| fory = Fory(xlang=True, ref=True) |
| serializer = DataClassSerializer(fory.type_resolver, TestClass) |
| # Sorting order: |
| # 1. Non-compressed primitives (compress=0) by -size, then name: |
| # float64(8), float32(4), bool(1), int8(1) => f13, f5, f11, f7 |
| # (f11 < f7 alphabetically since '1' < '7') |
| # 2. Compressed primitives (compress=1) by -size, then name: |
| # int64(8), int32(4) => f12, f1 |
| # 3. Internal types by type_id, then name: str, datetime, bytes => f4, f15, f6 |
| # 4. Collection types by type_id, then name: list => f10, f2 |
| # 5. Set types by type_id, then name: set => f14 |
| # 6. Map types by type_id, then name: dict => f3, f9 |
| # 7. Other types (polymorphic/any) by name: any => f8 |
| assert serializer._field_names == ["f13", "f5", "f11", "f7", "f12", "f1", "f4", "f15", "f6", "f10", "f2", "f14", "f3", "f9", "f8"] |
| |
| |
| @pytest.mark.parametrize( |
| "value, expected", |
| [ |
| (1, True), |
| (0, False), |
| ], |
| ) |
| def test_bool_field_coercion(value, expected): |
| fory = Fory(xlang=False, ref=True, strict=False) |
| result = ser_de(fory, BoolCoercionObject(value)) |
| assert result.b is expected |
| |
| |
| def test_bool_field_coercion_numpy_bool(): |
| np = pytest.importorskip("numpy") |
| fory = Fory(xlang=False, ref=True, strict=False) |
| |
| result_true = ser_de(fory, BoolCoercionObject(np.bool_(True))) |
| assert result_true.b is True |
| |
| result_false = ser_de(fory, BoolCoercionObject(np.bool_(False))) |
| assert result_false.b is False |
| |
| |
| @pytest.mark.parametrize( |
| "numeric_type", |
| [ |
| pyfory.int8, |
| pyfory.int16, |
| pyfory.int32, |
| pyfory.fixed_int32, |
| pyfory.int64, |
| pyfory.fixed_int64, |
| pyfory.tagged_int64, |
| pyfory.uint8, |
| pyfory.uint16, |
| pyfory.uint32, |
| pyfory.fixed_uint32, |
| pyfory.uint64, |
| pyfory.fixed_uint64, |
| pyfory.tagged_uint64, |
| pyfory.float32, |
| pyfory.float64, |
| ], |
| ) |
| def test_numeric_serializer_need_to_write_ref_disabled(numeric_type): |
| fory = Fory(xlang=False, ref=True, strict=False) |
| serializer = fory.type_resolver.get_serializer(numeric_type) |
| assert serializer.need_to_write_ref is False |
| |
| |
| def test_data_class_serializer_xlang(): |
| fory = Fory(xlang=True, ref=True) |
| fory.register_type(ComplexObject, typename="example.ComplexObject") |
| fory.register_type(DataClassObject, typename="example.TestDataClassObject") |
| |
| complex_data = ComplexObject( |
| f1="nested_str", |
| f5=100, |
| f8=3.14, |
| f10={10: 1.0, 20: 2.0}, |
| ) |
| obj_original = DataClassObject( |
| f_int=123, |
| f_float=45.67, |
| f_str="hello xlang", |
| f_bool=True, |
| f_list=[1, 2, 3, 4, 5], |
| f_dict={"a": 1.1, "b": 2.2}, |
| f_any="any_value", |
| f_complex=complex_data, |
| ) |
| |
| obj_deserialized = ser_de(fory, obj_original) |
| |
| assert obj_deserialized == obj_original |
| assert obj_deserialized.f_int == obj_original.f_int |
| assert obj_deserialized.f_float == obj_original.f_float |
| assert obj_deserialized.f_str == obj_original.f_str |
| assert obj_deserialized.f_bool == obj_original.f_bool |
| assert obj_deserialized.f_list == obj_original.f_list |
| assert obj_deserialized.f_dict == obj_original.f_dict |
| assert obj_deserialized.f_any == obj_original.f_any |
| assert obj_deserialized.f_complex == obj_original.f_complex |
| assert type(fory.type_resolver.get_serializer(DataClassObject)) is pyfory.DataClassSerializer |
| # Ensure it's using xlang mode indirectly, by checking no JIT methods if possible, |
| # or by ensuring it was registered with _register_xtype which now uses DataClassSerializer. |
| # For now, the registration path check is implicit via xlang=True usage. |
| # We can also check if the hash is non-zero if it was computed, |
| # or if the _serializers attribute exists. |
| serializer_instance = fory.type_resolver.get_serializer(DataClassObject) |
| assert hasattr(serializer_instance, "_serializers") |
| assert not hasattr(serializer_instance, "_xlang") |
| |
| # Test with None for a complex field |
| obj_with_none_complex = DataClassObject( |
| f_int=789, |
| f_float=12.34, |
| f_str="another string", |
| f_bool=False, |
| f_list=[10, 20], |
| f_dict={"x": 7.7, "y": 8.8}, |
| f_any=None, |
| f_complex=None, |
| ) |
| obj_deserialized_none = ser_de(fory, obj_with_none_complex) |
| assert obj_deserialized_none == obj_with_none_complex |
| |
| |
| @pytest.mark.parametrize("track_ref", [False, True]) |
| def test_dataclass_with_typed_tuple_field(track_ref): |
| fory = Fory(xlang=False, ref=track_ref, strict=False) |
| obj = TupleFieldObject(bar=("a", 1)) |
| assert ser_de(fory, obj) == obj |
| |
| |
| @pytest.mark.parametrize("track_ref", [False, True]) |
| def test_xlang_dataclass_tuple_field(track_ref): |
| fory = Fory(xlang=True, ref=track_ref, strict=False) |
| fory.register_type(XlangTupleFieldObject, typename="example.XlangTupleFieldObject") |
| obj = XlangTupleFieldObject(bar=("a", 1)) |
| result = ser_de(fory, obj) |
| assert result == obj |
| assert isinstance(result.bar, tuple) |
| |
| |
| @pytest.mark.parametrize("track_ref", [False, True]) |
| def test_xlang_nested_tuple_container_fields(track_ref): |
| fory = Fory(xlang=True, ref=track_ref, strict=False) |
| fory.register_type(XlangNestedTupleObject, typename="example.XlangNestedTupleObject") |
| obj = XlangNestedTupleObject( |
| tuple_field=([1, 2], {"a": 1, "b": 2}), |
| list_of_tuples=[("a", 1), ("b", 2)], |
| map_of_tuples={"left": ("c", 3), "right": ("d", 4)}, |
| set_of_tuples={("e", 5), ("f", 6)}, |
| tuple_of_tuples=(("g", 7), ("h", 8)), |
| ) |
| result = ser_de(fory, obj) |
| assert result == obj |
| assert isinstance(result.tuple_field, tuple) |
| assert all(isinstance(value, tuple) for value in result.list_of_tuples) |
| assert all(isinstance(value, tuple) for value in result.map_of_tuples.values()) |
| assert all(isinstance(value, tuple) for value in result.set_of_tuples) |
| assert isinstance(result.tuple_of_tuples, tuple) |
| assert all(isinstance(value, tuple) for value in result.tuple_of_tuples) |
| |
| |
| def test_struct_evolving_override(): |
| @pyfory.dataclass |
| class EvolvingStruct: |
| f1: pyfory.int32 = 0 |
| |
| @pyfory.dataclass(evolving=False) |
| class FixedStruct: |
| f1: pyfory.int32 = 0 |
| |
| fory = Fory(xlang=True, compatible=True) |
| fory.register_type(EvolvingStruct, namespace="test", typename="EvolvingStruct") |
| fory.register_type(FixedStruct, namespace="test", typename="FixedStruct") |
| evolving_info = fory.type_resolver.get_type_info(EvolvingStruct) |
| fixed_info = fory.type_resolver.get_type_info(FixedStruct) |
| assert evolving_info.type_id == TypeId.NAMED_COMPATIBLE_STRUCT |
| assert fixed_info.type_id == TypeId.NAMED_STRUCT |
| |
| evolving = EvolvingStruct(f1=123) |
| fixed = FixedStruct(f1=123) |
| evolving_bytes = fory.serialize(evolving) |
| fixed_bytes = fory.serialize(fixed) |
| |
| assert len(fixed_bytes) < len(evolving_bytes) |
| assert fory.deserialize(evolving_bytes) == evolving |
| assert fory.deserialize(fixed_bytes) == fixed |
| |
| |
| def test_data_class_serializer_xlang_serializer(): |
| """Test DataClassSerializer round-trip behavior in xlang mode.""" |
| fory = Fory(xlang=True, ref=True) |
| |
| # Register types first |
| fory.register_type(ComplexObject, typename="example.ComplexObject") |
| fory.register_type(DataClassObject, typename="example.TestDataClassObject") |
| |
| # trigger lazy serializer replace |
| fory.serialize(DataClassObject.create()) |
| # Get the serializer that was created during registration |
| serializer = fory.type_resolver.get_serializer(DataClassObject) |
| |
| # Serializer API is unified: no mode-specific serializer attribute. |
| assert not hasattr(serializer, "_xlang") |
| assert hasattr(serializer, "_serializers") |
| assert len(serializer._serializers) == len(serializer._field_names) |
| |
| # Test that the generated methods work correctly through the normal serialization flow |
| test_obj = DataClassObject( |
| f_int=42, |
| f_float=3.14159, |
| f_str="test_codegen", |
| f_bool=True, |
| f_list=[1, 2, 3], |
| f_dict={"key": 1.5}, |
| f_any="any_data", |
| f_complex=None, |
| ) |
| |
| # Test serialization and deserialization using the normal fory flow |
| binary = fory.serialize(test_obj) |
| deserialized_obj = fory.deserialize(binary) |
| |
| # Verify the results |
| assert deserialized_obj.f_int == test_obj.f_int |
| assert deserialized_obj.f_float == test_obj.f_float |
| assert deserialized_obj.f_str == test_obj.f_str |
| assert deserialized_obj.f_bool == test_obj.f_bool |
| assert deserialized_obj.f_list == test_obj.f_list |
| assert deserialized_obj.f_dict == test_obj.f_dict |
| assert deserialized_obj.f_any == test_obj.f_any |
| assert deserialized_obj.f_complex == test_obj.f_complex |
| |
| |
| def test_data_class_serializer_xlang_vs_non_xlang(): |
| """Test that xlang and non-xlang modes use the same dataclass serializer behavior.""" |
| fory_xlang = Fory(xlang=True, ref=True) |
| fory_python = Fory(xlang=False, ref=True, strict=False) |
| |
| # Register types for xlang |
| fory_xlang.register_type(ComplexObject, typename="example.ComplexObject") |
| fory_xlang.register_type(DataClassObject, typename="example.TestDataClassObject") |
| |
| # trigger lazy serializer replace |
| fory_xlang.serialize(DataClassObject.create()) |
| # For Python mode, we can create the serializer directly since it doesn't require registration |
| serializer_xlang = fory_xlang.type_resolver.get_serializer(DataClassObject) |
| serializer_python = DataClassSerializer(fory_python.type_resolver, DataClassObject) |
| |
| assert not hasattr(serializer_xlang, "_xlang") |
| assert not hasattr(serializer_python, "_xlang") |
| |
| # Unified serializer metadata should be mode-independent. |
| assert serializer_xlang._field_names == serializer_python._field_names |
| assert serializer_xlang._nullable_fields == serializer_python._nullable_fields |
| assert serializer_xlang._dynamic_fields == serializer_python._dynamic_fields |
| assert serializer_xlang._hash == serializer_python._hash |
| |
| |
| class MissingDefaultEnum(enum.Enum): |
| A = 1 |
| B = 2 |
| |
| |
| @dataclass |
| class MissingDefaultFactoryFields: |
| required: int |
| required_float: float |
| required_str: str |
| required_bytes: bytes |
| required_list: List[int] |
| required_set: Set[int] |
| required_dict: Dict[str, int] |
| plain_default: int = 7 |
| list_default: List[int] = dataclasses.field(default_factory=list) |
| enum_default_none: MissingDefaultEnum = None |
| |
| |
| def test_build_default_values_factory(): |
| fory = Fory(xlang=False, ref=True, strict=False) |
| type_hints = typing.get_type_hints(MissingDefaultFactoryFields) |
| default_factories = build_default_values_factory( |
| fory, |
| type_hints, |
| dataclasses.fields(MissingDefaultFactoryFields), |
| ) |
| |
| assert callable(default_factories["required"]) |
| assert callable(default_factories["required_float"]) |
| assert callable(default_factories["required_str"]) |
| assert callable(default_factories["required_bytes"]) |
| assert callable(default_factories["required_list"]) |
| assert callable(default_factories["required_set"]) |
| assert callable(default_factories["required_dict"]) |
| assert callable(default_factories["plain_default"]) |
| assert callable(default_factories["list_default"]) |
| assert callable(default_factories["enum_default_none"]) |
| |
| assert default_factories["required"]() == 0 |
| assert default_factories["required_float"]() == 0.0 |
| assert default_factories["required_str"]() == "" |
| assert default_factories["required_bytes"]() == b"" |
| list_required_one = default_factories["required_list"]() |
| list_required_two = default_factories["required_list"]() |
| assert list_required_one == [] |
| assert list_required_two == [] |
| assert list_required_one is not list_required_two |
| set_required_one = default_factories["required_set"]() |
| set_required_two = default_factories["required_set"]() |
| assert set_required_one == set() |
| assert set_required_two == set() |
| assert set_required_one is not set_required_two |
| dict_required_one = default_factories["required_dict"]() |
| dict_required_two = default_factories["required_dict"]() |
| assert dict_required_one == {} |
| assert dict_required_two == {} |
| assert dict_required_one is not dict_required_two |
| assert default_factories["plain_default"]() == 7 |
| assert default_factories["enum_default_none"]() is MissingDefaultEnum.A |
| list_one = default_factories["list_default"]() |
| list_two = default_factories["list_default"]() |
| assert list_one == [] |
| assert list_two == [] |
| assert list_one is not list_two |
| |
| |
| @dataclass |
| class OptionalFieldsObject: |
| f1: Optional[int] = None |
| f2: Optional[str] = None |
| f3: Optional[List[int]] = None |
| f4: int = 0 |
| f5: str = "" |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| @pytest.mark.parametrize("compatible", [False, True]) |
| def test_optional_fields(xlang, compatible): |
| fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False) |
| if xlang: |
| fory.register_type(OptionalFieldsObject, typename="example.OptionalFieldsObject") |
| |
| obj_with_none = OptionalFieldsObject(f1=None, f2=None, f3=None, f4=42, f5="test") |
| result = ser_de(fory, obj_with_none) |
| assert result.f1 is None |
| assert result.f2 is None |
| assert result.f3 is None |
| assert result.f4 == 42 |
| assert result.f5 == "test" |
| |
| obj_with_values = OptionalFieldsObject(f1=100, f2="hello", f3=[1, 2, 3], f4=42, f5="test") |
| result = ser_de(fory, obj_with_values) |
| assert result.f1 == 100 |
| assert result.f2 == "hello" |
| assert result.f3 == [1, 2, 3] |
| assert result.f4 == 42 |
| assert result.f5 == "test" |
| |
| obj_mixed = OptionalFieldsObject(f1=100, f2=None, f3=[1, 2, 3], f4=42, f5="test") |
| result = ser_de(fory, obj_mixed) |
| assert result.f1 == 100 |
| assert result.f2 is None |
| assert result.f3 == [1, 2, 3] |
| assert result.f4 == 42 |
| assert result.f5 == "test" |
| |
| |
| @dataclass |
| class NestedOptionalObject: |
| f1: Optional[ComplexObject] = None |
| f2: Optional[Dict[str, int]] = None |
| f3: str = "" |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| @pytest.mark.parametrize("compatible", [False, True]) |
| def test_nested_optional_fields(xlang, compatible): |
| fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False) |
| if xlang: |
| fory.register_type(ComplexObject, typename="example.ComplexObject") |
| fory.register_type(NestedOptionalObject, typename="example.NestedOptionalObject") |
| |
| obj_with_none = NestedOptionalObject(f1=None, f2=None, f3="test") |
| result = ser_de(fory, obj_with_none) |
| assert result.f1 is None |
| assert result.f2 is None |
| assert result.f3 == "test" |
| |
| complex_obj = ComplexObject(f1="nested", f5=100, f8=3.14) |
| obj_with_values = NestedOptionalObject(f1=complex_obj, f2={"a": 1, "b": 2}, f3="test") |
| result = ser_de(fory, obj_with_values) |
| assert result.f1.f1 == "nested" |
| assert result.f1.f5 == 100 |
| assert result.f2 == {"a": 1, "b": 2} |
| assert result.f3 == "test" |
| |
| |
| @dataclass |
| class OptionalV1: |
| f1: Optional[int] = None |
| f2: str = "" |
| f3: Optional[List[int]] = None |
| |
| |
| @dataclass |
| class OptionalV2: |
| f1: Optional[int] = None |
| f2: str = "" |
| f3: Optional[List[int]] = None |
| f4: Optional[str] = None |
| |
| |
| @dataclass |
| class OptionalV3: |
| f1: Optional[int] = None |
| f2: str = "" |
| |
| |
| @dataclass |
| class CompatibleV1: |
| f1: int = 0 |
| f2: str = "" |
| f3: float = 0.0 |
| |
| |
| @dataclass |
| class CompatibleV2: |
| f1: int = 0 |
| f2: str = "" |
| f3: float = 0.0 |
| f4: bool = False |
| |
| |
| @dataclass |
| class CompatibleV3: |
| f1: int = 0 |
| f2: str = "" |
| |
| |
| @dataclass |
| class CompatibleRequiredFieldV1: |
| f1: int |
| |
| |
| @dataclass |
| class CompatibleRequiredFieldV2: |
| f1: int |
| f2: int |
| |
| |
| @dataclass |
| class CompatibleRequiredDefaultsV1: |
| f1: int |
| |
| |
| @dataclass |
| class CompatibleRequiredDefaultsV2: |
| f1: int |
| f_int: int |
| f_float: float |
| f_str: str |
| f_bytes: bytes |
| f_list: List[int] |
| f_set: Set[int] |
| f_dict: Dict[str, int] |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_add_field(xlang): |
| """Test that adding a field with default value works in compatible mode.""" |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleV1, typename="example.Compatible") |
| fory_v2.register_type(CompatibleV2, typename="example.Compatible") |
| |
| # V1 object serialized |
| v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14) |
| v1_binary = fory_v1.serialize(v1_obj) |
| |
| # V2 can read V1 data, new field gets default value |
| v2_result = fory_v2.deserialize(v1_binary) |
| assert v2_result.f1 == 100 |
| assert v2_result.f2 == "test" |
| assert v2_result.f3 == 3.14 |
| assert v2_result.f4 is False # Default value |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_remove_field(xlang): |
| """Test that removing a field works in compatible mode.""" |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v3 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v2.register_type(CompatibleV2, typename="example.Compatible") |
| fory_v3.register_type(CompatibleV3, typename="example.Compatible") |
| |
| # V2 object with all fields |
| v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True) |
| v2_binary = fory_v2.serialize(v2_obj) |
| |
| # V3 can read V2 data, extra fields are ignored |
| v3_result = fory_v3.deserialize(v2_binary) |
| assert v3_result.f1 == 200 |
| assert v3_result.f2 == "hello" |
| # f3 and f4 from V2 are ignored |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_bidirectional(xlang): |
| """Test bidirectional compatible serialization.""" |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleV1, typename="example.Compatible") |
| fory_v2.register_type(CompatibleV2, typename="example.Compatible") |
| |
| # V1 -> V2 |
| v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14) |
| v1_binary = fory_v1.serialize(v1_obj) |
| v2_result = fory_v2.deserialize(v1_binary) |
| assert v2_result.f1 == 100 |
| assert v2_result.f2 == "test" |
| assert v2_result.f3 == 3.14 |
| assert v2_result.f4 is False |
| |
| # V2 -> V1 |
| v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True) |
| v2_binary = fory_v2.serialize(v2_obj) |
| v1_result = fory_v1.deserialize(v2_binary) |
| assert v1_result.f1 == 200 |
| assert v1_result.f2 == "hello" |
| assert v1_result.f3 == 2.71 |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_add_required_field_without_default_uses_zero_value(xlang): |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleRequiredFieldV1, typename="example.CompatibleRequiredField") |
| fory_v2.register_type(CompatibleRequiredFieldV2, typename="example.CompatibleRequiredField") |
| |
| v1_binary = fory_v1.serialize(CompatibleRequiredFieldV1(f1=321)) |
| v2_result = fory_v2.deserialize(v1_binary) |
| |
| assert v2_result.f1 == 321 |
| assert hasattr(v2_result, "f2") |
| assert v2_result.f2 == 0 |
| |
| serializer_v2 = fory_v2.type_resolver.get_serializer(CompatibleRequiredFieldV2) |
| assert hasattr(serializer_v2, "_default_values_factory") |
| assert callable(serializer_v2._default_values_factory["f2"]) |
| assert serializer_v2._default_values_factory["f2"]() == 0 |
| assert ser_de(fory_v2, v2_result) == v2_result |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_add_required_fields_use_type_defaults(xlang): |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleRequiredDefaultsV1, typename="example.CompatibleRequiredDefaults") |
| fory_v2.register_type(CompatibleRequiredDefaultsV2, typename="example.CompatibleRequiredDefaults") |
| |
| v1_binary = fory_v1.serialize(CompatibleRequiredDefaultsV1(f1=11)) |
| v2_result = fory_v2.deserialize(v1_binary) |
| |
| assert v2_result.f1 == 11 |
| assert v2_result.f_int == 0 |
| assert v2_result.f_float == 0.0 |
| assert v2_result.f_str == "" |
| assert v2_result.f_bytes == b"" |
| assert v2_result.f_list == [] |
| assert v2_result.f_set == set() |
| assert v2_result.f_dict == {} |
| assert ser_de(fory_v2, v2_result) == v2_result |
| |
| |
| @dataclass |
| class CompatibleWithOptional: |
| f1: Optional[int] = None |
| f2: str = "" |
| f3: Optional[List[int]] = None |
| |
| |
| @dataclass |
| class CompatibleWithOptionalV2: |
| f1: Optional[int] = None |
| f2: str = "" |
| f3: Optional[List[int]] = None |
| f4: Optional[str] = None |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_with_optional_fields(xlang): |
| """Test compatible mode with optional fields.""" |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleWithOptional, typename="example.CompatibleOptional") |
| fory_v2.register_type(CompatibleWithOptionalV2, typename="example.CompatibleOptional") |
| |
| # V1 with None values |
| v1_obj = CompatibleWithOptional(f1=None, f2="test", f3=None) |
| v1_binary = fory_v1.serialize(v1_obj) |
| v2_result = fory_v2.deserialize(v1_binary) |
| assert v2_result.f1 is None |
| assert v2_result.f2 == "test" |
| assert v2_result.f3 is None |
| assert v2_result.f4 is None |
| |
| # V1 with values |
| v1_obj2 = CompatibleWithOptional(f1=100, f2="test", f3=[1, 2, 3]) |
| v1_binary2 = fory_v1.serialize(v1_obj2) |
| v2_result2 = fory_v2.deserialize(v1_binary2) |
| assert v2_result2.f1 == 100 |
| assert v2_result2.f2 == "test" |
| assert v2_result2.f3 == [1, 2, 3] |
| assert v2_result2.f4 is None |
| |
| |
| @dataclass |
| class CompatibleAllTypes: |
| f_int: int = 0 |
| f_str: str = "" |
| f_float: float = 0.0 |
| f_bool: bool = False |
| f_list: Optional[List[int]] = None |
| f_dict: Optional[Dict[str, int]] = None |
| |
| |
| @dataclass |
| class CompatibleAllTypesV2: |
| f_int: int = 0 |
| f_str: str = "" |
| f_float: float = 0.0 |
| f_bool: bool = False |
| f_list: Optional[List[int]] = None |
| f_dict: Optional[Dict[str, int]] = None |
| f_new: str = "default" |
| |
| |
| @pytest.mark.parametrize("xlang", [False, True]) |
| def test_compatible_mode_all_basic_types(xlang): |
| """Test compatible mode with all basic types.""" |
| fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False) |
| |
| fory_v1.register_type(CompatibleAllTypes, typename="example.CompatibleAllTypes") |
| fory_v2.register_type(CompatibleAllTypesV2, typename="example.CompatibleAllTypes") |
| |
| v1_obj = CompatibleAllTypes(f_int=42, f_str="hello", f_float=3.14, f_bool=True, f_list=[1, 2, 3], f_dict={"a": 1, "b": 2}) |
| v1_binary = fory_v1.serialize(v1_obj) |
| v2_result = fory_v2.deserialize(v1_binary) |
| |
| assert v2_result.f_int == 42 |
| assert v2_result.f_str == "hello" |
| assert v2_result.f_float == 3.14 |
| assert v2_result.f_bool is True |
| assert v2_result.f_list == [1, 2, 3] |
| assert v2_result.f_dict == {"a": 1, "b": 2} |
| assert v2_result.f_new == "default" |
| |
| |
| def test_optional_compatible_mode_evolution(): |
| fory_v1 = Fory(xlang=True, ref=True, compatible=True) |
| fory_v2 = Fory(xlang=True, ref=True, compatible=True) |
| fory_v3 = Fory(xlang=True, ref=True, compatible=True) |
| |
| fory_v1.register_type(OptionalV1, typename="example.OptionalVersioned") |
| fory_v2.register_type(OptionalV2, typename="example.OptionalVersioned") |
| fory_v3.register_type(OptionalV3, typename="example.OptionalVersioned") |
| |
| v1_obj = OptionalV1(f1=100, f2="test", f3=[1, 2, 3]) |
| v1_binary = fory_v1.serialize(v1_obj) |
| |
| v2_result = fory_v2.deserialize(v1_binary) |
| assert v2_result.f1 == 100 |
| assert v2_result.f2 == "test" |
| assert v2_result.f3 == [1, 2, 3] |
| assert v2_result.f4 is None |
| |
| v1_obj_with_none = OptionalV1(f1=None, f2="test", f3=None) |
| v1_binary_with_none = fory_v1.serialize(v1_obj_with_none) |
| |
| v2_result_with_none = fory_v2.deserialize(v1_binary_with_none) |
| assert v2_result_with_none.f1 is None |
| assert v2_result_with_none.f2 == "test" |
| assert v2_result_with_none.f3 is None |
| assert v2_result_with_none.f4 is None |
| |
| v2_obj = OptionalV2(f1=200, f2="test2", f3=[4, 5], f4="extra") |
| v2_binary = fory_v2.serialize(v2_obj) |
| |
| v3_result = fory_v3.deserialize(v2_binary) |
| assert v3_result.f1 == 200 |
| assert v3_result.f2 == "test2" |
| |
| v2_obj_partial_none = OptionalV2(f1=None, f2="test2", f3=None, f4=None) |
| v2_binary_partial_none = fory_v2.serialize(v2_obj_partial_none) |
| |
| v3_result_partial_none = fory_v3.deserialize(v2_binary_partial_none) |
| assert v3_result_partial_none.f1 is None |
| assert v3_result_partial_none.f2 == "test2" |
| |
| v3_obj = OptionalV3(f1=300, f2="test3") |
| v3_binary = fory_v3.serialize(v3_obj) |
| |
| v1_result = fory_v1.deserialize(v3_binary) |
| assert v1_result.f1 == 300 |
| assert v1_result.f2 == "test3" |
| assert v1_result.f3 is None |
| |
| |
| # ============================================================================ |
| # Tests for dynamic field configuration |
| # ============================================================================ |
| |
| |
| @dataclass |
| class Animal: |
| name: str = pyfory.field(id=0, default="") |
| |
| |
| @dataclass |
| class Dog(Animal): |
| breed: str = pyfory.field(id=1, default="") |
| |
| |
| @dataclass |
| class Zoo: |
| # dynamic=True: can hold Dog instance in Animal field |
| animal: Animal = pyfory.field(id=0, dynamic=True) |
| # dynamic=False: use declared type's serializer, subclass info lost |
| animal2: Animal = pyfory.field(id=1, dynamic=False) |
| |
| |
| def test_dynamic_with_inheritance(): |
| """Test dynamic=True allows polymorphic serialization with inheritance.""" |
| fory = Fory(xlang=False, ref=True, strict=False) |
| fory.register_type(Animal) |
| fory.register_type(Dog) |
| fory.register_type(Zoo) |
| |
| dog1 = Dog(name="Buddy", breed="Labrador") |
| dog2 = Dog(name="Rex", breed="German Shepherd") |
| zoo = Zoo(animal=dog1, animal2=dog2) |
| |
| result = ser_de(fory, zoo) |
| # dynamic=True: Dog type preserved |
| assert isinstance(result.animal, Dog) |
| assert result.animal.name == "Buddy" |
| assert result.animal.breed == "Labrador" |
| # dynamic=False: subclass info lost, only Animal fields deserialized |
| assert isinstance(result.animal2, Animal) |
| assert not isinstance(result.animal2, Dog) |
| assert result.animal2.name == "Rex" |
| assert not hasattr(result.animal2, "breed") or getattr(result.animal2, "breed", None) != "German Shepherd" |
| |
| |
| def test_dynamic_with_inheritance_xlang(): |
| """Test dynamic=True allows polymorphic serialization in xlang mode.""" |
| fory = Fory(xlang=True, ref=True) |
| fory.register_type(Animal, typename="example.Animal") |
| fory.register_type(Dog, typename="example.Dog") |
| fory.register_type(Zoo, typename="example.Zoo") |
| |
| dog1 = Dog(name="Max", breed="Husky") |
| dog2 = Dog(name="Luna", breed="Poodle") |
| zoo = Zoo(animal=dog1, animal2=dog2) |
| |
| result = ser_de(fory, zoo) |
| # dynamic=True: Dog type preserved |
| assert isinstance(result.animal, Dog) |
| assert result.animal.name == "Max" |
| assert result.animal.breed == "Husky" |
| # dynamic=False: subclass info lost, only Animal fields deserialized |
| assert isinstance(result.animal2, Animal) |
| assert not isinstance(result.animal2, Dog) |
| assert result.animal2.name == "Luna" |
| assert not hasattr(result.animal2, "breed") or getattr(result.animal2, "breed", None) != "Poodle" |