Skip to content

Commit 2235ef7

Browse files
authored
Merge pull request #82 from CAVEconnectome/bugfix_nov
Bugfix nov
2 parents b917fd0 + cb59e0f commit 2235ef7

File tree

3 files changed

+227
-6
lines changed

3 files changed

+227
-6
lines changed

src/nglui/statebuilder/base.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,44 @@
3636
import pandas as pd
3737

3838

39+
class NumpyEncoder(json.JSONEncoder):
40+
"""Custom JSON encoder that handles numpy and pandas types.
41+
42+
Converts numpy scalars, arrays, and pandas Series to native Python types
43+
for JSON serialization.
44+
"""
45+
46+
def default(self, obj):
47+
"""Override default method to handle numpy types.
48+
49+
Parameters
50+
----------
51+
obj : any
52+
Object to serialize
53+
54+
Returns
55+
-------
56+
any
57+
JSON-serializable representation of the object
58+
"""
59+
if isinstance(obj, np.integer):
60+
return int(obj)
61+
elif isinstance(obj, np.floating):
62+
return float(obj)
63+
elif isinstance(obj, np.ndarray):
64+
return obj.tolist()
65+
elif isinstance(obj, np.bool_):
66+
return bool(obj)
67+
try:
68+
import pandas as pd
69+
70+
if isinstance(obj, pd.Series):
71+
return obj.tolist()
72+
except ImportError:
73+
pass
74+
return super().default(obj)
75+
76+
3977
class UnservedViewer(viewer_base.UnsynchronizedViewerBase):
4078
def __init__(self, **kwargs):
4179
super().__init__(**kwargs)
@@ -163,7 +201,7 @@ def add_layers_from_client(
163201
imagery_kws : Optional[dict], optional
164202
Additional keyword arguments to pass to the image layer constructor.
165203
segmentation_kws : Optional[dict], optional
166-
Additional keyword arguments to pass to the segmentation layer constructor.
204+
Additional keyword arguments to pass to the segmentation layer constructor.P
167205
168206
Returns
169207
-------
@@ -1318,7 +1356,6 @@ def to_neuroglancer_state(self):
13181356
)
13191357
if self.layout:
13201358
s.layout = self.layout
1321-
print(s.dimensions.rank)
13221359
if s.dimensions.rank == 0:
13231360
s.dimensions = self.dimensions.to_neuroglancer()
13241361
if not self.base_state:
@@ -1365,22 +1402,27 @@ def to_dict(self) -> dict:
13651402
"""
13661403
return self.viewer.state.to_json()
13671404

1368-
def to_json_string(self, indent: int = 2) -> str:
1405+
def to_json_string(self, indent: int = 2, compact: bool = False) -> str:
13691406
"""Return a JSON string representation of the viewer state.
13701407
13711408
Parameters
13721409
----------
13731410
indent : int
13741411
The number of spaces to use for indentation in the JSON string.
13751412
Default is 2.
1413+
compact: bool
1414+
If True, the JSON string will be compact with no extra whitespace or newlines.
1415+
Default is False.
13761416
13771417
Returns
13781418
-------
13791419
str
13801420
A JSON string representation of the viewer state.
13811421
"""
13821422

1383-
return json.dumps(self.to_dict(), indent=indent)
1423+
if compact:
1424+
return json.dumps(self.to_dict(), separators=(",", ":"), cls=NumpyEncoder)
1425+
return json.dumps(self.to_dict(), indent=indent, cls=NumpyEncoder)
13841426

13851427
def to_url(
13861428
self,

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def client_full():
2020
available_materialization_versions=[1],
2121
set_version=1,
2222
)
23-
print(client.materialize.version)
2423
return client
2524

2625

tests/test_base.py

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import pandas as pd
77
import pytest
88

9-
from nglui.statebuilder.base import UnservedViewer, ViewerState, webbrowser
9+
from nglui.statebuilder.base import (
10+
NumpyEncoder,
11+
UnservedViewer,
12+
ViewerState,
13+
webbrowser,
14+
)
1015
from nglui.statebuilder.ngl_components import (
1116
AnnotationLayer,
1217
CoordSpace,
@@ -953,6 +958,39 @@ def test_to_json_string(self):
953958
expected = json.dumps({"test": "data"}, indent=4)
954959
assert result == expected
955960

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+
956994
def test_to_url(self):
957995
vs = ViewerState()
958996

@@ -1085,6 +1123,148 @@ def test_get_layer_nonexistent(self):
10851123
vs.get_layer("nonexistent_layer")
10861124

10871125

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+
10881268
def test_raw_layer_basic():
10891269
# Minimal raw layer data
10901270
raw_data = {

0 commit comments

Comments
 (0)