This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 72b16e516 fix(python): fix collection null elements read/write (#3149)
72b16e516 is described below

commit 72b16e5162c3af7335d4d08232d690872412afe1
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jan 14 19:35:03 2026 +0800

    fix(python): fix collection null elements read/write (#3149)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    Closes #3145
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 python/pyfory/collection.pxi           |  89 ++++++--
 python/pyfory/collection.py            |  44 +++-
 python/pyfory/registry.py              |   4 +-
 python/pyfory/tests/test_collection.py | 376 +++++++++++++++++++++++++++++++++
 4 files changed, 489 insertions(+), 24 deletions(-)

diff --git a/python/pyfory/collection.pxi b/python/pyfory/collection.pxi
index 7c335c6e7..45b95654d 100644
--- a/python/pyfory/collection.pxi
+++ b/python/pyfory/collection.pxi
@@ -112,19 +112,23 @@ cdef class CollectionSerializer(Serializer):
         cdef c_bool tracking_ref
         cdef c_bool has_null
         if (collect_flag & COLL_IS_SAME_TYPE) != 0:
-            if elem_type is str:
-                self._write_string(buffer, value)
-            elif serializer is Int64Serializer:
-                self._write_int(buffer, value)
-            elif elem_type is bool:
-                self._write_bool(buffer, value)
-            elif serializer is Float64Serializer:
-                self._write_float(buffer, value)
-            else:
-                if (collect_flag & COLL_TRACKING_REF) == 0:
+            if (collect_flag & COLL_HAS_NULL) == 0:
+                if elem_type is str:
+                    self._write_string(buffer, value)
+                elif serializer is Int64Serializer:
+                    self._write_int(buffer, value)
+                elif elem_type is bool:
+                    self._write_bool(buffer, value)
+                elif serializer is Float64Serializer:
+                    self._write_float(buffer, value)
+                elif (collect_flag & COLL_TRACKING_REF) == 0:
                     self._write_same_type_no_ref(buffer, value, elem_typeinfo)
                 else:
                     self._write_same_type_ref(buffer, value, elem_typeinfo)
+            elif (collect_flag & COLL_TRACKING_REF) != 0:
+                self._write_same_type_ref(buffer, value, elem_typeinfo)
+            else:
+                self._write_same_type_has_null(buffer, value, elem_typeinfo)
         else:
             # Check tracking_ref and has_null flags for different types writing
             tracking_ref = (collect_flag & COLL_TRACKING_REF) != 0
@@ -248,6 +252,41 @@ cdef class CollectionSerializer(Serializer):
                 self._add_element(collection_, i, obj)
         self.fory.dec_depth()
 
+    cpdef _write_same_type_has_null(self, Buffer buffer, value, TypeInfo 
typeinfo):
+        if self.is_py:
+            for s in value:
+                if s is None:
+                    buffer.write_int8(NULL_FLAG)
+                else:
+                    buffer.write_int8(NOT_NULL_VALUE_FLAG)
+                    typeinfo.serializer.write(buffer, s)
+        else:
+            for s in value:
+                if s is None:
+                    buffer.write_int8(NULL_FLAG)
+                else:
+                    buffer.write_int8(NOT_NULL_VALUE_FLAG)
+                    typeinfo.serializer.xwrite(buffer, s)
+
+    cpdef _read_same_type_has_null(self, Buffer buffer, int64_t len_, object 
collection_, TypeInfo typeinfo):
+        cdef int8_t flag
+        self.fory.inc_depth()
+        if self.is_py:
+            for i in range(len_):
+                flag = buffer.read_int8()
+                if flag == NULL_FLAG:
+                    self._add_element(collection_, i, None)
+                else:
+                    self._add_element(collection_, i, 
typeinfo.serializer.read(buffer))
+        else:
+            for i in range(len_):
+                flag = buffer.read_int8()
+                if flag == NULL_FLAG:
+                    self._add_element(collection_, i, None)
+                else:
+                    self._add_element(collection_, i, 
typeinfo.serializer.xread(buffer))
+        self.fory.dec_depth()
+
     cpdef _write_same_type_ref(self, Buffer buffer, value, TypeInfo typeinfo):
         cdef MapRefResolver ref_resolver = self.ref_resolver
         cdef TypeResolver type_resolver = self.type_resolver
@@ -319,10 +358,14 @@ cdef class ListSerializer(CollectionSerializer):
                 elif type_id == <int32_t>TypeId.FLOAT64:
                     self._read_float(buffer, len_, list_)
                     return list_
-            if (collect_flag & COLL_TRACKING_REF) == 0:
-                self._read_same_type_no_ref(buffer, len_, list_, typeinfo)
-            else:
+                elif (collect_flag & COLL_TRACKING_REF) == 0:
+                    self._read_same_type_no_ref(buffer, len_, list_, typeinfo)
+                else:
+                    self._read_same_type_ref(buffer, len_, list_, typeinfo)
+            elif (collect_flag & COLL_TRACKING_REF) != 0:
                 self._read_same_type_ref(buffer, len_, list_, typeinfo)
+            else:
+                self._read_same_type_has_null(buffer, len_, list_, typeinfo)
         else:
             self.fory.inc_depth()
             # Check tracking_ref and has_null flags for different types 
handling
@@ -437,10 +480,14 @@ cdef class TupleSerializer(CollectionSerializer):
                 if type_id == <int32_t>TypeId.FLOAT64:
                     self._read_float(buffer, len_, tuple_)
                     return tuple_
-            if (collect_flag & COLL_TRACKING_REF) == 0:
-                self._read_same_type_no_ref(buffer, len_, tuple_, typeinfo)
-            else:
+                elif (collect_flag & COLL_TRACKING_REF) == 0:
+                    self._read_same_type_no_ref(buffer, len_, tuple_, typeinfo)
+                else:
+                    self._read_same_type_ref(buffer, len_, tuple_, typeinfo)
+            elif (collect_flag & COLL_TRACKING_REF) != 0:
                 self._read_same_type_ref(buffer, len_, tuple_, typeinfo)
+            else:
+                self._read_same_type_has_null(buffer, len_, tuple_, typeinfo)
         else:
             self.fory.inc_depth()
             # Check tracking_ref and has_null flags for different types 
handling
@@ -530,10 +577,14 @@ cdef class SetSerializer(CollectionSerializer):
                 if type_id == <int32_t>TypeId.FLOAT64:
                     self._read_float(buffer, len_, instance)
                     return instance
-            if (collect_flag & COLL_TRACKING_REF) == 0:
-                self._read_same_type_no_ref(buffer, len_, instance, typeinfo)
-            else:
+                elif (collect_flag & COLL_TRACKING_REF) == 0:
+                    self._read_same_type_no_ref(buffer, len_, instance, 
typeinfo)
+                else:
+                    self._read_same_type_ref(buffer, len_, instance, typeinfo)
+            elif (collect_flag & COLL_TRACKING_REF) != 0:
                 self._read_same_type_ref(buffer, len_, instance, typeinfo)
+            else:
+                self._read_same_type_has_null(buffer, len_, instance, typeinfo)
         else:
             self.fory.inc_depth()
             # Check tracking_ref and has_null flags for different types 
handling
diff --git a/python/pyfory/collection.py b/python/pyfory/collection.py
index f8a026ddc..ac4efd6c5 100644
--- a/python/pyfory/collection.py
+++ b/python/pyfory/collection.py
@@ -114,10 +114,12 @@ class CollectionSerializer(Serializer):
             return
         collect_flag, typeinfo = self.write_header(buffer, value)
         if (collect_flag & COLL_IS_SAME_TYPE) != 0:
-            if (collect_flag & COLL_TRACKING_REF) == 0:
+            if (collect_flag & COLL_TRACKING_REF) != 0:
+                self._write_same_type_ref(buffer, value, typeinfo)
+            elif (collect_flag & COLL_HAS_NULL) == 0:
                 self._write_same_type_no_ref(buffer, value, typeinfo)
             else:
-                self._write_same_type_ref(buffer, value, typeinfo)
+                self._write_same_type_has_null(buffer, value, typeinfo)
         else:
             self._write_different_types(buffer, value, collect_flag)
 
@@ -129,6 +131,22 @@ class CollectionSerializer(Serializer):
             for s in value:
                 typeinfo.serializer.xwrite(buffer, s)
 
+    def _write_same_type_has_null(self, buffer, value, typeinfo):
+        if self.is_py:
+            for s in value:
+                if s is None:
+                    buffer.write_int8(NULL_FLAG)
+                else:
+                    buffer.write_int8(NOT_NULL_VALUE_FLAG)
+                    typeinfo.serializer.write(buffer, s)
+        else:
+            for s in value:
+                if s is None:
+                    buffer.write_int8(NULL_FLAG)
+                else:
+                    buffer.write_int8(NOT_NULL_VALUE_FLAG)
+                    typeinfo.serializer.xwrite(buffer, s)
+
     def _write_same_type_ref(self, buffer, value, typeinfo):
         if self.is_py:
             for s in value:
@@ -186,10 +204,12 @@ class CollectionSerializer(Serializer):
                 typeinfo = self.type_resolver.read_typeinfo(buffer)
             else:
                 typeinfo = self.elem_typeinfo
-            if (collect_flag & COLL_TRACKING_REF) == 0:
+            if (collect_flag & COLL_TRACKING_REF) != 0:
+                self._read_same_type_ref(buffer, len_, collection_, typeinfo)
+            elif (collect_flag & COLL_HAS_NULL) == 0:
                 self._read_same_type_no_ref(buffer, len_, collection_, 
typeinfo)
             else:
-                self._read_same_type_ref(buffer, len_, collection_, typeinfo)
+                self._read_same_type_has_null(buffer, len_, collection_, 
typeinfo)
         else:
             self._read_different_types(buffer, len_, collection_, collect_flag)
         return collection_
@@ -210,6 +230,22 @@ class CollectionSerializer(Serializer):
                 self._add_element(collection_, 
typeinfo.serializer.xread(buffer))
         self.fory.dec_depth()
 
+    def _read_same_type_has_null(self, buffer, len_, collection_, typeinfo):
+        self.fory.inc_depth()
+        if self.is_py:
+            for _ in range(len_):
+                if buffer.read_int8() == NULL_FLAG:
+                    self._add_element(collection_, None)
+                else:
+                    self._add_element(collection_, 
typeinfo.serializer.read(buffer))
+        else:
+            for _ in range(len_):
+                if buffer.read_int8() == NULL_FLAG:
+                    self._add_element(collection_, None)
+                else:
+                    self._add_element(collection_, 
typeinfo.serializer.xread(buffer))
+        self.fory.dec_depth()
+
     def _read_same_type_ref(self, buffer, len_, collection_, typeinfo):
         self.fory.inc_depth()
         for _ in range(len_):
diff --git a/python/pyfory/registry.py b/python/pyfory/registry.py
index daec5fc9b..79c16311c 100644
--- a/python/pyfory/registry.py
+++ b/python/pyfory/registry.py
@@ -279,7 +279,9 @@ class TypeResolver:
 
     def _initialize_common(self):
         register = functools.partial(self._register_type, internal=True)
-        register(None, type_id=TypeId.UNKNOWN, serializer=NoneSerializer)
+        register(type(None), type_id=TypeId.NONE, serializer=NoneSerializer)
+        # Also register None value to map to type(None) for get_typeinfo(None) 
calls
+        self._types_info[None] = self._types_info[type(None)]
         register(bool, type_id=TypeId.BOOL, serializer=BooleanSerializer)
         # Signed integers
         # Note: int32/int64 use VARINT32/VARINT64 for xlang compatibility 
(matches Java/Rust)
diff --git a/python/pyfory/tests/test_collection.py 
b/python/pyfory/tests/test_collection.py
new file mode 100644
index 000000000..93cb8ff2a
--- /dev/null
+++ b/python/pyfory/tests/test_collection.py
@@ -0,0 +1,376 @@
+# 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.
+
+"""
+Test cases for collection serialization edge cases including None handling.
+"""
+
+from dataclasses import dataclass
+from typing import List, Dict, Set, Optional
+
+import pytest
+
+import pyfory
+
+
+class TestListWithNone:
+    """Test list serialization with None elements."""
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_with_single_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [None]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_with_multiple_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [None, None, None]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_with_mixed_none_and_int(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [1, None, 2, None, 3]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_with_mixed_none_and_string(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = ["a", None, "b", None]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_nested_list_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [5, [5, None]]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_deeply_nested_list_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [1, [2, [3, None, [4, None]]]]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+
+class TestSetWithNone:
+    """Test set serialization with None elements."""
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_set_with_single_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {None}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_set_with_none_and_values(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {None, 1, 2, 3}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_set_with_none_and_strings(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {None, "a", "b"}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+
+class TestTupleWithNone:
+    """Test tuple serialization with None elements."""
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_tuple_with_single_none(self, ref):
+        # Tuple is Python-only, xlang=False
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        data = (None,)
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_tuple_with_multiple_none(self, ref):
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        data = (None, None, None)
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_tuple_with_mixed_none(self, ref):
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        data = (None, 1, None, "a")
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_nested_tuple_with_none(self, ref):
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        data = (1, (2, None, (3, None)))
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+
+class TestDictWithNone:
+    """Test dict serialization with None keys/values."""
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_with_none_value(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"key": None}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_with_none_key(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {None: "value"}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_with_none_key_and_value(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {None: None}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_with_list_containing_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"a": [None]}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_with_multiple_none_values(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"a": None, "b": None, "c": 1}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_nested_dict_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"outer": {"inner": None, "list": [1, None, 3]}}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+
+class TestComplexNestedStructures:
+    """Test complex nested structures with None."""
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_complex_nested_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"a": [1, None, 3], "b": None, "c": [None, None]}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_of_dicts_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [{"a": None}, {"b": [None, 1]}, None]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_dict_of_sets_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {"set1": {1, None, 2}, "set2": {None}}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+
+class TestStructWithCollections:
+    """Test dataclass/struct fields containing collections with None."""
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_struct_with_list_containing_none(self, ref):
+        # Struct tests are Python-only
+        @dataclass
+        class MyStruct:
+            items: List[Optional[int]]
+
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        fory.register(MyStruct)
+        data = MyStruct(items=[1, None, 3])
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_struct_with_dict_field(self, ref):
+        @dataclass
+        class MyStruct:
+            mapping: Dict[Optional[str], Optional[int]]
+
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        fory.register(MyStruct)
+        # Test normal dict
+        data = MyStruct(mapping={"a": 1, "b": 2})
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+        # Test with None value
+        data2 = MyStruct(mapping={"a": None, "b": 2})
+        result2 = fory.loads(fory.dumps(data2))
+        assert result2 == data2
+        # Test with None key
+        data3 = MyStruct(mapping={None: 1, "b": 2})
+        result3 = fory.loads(fory.dumps(data3))
+        assert result3 == data3
+        # Test with both None key and value
+        data4 = MyStruct(mapping={None: None, "a": 1})
+        result4 = fory.loads(fory.dumps(data4))
+        assert result4 == data4
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_struct_with_set_field(self, ref):
+        @dataclass
+        class MyStruct:
+            items: Set[Optional[int]]
+
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        fory.register(MyStruct)
+        # Test normal set
+        data = MyStruct(items={1, 2, 3})
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+        # Test set with None
+        data2 = MyStruct(items={1, None, 3})
+        result2 = fory.loads(fory.dumps(data2))
+        assert result2 == data2
+        # Test set with only None
+        data3 = MyStruct(items={None})
+        result3 = fory.loads(fory.dumps(data3))
+        assert result3 == data3
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_struct_with_nested_collections(self, ref):
+        @dataclass
+        class MyStruct:
+            nested: List[Optional[Dict[Optional[str], 
Optional[List[Optional[int]]]]]]
+
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        fory.register(MyStruct)
+        # Test normal nested structure
+        data = MyStruct(nested=[{"a": [1, 2]}, {"b": [3, 4]}])
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+        # Test with None in outer list
+        data2 = MyStruct(nested=[{"a": [1, 2]}, None])
+        result2 = fory.loads(fory.dumps(data2))
+        assert result2 == data2
+        # Test with None key in dict
+        data3 = MyStruct(nested=[{None: [1, 2], "b": [3]}])
+        result3 = fory.loads(fory.dumps(data3))
+        assert result3 == data3
+        # Test with None value in dict (list is None)
+        data4 = MyStruct(nested=[{"a": None, "b": [1, 2]}])
+        result4 = fory.loads(fory.dumps(data4))
+        assert result4 == data4
+        # Test with None in inner list
+        data5 = MyStruct(nested=[{"a": [1, None, 3]}])
+        result5 = fory.loads(fory.dumps(data5))
+        assert result5 == data5
+        # Test deeply nested None
+        data6 = MyStruct(nested=[None, {None: None}, {"a": [None]}])
+        result6 = fory.loads(fory.dumps(data6))
+        assert result6 == data6
+
+
+class TestEdgeCases:
+    """Test edge cases for collection serialization."""
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_empty_list(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = []
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_empty_set(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = set()
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_empty_dict(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = {}
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_empty_tuple(self, ref):
+        # Tuple is Python-only
+        fory = pyfory.Fory(xlang=False, ref=ref)
+        data = ()
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_single_element_collections(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        for data in [[1], {1}, {"a": 1}]:
+            result = fory.loads(fory.dumps(data))
+            assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_large_list_with_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [i if i % 3 != 0 else None for i in range(100)]
+        result = fory.loads(fory.dumps(data))
+        assert result == data
+
+    @pytest.mark.parametrize("xlang", [False, True])
+    @pytest.mark.parametrize("ref", [False, True])
+    def test_list_with_different_types_and_none(self, xlang, ref):
+        fory = pyfory.Fory(xlang=xlang, ref=ref)
+        data = [1, "string", 3.14, None, True, [1, 2], {"a": 1}]
+        result = fory.loads(fory.dumps(data))
+        assert result == data


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to