|
6 | 6 | import pandas as pd |
7 | 7 | import pytest |
8 | 8 |
|
9 | | -from nglui.statebuilder.base import UnservedViewer, ViewerState, webbrowser |
| 9 | +from nglui.statebuilder.base import ( |
| 10 | + NumpyEncoder, |
| 11 | + UnservedViewer, |
| 12 | + ViewerState, |
| 13 | + webbrowser, |
| 14 | +) |
10 | 15 | from nglui.statebuilder.ngl_components import ( |
11 | 16 | AnnotationLayer, |
12 | 17 | CoordSpace, |
@@ -953,6 +958,39 @@ def test_to_json_string(self): |
953 | 958 | expected = json.dumps({"test": "data"}, indent=4) |
954 | 959 | assert result == expected |
955 | 960 |
|
| 961 | + def test_to_json_string_compact(self): |
| 962 | + """Test that compact parameter removes newlines and spaces""" |
| 963 | + vs = ViewerState() |
| 964 | + |
| 965 | + test_data = {"key": "value", "array": [1, 2, 3], "nested": {"inner": "data"}} |
| 966 | + |
| 967 | + with patch.object(vs, "to_dict") as mock_to_dict: |
| 968 | + mock_to_dict.return_value = test_data |
| 969 | + |
| 970 | + # Test default (pretty-printed with indent) |
| 971 | + result_default = vs.to_json_string() |
| 972 | + assert "\n" in result_default # Should have newlines |
| 973 | + assert " " in result_default # Should have indentation |
| 974 | + |
| 975 | + # Test compact=False (explicit pretty-print) |
| 976 | + result_pretty = vs.to_json_string(compact=False, indent=2) |
| 977 | + assert "\n" in result_pretty # Should have newlines |
| 978 | + assert " " in result_pretty # Should have indentation |
| 979 | + |
| 980 | + # Test compact=True (minimal JSON) |
| 981 | + result_compact = vs.to_json_string(compact=True) |
| 982 | + assert "\n" not in result_compact # No newlines |
| 983 | + assert ": " not in result_compact # No space after colon |
| 984 | + assert ", " not in result_compact # No space after comma |
| 985 | + |
| 986 | + # Verify it's still valid JSON |
| 987 | + parsed = json.loads(result_compact) |
| 988 | + assert parsed == test_data |
| 989 | + |
| 990 | + # Verify compact output matches expected format |
| 991 | + expected_compact = json.dumps(test_data, separators=(",", ":")) |
| 992 | + assert result_compact == expected_compact |
| 993 | + |
956 | 994 | def test_to_url(self): |
957 | 995 | vs = ViewerState() |
958 | 996 |
|
@@ -1085,6 +1123,148 @@ def test_get_layer_nonexistent(self): |
1085 | 1123 | vs.get_layer("nonexistent_layer") |
1086 | 1124 |
|
1087 | 1125 |
|
| 1126 | +class TestNumpyEncoder: |
| 1127 | + """Test custom JSON encoder for numpy types""" |
| 1128 | + |
| 1129 | + def test_numpy_int_encoding(self): |
| 1130 | + """Test that numpy integers are converted to Python ints""" |
| 1131 | + encoder = NumpyEncoder() |
| 1132 | + |
| 1133 | + # Test various numpy integer types |
| 1134 | + assert encoder.default(np.int64(42)) == 42 |
| 1135 | + assert encoder.default(np.int32(100)) == 100 |
| 1136 | + assert encoder.default(np.int16(50)) == 50 |
| 1137 | + assert isinstance(encoder.default(np.int64(42)), int) |
| 1138 | + |
| 1139 | + def test_numpy_float_encoding(self): |
| 1140 | + """Test that numpy floats are converted to Python floats""" |
| 1141 | + encoder = NumpyEncoder() |
| 1142 | + |
| 1143 | + # Test various numpy float types |
| 1144 | + assert encoder.default(np.float64(3.14)) == 3.14 |
| 1145 | + assert encoder.default(np.float32(2.71)) == pytest.approx(2.71, rel=1e-6) |
| 1146 | + assert isinstance(encoder.default(np.float64(3.14)), float) |
| 1147 | + |
| 1148 | + def test_numpy_bool_encoding(self): |
| 1149 | + """Test that numpy booleans are converted to Python bools""" |
| 1150 | + encoder = NumpyEncoder() |
| 1151 | + |
| 1152 | + assert encoder.default(np.bool_(True)) is True |
| 1153 | + assert encoder.default(np.bool_(False)) is False |
| 1154 | + assert isinstance(encoder.default(np.bool_(True)), bool) |
| 1155 | + |
| 1156 | + def test_numpy_array_encoding(self): |
| 1157 | + """Test that numpy arrays are converted to Python lists""" |
| 1158 | + encoder = NumpyEncoder() |
| 1159 | + |
| 1160 | + # 1D array |
| 1161 | + arr_1d = np.array([1, 2, 3, 4, 5]) |
| 1162 | + result_1d = encoder.default(arr_1d) |
| 1163 | + assert result_1d == [1, 2, 3, 4, 5] |
| 1164 | + assert isinstance(result_1d, list) |
| 1165 | + |
| 1166 | + # 2D array |
| 1167 | + arr_2d = np.array([[1, 2], [3, 4]]) |
| 1168 | + result_2d = encoder.default(arr_2d) |
| 1169 | + assert result_2d == [[1, 2], [3, 4]] |
| 1170 | + assert isinstance(result_2d, list) |
| 1171 | + |
| 1172 | + # Array with floats |
| 1173 | + arr_float = np.array([1.1, 2.2, 3.3]) |
| 1174 | + result_float = encoder.default(arr_float) |
| 1175 | + assert result_float == pytest.approx([1.1, 2.2, 3.3]) |
| 1176 | + |
| 1177 | + def test_pandas_series_encoding(self): |
| 1178 | + """Test that pandas Series are converted to Python lists""" |
| 1179 | + encoder = NumpyEncoder() |
| 1180 | + |
| 1181 | + series = pd.Series([10, 20, 30, 40]) |
| 1182 | + result = encoder.default(series) |
| 1183 | + assert result == [10, 20, 30, 40] |
| 1184 | + assert isinstance(result, list) |
| 1185 | + |
| 1186 | + # Series with different dtypes |
| 1187 | + series_float = pd.Series([1.5, 2.5, 3.5]) |
| 1188 | + result_float = encoder.default(series_float) |
| 1189 | + assert result_float == pytest.approx([1.5, 2.5, 3.5]) |
| 1190 | + |
| 1191 | + def test_json_dumps_with_numpy_types(self): |
| 1192 | + """Test json.dumps with NumpyEncoder for complex nested structures""" |
| 1193 | + data = { |
| 1194 | + "int_value": np.int64(42), |
| 1195 | + "float_value": np.float64(3.14159), |
| 1196 | + "bool_value": np.bool_(True), |
| 1197 | + "array": np.array([1, 2, 3]), |
| 1198 | + "nested": { |
| 1199 | + "another_int": np.int32(100), |
| 1200 | + "another_array": np.array([[1, 2], [3, 4]]), |
| 1201 | + }, |
| 1202 | + "series": pd.Series([10, 20, 30]), |
| 1203 | + } |
| 1204 | + |
| 1205 | + # Should not raise an error |
| 1206 | + result = json.dumps(data, cls=NumpyEncoder) |
| 1207 | + assert isinstance(result, str) |
| 1208 | + |
| 1209 | + # Deserialize and verify |
| 1210 | + parsed = json.loads(result) |
| 1211 | + assert parsed["int_value"] == 42 |
| 1212 | + assert parsed["float_value"] == pytest.approx(3.14159) |
| 1213 | + assert parsed["bool_value"] is True |
| 1214 | + assert parsed["array"] == [1, 2, 3] |
| 1215 | + assert parsed["nested"]["another_int"] == 100 |
| 1216 | + assert parsed["nested"]["another_array"] == [[1, 2], [3, 4]] |
| 1217 | + assert parsed["series"] == [10, 20, 30] |
| 1218 | + |
| 1219 | + def test_viewerstate_to_json_string_with_numpy(self): |
| 1220 | + """Test that ViewerState.to_json_string handles numpy types correctly""" |
| 1221 | + vs = ViewerState(dimensions=[4, 4, 40]) |
| 1222 | + |
| 1223 | + # Mock to_dict to return data with numpy types |
| 1224 | + with patch.object(vs, "to_dict") as mock_to_dict: |
| 1225 | + mock_to_dict.return_value = { |
| 1226 | + "position": np.array([100, 200, 300]), |
| 1227 | + "scale": np.float64(1.5), |
| 1228 | + "id": np.int64(12345), |
| 1229 | + "visible": np.bool_(True), |
| 1230 | + } |
| 1231 | + |
| 1232 | + # Should not raise an error |
| 1233 | + result = vs.to_json_string(indent=2) |
| 1234 | + assert isinstance(result, str) |
| 1235 | + |
| 1236 | + # Verify it's valid JSON |
| 1237 | + parsed = json.loads(result) |
| 1238 | + assert parsed["position"] == [100, 200, 300] |
| 1239 | + assert parsed["scale"] == pytest.approx(1.5) |
| 1240 | + assert parsed["id"] == 12345 |
| 1241 | + assert parsed["visible"] is True |
| 1242 | + |
| 1243 | + def test_edge_cases(self): |
| 1244 | + """Test edge cases for numpy type encoding""" |
| 1245 | + encoder = NumpyEncoder() |
| 1246 | + |
| 1247 | + # Zero values |
| 1248 | + assert encoder.default(np.int64(0)) == 0 |
| 1249 | + assert encoder.default(np.float64(0.0)) == 0.0 |
| 1250 | + |
| 1251 | + # Negative values |
| 1252 | + assert encoder.default(np.int64(-42)) == -42 |
| 1253 | + assert encoder.default(np.float64(-3.14)) == pytest.approx(-3.14) |
| 1254 | + |
| 1255 | + # Large values |
| 1256 | + large_int = np.int64(9223372036854775807) # Max int64 |
| 1257 | + assert encoder.default(large_int) == 9223372036854775807 |
| 1258 | + |
| 1259 | + # Empty array |
| 1260 | + empty_array = np.array([]) |
| 1261 | + assert encoder.default(empty_array) == [] |
| 1262 | + |
| 1263 | + # Empty series |
| 1264 | + empty_series = pd.Series([]) |
| 1265 | + assert encoder.default(empty_series) == [] |
| 1266 | + |
| 1267 | + |
1088 | 1268 | def test_raw_layer_basic(): |
1089 | 1269 | # Minimal raw layer data |
1090 | 1270 | raw_data = { |
|
0 commit comments