@@ -114,7 +114,7 @@ def test_nullable_bool_is_a_real_bool():
114114 """The case that motivated the whole design: no uint8, no reserved 255."""
115115 t = simple ([True , None , False ], spec = blosc2 .bool (null_storage = "mask" ))
116116 assert t ["a" ].dtype == np .dtype (np .bool_ )
117- assert t ["a" ][:].tolist () == [True , False , False ]
117+ assert t ["a" ][:].tolist () == [True , None , False ]
118118 assert t ["a" ].is_null ().tolist () == [False , True , False ]
119119
120120
@@ -145,7 +145,7 @@ def test_timestamp_accepts_none():
145145 spec = blosc2 .timestamp (null_storage = "mask" )
146146 t = simple (["2020-01-01" , None , "2020-01-03" ], spec = spec )
147147 assert t ["a" ].is_null ().tolist () == [False , True , False ]
148- assert np .isnat (t ["a" ][:][1 ])
148+ assert np .ma . is_masked (t ["a" ][:][1 ])
149149
150150
151151def test_ndarray_column_accepts_none ():
@@ -651,7 +651,7 @@ def test_extend_from_a_table_carries_the_nulls_over():
651651 src = simple ([1 , None , 3 ])
652652 dst = simple ([])
653653 dst .extend (src )
654- assert dst ["a" ][:].tolist () == [1 , 0 , 3 ]
654+ assert dst ["a" ][:].tolist () == [1 , None , 3 ]
655655 assert dst ["a" ].is_null ().tolist () == [False , True , False ]
656656 assert dst ["a" ].null_count () == 1
657657
@@ -677,7 +677,7 @@ def test_extend_from_a_table_carries_utf8_nulls():
677677 src = table ([("x" ,), (None ,), ("" ,)], s = utf8_spec (null_storage = "mask" ))
678678 dst = table ([], s = utf8_spec (null_storage = "mask" ))
679679 dst .extend (src )
680- assert list ( dst ["s" ][:]) == ["x" , "" , "" ]
680+ assert dst ["s" ][:]. tolist ( ) == ["x" , None , "" ]
681681 assert dst ["s" ].is_null ().tolist () == [False , True , False ]
682682
683683
@@ -705,7 +705,7 @@ def test_extend_from_a_sorted_view_copies_it_in_sorted_order():
705705 dst = simple ([])
706706 dst .extend (src .sort_by ("a" , view = True ))
707707 # Nulls sort last, in both directions, and the copy has to agree.
708- assert dst ["a" ][:].tolist () == [1 , 2 , 3 , 0 ]
708+ assert dst ["a" ][:].tolist () == [1 , 2 , 3 , None ]
709709 assert dst ["a" ].is_null ().tolist () == [False , False , False , True ]
710710
711711
@@ -730,7 +730,7 @@ def test_extend_from_a_null_free_table_writes_no_sidecar():
730730def test_add_column_values_accept_none ():
731731 t = simple ([1 , 2 , 3 ], spec = blosc2 .int64 ())
732732 t .add_column ("b" , blosc2 .int64 (nullable = True ), values = [10 , None , 30 ])
733- assert t ["b" ][:].tolist () == [10 , 0 , 30 ]
733+ assert t ["b" ][:].tolist () == [10 , None , 30 ]
734734 assert t ["b" ].is_null ().tolist () == [False , True , False ]
735735 assert t ["b" ].null_count () == 1
736736
@@ -747,7 +747,7 @@ def test_add_column_default_none_still_applies_to_later_rows():
747747 t .add_column ("b" , blosc2 .field (blosc2 .int64 (nullable = True ), default = None ))
748748 t .append ((3 , 9 ))
749749 t .extend ([(4 , None )])
750- assert t ["b" ][:].tolist () == [0 , 0 , 9 , 0 ]
750+ assert t ["b" ][:].tolist () == [None , None , 9 , None ]
751751 assert t ["b" ].is_null ().tolist () == [True , True , False , True ]
752752
753753
@@ -767,15 +767,15 @@ def test_add_column_scatters_nulls_past_deleted_rows():
767767 assert t ["b" ].is_null ().tolist () == [False , True , False ]
768768 t .compact ()
769769 assert t ["b" ].is_null ().tolist () == [False , True , False ]
770- assert t ["b" ][:].tolist () == [10 , 0 , 30 ]
770+ assert t ["b" ][:].tolist () == [10 , None , 30 ]
771771
772772
773773@needs_utf8
774774def test_add_column_utf8_values_accept_none ():
775775 """The fill is "" here, which a genuine row may also hold."""
776776 t = simple ([1 , 2 , 3 ], spec = blosc2 .int64 ())
777777 t .add_column ("s" , utf8_spec (null_storage = "mask" ), values = ["p" , None , "" ])
778- assert list ( t ["s" ][:]) == ["p" , "" , "" ]
778+ assert t ["s" ][:]. tolist ( ) == ["p" , None , "" ]
779779 assert t ["s" ].is_null ().tolist () == [False , True , False ]
780780
781781
@@ -786,7 +786,7 @@ def test_add_column_ndarray_values_accept_none():
786786 blosc2 .ndarray ((3 ,), dtype = blosc2 .int64 (), nullable = True ),
787787 values = [np .array ([1 , 2 , 3 ]), None ],
788788 )
789- assert t ["v" ][:].tolist () == [[1 , 2 , 3 ], [0 , 0 , 0 ]]
789+ assert t ["v" ][:].tolist () == [[1 , 2 , 3 ], [None , None , None ]]
790790 assert t ["v" ].is_null ().tolist () == [False , True ]
791791
792792
@@ -846,7 +846,7 @@ def test_add_column_timestamp_null_reads_as_nat():
846846 t = simple ([1 , 2 ], spec = blosc2 .int64 ())
847847 t .add_column ("ts" , blosc2 .timestamp (nullable = True ), values = [when , None ])
848848 assert t ["ts" ].is_null ().tolist () == [False , True ]
849- assert np .isnat (t ["ts" ][:][1 ])
849+ assert np .ma . is_masked (t ["ts" ][:][1 ])
850850
851851
852852# ---------------------------------------------------------------------------
@@ -863,7 +863,7 @@ def test_add_column_timestamp_null_reads_as_nat():
863863def test_isin_does_not_match_the_fill ():
864864 t = simple ([1 , None , 0 ])
865865 # Rows 1 and 2 both hold a physical 0; only row 2 holds it as a value.
866- assert t ["a" ][:].tolist () == [1 , 0 , 0 ]
866+ assert t ["a" ][:].tolist () == [1 , None , 0 ]
867867 assert t ["a" ].isin ([0 ]).tolist () == [False , False , True ]
868868
869869
@@ -887,7 +887,7 @@ def test_isin_pandas_na_spells_the_same_request():
887887def test_isin_does_not_match_the_empty_string_fill ():
888888 """The utf8 fill is "", which a genuine row may hold -- so it has to be the sidecar."""
889889 t = table ([("x" ,), (None ,), ("" ,)], s = utf8_spec (null_storage = "mask" ))
890- assert list ( t ["s" ][:]) == ["x" , "" , "" ]
890+ assert t ["s" ][:]. tolist ( ) == ["x" , None , "" ]
891891 assert t ["s" ].isin (["" ]).tolist () == [False , False , True ]
892892
893893
@@ -1160,4 +1160,100 @@ def test_a_sentinel_column_still_shows_its_sentinel_in_a_row():
11601160 t = blosc2 .CTable (Row , expected_size = 8 )
11611161 t .extend ([(1 ,), (- 1 ,), (3 ,)])
11621162 assert t [1 ].n == - 1
1163+ assert t ["n" ][1 ] == - 1
11631164 assert t ["n" ].is_null ().tolist () == [False , True , False ]
1165+
1166+
1167+ @pytest .mark .parametrize (
1168+ ("spec" , "value" ),
1169+ [
1170+ (blosc2 .int64 (null_storage = "mask" ), 0 ),
1171+ (blosc2 .uint8 (null_storage = "mask" ), 0 ),
1172+ (blosc2 .float64 (null_storage = "mask" ), float ("nan" )),
1173+ (blosc2 .bool (null_storage = "mask" ), False ),
1174+ (blosc2 .string (max_length = 4 , null_storage = "mask" ), "" ),
1175+ (blosc2 .timestamp (null_storage = "mask" ), datetime .datetime (2020 , 1 , 1 )),
1176+ ],
1177+ )
1178+ def test_scalar_column_reads_apply_null_mask (spec , value ):
1179+ with simple ([value , None ], spec ) as t :
1180+ col = t ["a" ]
1181+ assert col [1 ] is None
1182+ assert col [- 1 ] is None
1183+ assert col [np .int64 (1 )] is None
1184+ assert col [np .int32 (- 1 )] is None
1185+ assert col [0 ] is not None
1186+ assert col .is_null ().tolist () == [False , True ]
1187+ if isinstance (value , float ):
1188+ assert np .isnan (col [0 ]) # A valid NaN remains a value.
1189+ with pytest .raises (IndexError ):
1190+ col [np .int64 (2 )]
1191+
1192+
1193+ def test_scalar_column_nulls_follow_view_positions_and_deletions ():
1194+ with mixed_null_table () as t :
1195+ for name in ("n" , "v" , "t" , "ts" ):
1196+ assert t [name ][1 ] is None
1197+ assert t [1 :][name ][0 ] is None
1198+ assert t [name ].view [1 :][0 ] is None
1199+ sorted_view = t .sort_by ("n" , view = True )
1200+ assert sorted_view ["n" ][- 1 ] is None
1201+ assert sorted_view ["n" ][0 ] == 1
1202+ t .delete (0 )
1203+ assert t ["n" ][0 ] is None
1204+ assert t ["n" ][1 ] == 3
1205+
1206+
1207+ def test_scalar_masked_ndarray_column_returns_none ():
1208+ spec = blosc2 .ndarray ((2 ,), dtype = blosc2 .float32 (), null_storage = "mask" )
1209+ with simple ([[1.0 , 2.0 ], None ], spec ) as t :
1210+ assert t ["a" ][1 ] is None
1211+ assert t ["a" ][1 , 0 ] is None
1212+ assert t ["a" ][np .int64 (1 ), 0 ] is None
1213+ assert t ["a" ][0 , 1 ] == 2.0
1214+
1215+
1216+ @pytest .mark .parametrize (
1217+ "key" , [slice (None ), slice (None , None , - 1 ), slice (0 , 3 , 2 ), [2 , 0 , 2 ], np .array ([True , False , True ])]
1218+ )
1219+ def test_column_selections_mask_nulls_in_selection_order (key ):
1220+ with simple ([None , 0 , 33 ], blosc2 .uint8 (null_storage = "mask" )) as t :
1221+ values = t ["a" ][key ]
1222+ expected = np .array ([True , False , False ])[key ]
1223+ if expected .any ():
1224+ assert isinstance (values , np .ma .MaskedArray )
1225+ assert values .dtype == np .dtype ("uint8" )
1226+ np .testing .assert_array_equal (values .mask , expected )
1227+ np .testing .assert_array_equal (
1228+ values .compressed (), np .array ([0 , 0 , 33 ], dtype = np .uint8 )[key ][~ expected ]
1229+ )
1230+ assert values .tolist () == np .array ([None , 0 , 33 ], dtype = object )[key ].tolist ()
1231+ else :
1232+ assert not isinstance (values , np .ma .MaskedArray )
1233+ assert not isinstance (t ["a" ][1 :], np .ma .MaskedArray )
1234+ assert not isinstance (t ["a" ][0 :0 ], np .ma .MaskedArray )
1235+
1236+
1237+ def test_masked_column_selections_follow_views_and_deletions ():
1238+ with mixed_null_table () as t :
1239+ assert t [1 :]["n" ][:].tolist () == [None , 3 ]
1240+ assert t ["n" ].view [1 :][:].tolist () == [None , 3 ]
1241+ assert t .sort_by ("n" , view = True )["n" ][:].tolist () == [1 , 3 , None ]
1242+ t .delete (0 )
1243+ assert t ["n" ][:].tolist () == [None , 3 ]
1244+ assert t ["n" ][[1 , 0 ]].tolist () == [3 , None ]
1245+
1246+
1247+ def test_masked_ndarray_selections_broadcast_validity ():
1248+ spec = blosc2 .ndarray ((2 ,), dtype = blosc2 .float32 (), null_storage = "mask" )
1249+ with simple ([[1.0 , 2.0 ], None , [3.0 , 4.0 ]], spec ) as t :
1250+ values = t ["a" ][:]
1251+ assert values .shape == (3 , 2 )
1252+ assert values .mask .tolist () == [[False , False ], [True , True ], [False , False ]]
1253+ assert values .dtype == np .dtype ("float32" )
1254+ assert t ["a" ][:, 1 ].tolist () == [2.0 , None , 4.0 ]
1255+ assert t ["a" ][[1 , 0 ], 0 ].tolist () == [None , 1.0 ]
1256+ assert t ["a" ].to_numpy (masked = True ).mask .tolist () == values .mask .tolist ()
1257+ values [1 ] = [5.0 , 6.0 ] # The returned mask is writable and independent of storage.
1258+ assert not values .mask [1 ].any ()
1259+ assert t ["a" ][1 ] is None
0 commit comments