@@ -763,3 +763,142 @@ def test_explicit_native_list_construction() -> None:
763763 assert storage == [1 , 2 , 3 ]
764764 result = lt .storage_to_python ([1 , 2 , 3 ], converter = None )
765765 assert result == [1 , 2 , 3 ]
766+
767+
768+ # ── set[T] native element full round-trip tests (ITL-611) ─────────────────────
769+
770+
771+ def test_set_of_int_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
772+ """set[int] values round-trip as sets, not lists; extension name is 'set[int]'."""
773+ data = {1 , 2 , 3 }
774+ result , read_converter = _write_and_read (
775+ {"s" : set [int ]},
776+ [{"s" : data }],
777+ storage_backend ,
778+ tmp_path ,
779+ )
780+ field = result .schema .field ("s" )
781+ assert hasattr (field .type , "extension_name" ), (
782+ f"Expected extension type on 's', got { field .type !r} "
783+ )
784+ assert field .type .extension_name == "set[int]"
785+ rows = read_converter .arrow_table_to_python_dicts (result )
786+ assert len (rows ) == 1
787+ assert isinstance (rows [0 ]["s" ], set ), f"Expected set, got { type (rows [0 ]['s' ])} "
788+ assert rows [0 ]["s" ] == data
789+
790+
791+ def test_set_of_str_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
792+ """set[str] values round-trip as sets; extension name is 'set[str]'."""
793+ data = {"alpha" , "beta" , "gamma" }
794+ result , read_converter = _write_and_read (
795+ {"tags" : set [str ]},
796+ [{"tags" : data }],
797+ storage_backend ,
798+ tmp_path ,
799+ )
800+ field = result .schema .field ("tags" )
801+ assert hasattr (field .type , "extension_name" )
802+ assert field .type .extension_name == "set[str]"
803+ rows = read_converter .arrow_table_to_python_dicts (result )
804+ assert isinstance (rows [0 ]["tags" ], set )
805+ assert rows [0 ]["tags" ] == data
806+
807+
808+ def test_set_of_float_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
809+ """set[float] values round-trip as sets; extension name is 'set[float]'."""
810+ data = {1.0 , 2.5 , 3.14 }
811+ result , read_converter = _write_and_read (
812+ {"values" : set [float ]},
813+ [{"values" : data }],
814+ storage_backend ,
815+ tmp_path ,
816+ )
817+ field = result .schema .field ("values" )
818+ assert hasattr (field .type , "extension_name" )
819+ assert field .type .extension_name == "set[float]"
820+ rows = read_converter .arrow_table_to_python_dicts (result )
821+ assert isinstance (rows [0 ]["values" ], set )
822+ assert rows [0 ]["values" ] == data
823+
824+
825+ def test_set_of_bool_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
826+ """set[bool] values round-trip as sets; extension name is 'set[bool]'."""
827+ data = {True , False }
828+ result , read_converter = _write_and_read (
829+ {"flags" : set [bool ]},
830+ [{"flags" : data }],
831+ storage_backend ,
832+ tmp_path ,
833+ )
834+ field = result .schema .field ("flags" )
835+ assert hasattr (field .type , "extension_name" )
836+ assert field .type .extension_name == "set[bool]"
837+ rows = read_converter .arrow_table_to_python_dicts (result )
838+ assert isinstance (rows [0 ]["flags" ], set )
839+ assert rows [0 ]["flags" ] == data
840+
841+
842+ def test_set_of_bytes_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
843+ """set[bytes] values round-trip as sets; extension name is 'set[bytes]'."""
844+ data = {b"foo" , b"bar" , b"baz" }
845+ result , read_converter = _write_and_read (
846+ {"blobs" : set [bytes ]},
847+ [{"blobs" : data }],
848+ storage_backend ,
849+ tmp_path ,
850+ )
851+ field = result .schema .field ("blobs" )
852+ assert hasattr (field .type , "extension_name" )
853+ assert field .type .extension_name == "set[bytes]"
854+ rows = read_converter .arrow_table_to_python_dicts (result )
855+ assert isinstance (rows [0 ]["blobs" ], set )
856+ assert rows [0 ]["blobs" ] == data
857+
858+
859+ def test_set_of_datetime_round_trip (storage_backend : _StorageBackend , tmp_path : Path ) -> None :
860+ """set[datetime] values round-trip as sets of timezone-aware datetimes."""
861+ from datetime import datetime , timezone
862+ dt1 = datetime (2024 , 1 , 1 , 0 , 0 , 0 , tzinfo = timezone .utc )
863+ dt2 = datetime (2024 , 6 , 15 , 12 , 30 , 0 , tzinfo = timezone .utc )
864+ data = {dt1 , dt2 }
865+ result , read_converter = _write_and_read (
866+ {"timestamps" : set [datetime ]},
867+ [{"timestamps" : data }],
868+ storage_backend ,
869+ tmp_path ,
870+ )
871+ field = result .schema .field ("timestamps" )
872+ assert hasattr (field .type , "extension_name" )
873+ assert field .type .extension_name == "set[datetime]"
874+ rows = read_converter .arrow_table_to_python_dicts (result )
875+ assert isinstance (rows [0 ]["timestamps" ], set )
876+ assert rows [0 ]["timestamps" ] == data
877+
878+
879+ def test_fresh_converter_reads_set_of_int (
880+ storage_backend : _StorageBackend , tmp_path : Path
881+ ) -> None :
882+ """A fresh converter (no prior registration) reconstructs set[int] via load_logical_types."""
883+ data = {1 , 2 , 3 }
884+
885+ # Write with converter A.
886+ write_converter = _fresh_converter ()
887+ write_converter .register_python_class (set [int ])
888+ arrow_schema = write_converter .python_schema_to_arrow_schema ({"s" : set [int ]})
889+ table = write_converter .python_dicts_to_arrow_table ([{"s" : data }], arrow_schema = arrow_schema )
890+ storage_backend .write (table , tmp_path )
891+
892+ # Read with converter B — no prior registration; load_logical_types triggers factory.
893+ read_converter = _fresh_converter ()
894+ result = storage_backend .read (tmp_path , read_converter )
895+
896+ field = result .schema .field ("s" )
897+ assert hasattr (field .type , "extension_name" ), (
898+ f"Expected extension type after fresh-converter read, got { field .type !r} "
899+ )
900+ assert field .type .extension_name == "set[int]"
901+
902+ rows = read_converter .arrow_table_to_python_dicts (result )
903+ assert isinstance (rows [0 ]["s" ], set )
904+ assert rows [0 ]["s" ] == data
0 commit comments