arrayview 0.47.3__tar.gz → 0.47.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. {arrayview-0.47.3 → arrayview-0.47.4}/PKG-INFO +1 -1
  2. {arrayview-0.47.3 → arrayview-0.47.4}/pyproject.toml +1 -1
  3. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_io.py +68 -4
  4. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_launcher.py +12 -11
  5. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_loading.py +11 -11
  6. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_source_safety.py +2 -0
  7. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_vscode_extension.py +1 -1
  8. arrayview-0.47.4/src/arrayview/arrayview-opener.vsix +0 -0
  9. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_api.py +24 -0
  10. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_cli.py +28 -0
  11. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_io.py +38 -0
  12. arrayview-0.47.3/src/arrayview/arrayview-opener.vsix +0 -0
  13. {arrayview-0.47.3 → arrayview-0.47.4}/.gitignore +0 -0
  14. {arrayview-0.47.3 → arrayview-0.47.4}/LICENSE +0 -0
  15. {arrayview-0.47.3 → arrayview-0.47.4}/README.md +0 -0
  16. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/ARCHITECTURE.md +0 -0
  17. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/__init__.py +0 -0
  18. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/__main__.py +0 -0
  19. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_analysis.py +0 -0
  20. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_app.py +0 -0
  21. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_bootstrap_app.py +0 -0
  22. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_config.py +0 -0
  23. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_diagnostics.py +0 -0
  24. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_dicom.py +0 -0
  25. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_diff.py +0 -0
  26. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_extra_ports.py +0 -0
  27. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_icon.png +0 -0
  28. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_imaging.py +0 -0
  29. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_instance_registry.py +0 -0
  30. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_launch_plan.py +0 -0
  31. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_launch_trace.py +0 -0
  32. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_lifecycle.py +0 -0
  33. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_overlays.py +0 -0
  34. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_platform.py +0 -0
  35. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_render.py +0 -0
  36. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_analysis.py +0 -0
  37. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_drop.py +0 -0
  38. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_export.py +0 -0
  39. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_persistence.py +0 -0
  40. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_preferences.py +0 -0
  41. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_preload.py +0 -0
  42. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_query.py +0 -0
  43. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_rendering.py +0 -0
  44. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_segmentation.py +0 -0
  45. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_state.py +0 -0
  46. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_vectorfield.py +0 -0
  47. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_routes_websocket.py +0 -0
  48. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_segmentation.py +0 -0
  49. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_server.py +0 -0
  50. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_session.py +0 -0
  51. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_session_spec.py +0 -0
  52. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_shell.html +0 -0
  53. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_synthetic_mri.py +0 -0
  54. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_torch.py +0 -0
  55. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_tutorial.py +0 -0
  56. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_vectorfield.py +0 -0
  57. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_viewer.html +0 -0
  58. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_vscode.py +0 -0
  59. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_vscode_browser.py +0 -0
  60. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_vscode_signal.py +0 -0
  61. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/_watch.py +0 -0
  62. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/gsap.min.js +0 -0
  63. {arrayview-0.47.3 → arrayview-0.47.4}/src/arrayview/todo.md +0 -0
  64. {arrayview-0.47.3 → arrayview-0.47.4}/tests/capture_patient_loading.py +0 -0
  65. {arrayview-0.47.3 → arrayview-0.47.4}/tests/capture_v_animation.py +0 -0
  66. {arrayview-0.47.3 → arrayview-0.47.4}/tests/conftest.py +0 -0
  67. {arrayview-0.47.3 → arrayview-0.47.4}/tests/make_vectorfield_test_arrays.py +0 -0
  68. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_1d_arrays.py +0 -0
  69. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_backend_shared.py +0 -0
  70. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_browser.py +0 -0
  71. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_config.py +0 -0
  72. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_dicom.py +0 -0
  73. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_drop_import.py +0 -0
  74. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_instance_registry.py +0 -0
  75. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_large_arrays.py +0 -0
  76. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_launch_blackbox.py +0 -0
  77. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_mode_roundtrip.py +0 -0
  78. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_nifti_meta.py +0 -0
  79. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_nifti_series.py +0 -0
  80. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_rgb_pixel_art.py +0 -0
  81. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_safe_server_stop.py +0 -0
  82. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_source_safety.py +0 -0
  83. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_torch.py +0 -0
  84. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_tutorial.py +0 -0
  85. {arrayview-0.47.3 → arrayview-0.47.4}/tests/test_vsix_bundle.py +0 -0
  86. {arrayview-0.47.3 → arrayview-0.47.4}/tests/ui_audit.py +0 -0
  87. {arrayview-0.47.3 → arrayview-0.47.4}/tests/v_anim_frames/.gitkeep +0 -0
  88. {arrayview-0.47.3 → arrayview-0.47.4}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arrayview
3
- Version: 0.47.3
3
+ Version: 0.47.4
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.47.3"
7
+ version = "0.47.4"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -667,7 +667,7 @@ class _FileSeries:
667
667
  """Lazy 4D/5D view over a directory of same-shape array files.
668
668
 
669
669
  Like ``_NiftiSeries`` but works with any supported file format
670
- (.npy, .npz, .zarr, .pt/.pth, .h5/.hdf5, .tif/.tiff, .mat).
670
+ (.npy, .npz, .zarr, .pt/.pth, .h5/.hdf5, .tif/.tiff, .mat, .gipl).
671
671
  Uses ``load_data`` for each volume — no nibabel reorientation.
672
672
  """
673
673
 
@@ -940,6 +940,8 @@ def _get_ext(filepath):
940
940
  return ".nii.gz"
941
941
  if lower.endswith(".zarr.zip"):
942
942
  return ".zarr.zip"
943
+ if lower.endswith(".gipl.gz"):
944
+ return ".gipl.gz"
943
945
  return os.path.splitext(lower)[1]
944
946
 
945
947
 
@@ -950,7 +952,7 @@ def _is_supported_collection_path(filepath):
950
952
  def _strip_array_ext(filepath):
951
953
  base = os.path.basename(filepath)
952
954
  lower = base.lower()
953
- for suffix in (".nii.gz", ".zarr.zip"):
955
+ for suffix in (".nii.gz", ".zarr.zip", ".gipl.gz"):
954
956
  if lower.endswith(suffix):
955
957
  return base[: -len(suffix)]
956
958
  return os.path.splitext(base)[0]
@@ -1738,6 +1740,8 @@ def load_data(filepath, key=None, *, progress=None):
1738
1740
  import tifffile
1739
1741
 
1740
1742
  return tifffile.imread(filepath)
1743
+ elif filepath.lower().endswith((".gipl", ".gipl.gz")):
1744
+ return _load_gipl(filepath)
1741
1745
  elif filepath.endswith(".mat"):
1742
1746
  try:
1743
1747
  import scipy.io
@@ -1783,10 +1787,68 @@ def load_data(filepath, key=None, *, progress=None):
1783
1787
  return data
1784
1788
  raise ValueError(
1785
1789
  "Unsupported format. Supported: .npy, .npz, .nii/.nii.gz, .zarr, "
1786
- ".pt/.pth, .h5/.hdf5, .tif/.tiff, .mat"
1790
+ ".pt/.pth, .h5/.hdf5, .tif/.tiff, .mat, .gipl/.gipl.gz"
1787
1791
  )
1788
1792
 
1789
1793
 
1794
+ # GIPL (Guys Image Processing Lab) image types -> numpy dtypes. Data is
1795
+ # big-endian; complex types store interleaved real/imaginary components.
1796
+ _GIPL_DTYPES = {
1797
+ 1: ">u1", # binary, one value per byte
1798
+ 7: ">i1",
1799
+ 8: ">u1",
1800
+ 15: ">i2",
1801
+ 16: ">u2",
1802
+ 31: ">u4",
1803
+ 32: ">i4",
1804
+ 64: ">f4",
1805
+ 65: ">f8",
1806
+ 144: ">i2", # complex short
1807
+ 160: ">i4", # complex int
1808
+ 192: ">f4", # complex float
1809
+ 193: ">f8", # complex double
1810
+ }
1811
+ _GIPL_COMPLEX = frozenset([144, 160, 192, 193])
1812
+ _GIPL_MAGIC = (4026526128, 719555000)
1813
+ _GIPL_HEADER_SIZE = 256
1814
+
1815
+
1816
+ def _load_gipl(filepath):
1817
+ """Read a .gipl/.gipl.gz file into an array indexed (x, y, z, t).
1818
+
1819
+ Trailing singleton dimensions are dropped, matching how a NIfTI volume
1820
+ of the same data is shown.
1821
+ """
1822
+ import gzip
1823
+
1824
+ opener = gzip.open if filepath.lower().endswith(".gz") else open
1825
+ with opener(filepath, "rb") as f:
1826
+ raw = f.read()
1827
+ if len(raw) < _GIPL_HEADER_SIZE:
1828
+ raise ValueError(f"{filepath!r} is too short to be a GIPL file.")
1829
+ header = raw[:_GIPL_HEADER_SIZE]
1830
+ magic = int(np.frombuffer(header, ">u4", count=1, offset=252)[0])
1831
+ if magic not in _GIPL_MAGIC:
1832
+ raise ValueError(f"{filepath!r} is not a GIPL file (bad magic number).")
1833
+ dims = [int(d) for d in np.frombuffer(header, ">u2", count=4, offset=0)]
1834
+ image_type = int(np.frombuffer(header, ">u2", count=1, offset=8)[0])
1835
+ if image_type not in _GIPL_DTYPES:
1836
+ raise ValueError(f"{filepath!r} has unsupported GIPL image type {image_type}.")
1837
+ dims = [max(d, 1) for d in dims]
1838
+ is_complex = image_type in _GIPL_COMPLEX
1839
+ count = int(np.prod(dims)) * (2 if is_complex else 1)
1840
+ data = np.frombuffer(raw, _GIPL_DTYPES[image_type], count=count, offset=_GIPL_HEADER_SIZE)
1841
+ data = data.astype(data.dtype.newbyteorder("="))
1842
+ if is_complex:
1843
+ data = data.astype(np.float64 if data.itemsize == 8 else np.float32)
1844
+ data = data[0::2] + 1j * data[1::2]
1845
+ # x varies fastest on disk.
1846
+ arr = data.reshape(dims[::-1]).transpose(3, 2, 1, 0)
1847
+ while arr.ndim > 2 and arr.shape[-1] == 1:
1848
+ arr = arr[..., 0]
1849
+ return np.ascontiguousarray(arr)
1850
+
1851
+
1790
1852
  def _tensor_to_numpy(obj, source="object"):
1791
1853
  """Convert a tensor-like object (PyTorch, etc.) to a numpy array."""
1792
1854
  if isinstance(obj, np.ndarray):
@@ -1823,13 +1885,15 @@ _SUPPORTED_EXTS = frozenset(
1823
1885
  ".tif",
1824
1886
  ".tiff",
1825
1887
  ".mat",
1888
+ ".gipl",
1889
+ ".gipl.gz",
1826
1890
  ".dcm",
1827
1891
  ]
1828
1892
  )
1829
1893
 
1830
1894
  # Formats that load the entire array into RAM (no mmap/lazy access).
1831
1895
  # RAM guard only applies to these.
1832
- FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat"])
1896
+ FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat", ".gipl", ".gipl.gz"])
1833
1897
 
1834
1898
 
1835
1899
  def _peek_file_shape(fpath: str, ext: str):
@@ -6211,17 +6211,18 @@ def arrayview():
6211
6211
  pattern for _name, pattern in _normalize_dir_overlay_specs(args.overlay or [])
6212
6212
  )
6213
6213
  collection_inputs.extend(args.overlay_dir or [])
6214
- for pattern in collection_inputs:
6215
- pattern = lexical_abspath(pattern)
6216
- probe_prefix = scan_root_before_magic(pattern)
6217
- mount = direct_network_mount(probe_prefix)
6218
- nested_mount = network_mount_below(probe_prefix)
6219
- if mount is not None or nested_mount is not None:
6220
- _print_failure(
6221
- "network directory collections are not accessed directly. "
6222
- "Copy the collection to local storage before using --match."
6223
- )
6224
- sys.exit(1)
6214
+ if os.environ.get("ARRAYVIEW_SKIP_SOURCE_STAGING", "1") == "0":
6215
+ for pattern in collection_inputs:
6216
+ pattern = lexical_abspath(pattern)
6217
+ probe_prefix = scan_root_before_magic(pattern)
6218
+ mount = direct_network_mount(probe_prefix)
6219
+ nested_mount = network_mount_below(probe_prefix)
6220
+ if mount is not None or nested_mount is not None:
6221
+ _print_failure(
6222
+ "network directory collections are not accessed directly. "
6223
+ "Copy the collection to local storage before using --match."
6224
+ )
6225
+ sys.exit(1)
6225
6226
  if len(dir_patterns) == 1 and os.path.isdir(dir_patterns[0]):
6226
6227
  dir_patterns = [os.path.join(dir_patterns[0], "**", "*")]
6227
6228
  dir_overlay_specs = [
@@ -167,17 +167,17 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
167
167
  f"{nested_sibling_mount.mountpoint!r}; copy the series locally first."
168
168
  )
169
169
  }
170
- for pattern in dir_patterns or []:
171
- pattern_path = lexical_abspath(str(pattern))
172
- scan_root = scan_root_before_magic(pattern_path)
173
- unsafe_mount = direct_network_mount(scan_root) or network_mount_below(scan_root)
174
- if unsafe_mount is not None:
175
- return {
176
- "error": (
177
- f"Directory scan would enter network mount "
178
- f"{unsafe_mount.mountpoint!r}; copy the collection locally first."
179
- )
180
- }
170
+ for pattern in dir_patterns or []:
171
+ pattern_path = lexical_abspath(str(pattern))
172
+ scan_root = scan_root_before_magic(pattern_path)
173
+ unsafe_mount = direct_network_mount(scan_root) or network_mount_below(scan_root)
174
+ if unsafe_mount is not None:
175
+ return {
176
+ "error": (
177
+ f"Directory scan would enter network mount "
178
+ f"{unsafe_mount.mountpoint!r}; copy the collection locally first."
179
+ )
180
+ }
181
181
  if requested_sid in CANCELLED_PENDING_SESSIONS:
182
182
  CANCELLED_PENDING_SESSIONS.discard(requested_sid)
183
183
  raise HTTPException(status_code=409, detail="load request was cancelled")
@@ -53,6 +53,8 @@ _KNOWN_LOCAL_STAGE_FILESYSTEMS = frozenset(
53
53
  }
54
54
  )
55
55
  _SELF_CONTAINED_FILE_EXTENSIONS = (
56
+ ".gipl",
57
+ ".gipl.gz",
56
58
  ".h5",
57
59
  ".hdf5",
58
60
  ".mat",
@@ -58,7 +58,7 @@ _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
58
58
  _VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
59
59
  _VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
60
60
  _VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
61
- _VSCODE_EXT_VERSION = "0.15.63" # current bundled extension version
61
+ _VSCODE_EXT_VERSION = "0.15.64" # current bundled extension version
62
62
  _VSCODE_CONFIGURED_PORTS: set[int] = set()
63
63
  # Version-skew notices already printed, so a long-lived process (Jupyter, a
64
64
  # script opening several arrays) states the mismatch once instead of per call.
@@ -423,6 +423,30 @@ class TestLoad:
423
423
  assert "Direct server access" in response.json()["error"]
424
424
  assert time.monotonic() - started < 2
425
425
 
426
+ @pytest.mark.parametrize("staging", [None, "1", "0"])
427
+ @pytest.mark.parametrize("nested", [False, True])
428
+ def test_network_collection_respects_staging_setting(self, client, tmp_path, monkeypatch, staging, nested):
429
+ images = tmp_path / "images"
430
+ images.mkdir()
431
+ np.save(images / "case_a.npy", np.ones((4, 5, 6), dtype=np.float32))
432
+ mountpoint = images if nested else tmp_path
433
+ mountinfo = tmp_path / "mountinfo"
434
+ mountinfo.write_text(f"42 31 0:55 / {mountpoint} rw - cifs //server/share rw\n")
435
+ monkeypatch.setenv("_ARRAYVIEW_TEST_MOUNTINFO", str(mountinfo))
436
+ if staging is None:
437
+ monkeypatch.delenv("ARRAYVIEW_SKIP_SOURCE_STAGING", raising=False)
438
+ else:
439
+ monkeypatch.setenv("ARRAYVIEW_SKIP_SOURCE_STAGING", staging)
440
+ pattern = str(tmp_path / "*" / "case_a.npy") if nested else str(images / "*.npy")
441
+ body = client.post("/load", json={"dir_patterns": [pattern]}).json()
442
+ if staging == "0":
443
+ assert "network mount" in body["error"]
444
+ else:
445
+ assert "error" not in body
446
+ meta = client.get(f"/metadata/{body['sid']}").json()
447
+ assert meta["shape"] == [4, 5, 6, 1]
448
+ assert meta["case_names"] == ["case_a"]
449
+
426
450
  def test_load_rejects_arbitrary_staging_cleanup_authority(
427
451
  self, client, arr_2d, tmp_path, monkeypatch
428
452
  ):
@@ -477,6 +477,34 @@ def test_cli_dir_dry_run_prints_collection_summary(monkeypatch, tmp_path):
477
477
  assert "caseA, caseB" in text
478
478
 
479
479
 
480
+ @pytest.mark.parametrize("staging", [None, "1", "0"])
481
+ @pytest.mark.parametrize("nested", [False, True])
482
+ def test_cli_network_collection_respects_staging_setting(monkeypatch, tmp_path, staging, nested):
483
+ images = tmp_path / "images"
484
+ images.mkdir()
485
+ np.save(images / "case_a.npy", np.ones((4, 5, 6), dtype=np.float32))
486
+ mountpoint = images if nested else tmp_path
487
+ mountinfo = tmp_path / "mountinfo"
488
+ mountinfo.write_text(f"42 31 0:55 / {mountpoint} rw - cifs //server/share rw\n")
489
+ monkeypatch.setenv("_ARRAYVIEW_TEST_MOUNTINFO", str(mountinfo))
490
+ if staging is None:
491
+ monkeypatch.delenv("ARRAYVIEW_SKIP_SOURCE_STAGING", raising=False)
492
+ else:
493
+ monkeypatch.setenv("ARRAYVIEW_SKIP_SOURCE_STAGING", staging)
494
+ pattern = str(tmp_path / "*" / "case_a.npy") if nested else str(images / "*.npy")
495
+ monkeypatch.setattr(sys, "argv", ["arrayview", "--match", pattern, "--dry-run"])
496
+ stdout = io.StringIO()
497
+ monkeypatch.setattr(sys, "stdout", stdout)
498
+ if staging == "0":
499
+ with pytest.raises(SystemExit) as exc:
500
+ appmod.arrayview()
501
+ assert exc.value.code == 1
502
+ assert "network directory collections" in stdout.getvalue()
503
+ else:
504
+ appmod.arrayview()
505
+ assert "cases: 1" in stdout.getvalue()
506
+
507
+
480
508
  def test_cli_stack_can_continue_with_only_cases_that_have_overlays(
481
509
  monkeypatch, tmp_path
482
510
  ):
@@ -51,3 +51,41 @@ def test_mat_loader_returns_key_list_for_multiple_numeric_arrays(tmp_path):
51
51
  np.ones((4, 5), dtype=np.uint16),
52
52
  )
53
53
 
54
+
55
+
56
+ def _write_gipl(path, xyz, image_type, opener=open):
57
+ header = bytearray(256)
58
+ dims = list(xyz.shape) + [1] * (4 - xyz.ndim)
59
+ header[0:8] = np.array(dims, ">u2").tobytes()
60
+ header[8:10] = np.array([image_type], ">u2").tobytes()
61
+ header[252:256] = np.array([4026526128], ">u4").tobytes()
62
+ with opener(path, "wb") as f:
63
+ f.write(bytes(header) + xyz.T.astype(xyz.dtype.newbyteorder(">")).tobytes())
64
+
65
+
66
+ def test_gipl_loader_reads_x_y_z_order_plain_and_gzipped(tmp_path):
67
+ import gzip
68
+
69
+ from arrayview._io import _SUPPORTED_EXTS, _get_ext, load_data
70
+
71
+ expected = np.arange(4 * 5 * 3, dtype=np.int16).reshape(4, 5, 3) - 30
72
+ _write_gipl(tmp_path / "vol.gipl", expected, 15)
73
+ _write_gipl(tmp_path / "vol.gipl.gz", expected, 15, opener=gzip.open)
74
+
75
+ for name in ("vol.gipl", "vol.gipl.gz"):
76
+ path = str(tmp_path / name)
77
+ assert _get_ext(path) in _SUPPORTED_EXTS
78
+ loaded = load_data(path)
79
+ assert loaded.dtype == np.int16
80
+ np.testing.assert_array_equal(loaded, expected)
81
+
82
+
83
+ def test_gipl_loader_rejects_non_gipl_file(tmp_path):
84
+ import pytest
85
+
86
+ from arrayview._io import load_data
87
+
88
+ path = tmp_path / "bogus.gipl"
89
+ path.write_bytes(bytes(300))
90
+ with pytest.raises(ValueError, match="not a GIPL file"):
91
+ load_data(str(path))
File without changes
File without changes
File without changes
File without changes
File without changes