arrayview 0.38.1__tar.gz → 0.40.0__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.
- {arrayview-0.38.1 → arrayview-0.40.0}/PKG-INFO +5 -2
- {arrayview-0.38.1 → arrayview-0.40.0}/README.md +4 -1
- {arrayview-0.38.1 → arrayview-0.40.0}/pyproject.toml +1 -1
- arrayview-0.40.0/src/arrayview/_diagnostics.py +211 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_io.py +92 -19
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_launcher.py +187 -43
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_lifecycle.py +16 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_platform.py +19 -3
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_render.py +76 -11
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_preferences.py +4 -1
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_query.py +15 -7
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_rendering.py +5 -1
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_websocket.py +89 -5
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_server.py +76 -6
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_session.py +12 -0
- arrayview-0.40.0/src/arrayview/_tutorial.py +106 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_viewer.html +2060 -218
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_vscode_extension.py +15 -1
- arrayview-0.40.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/conftest.py +23 -0
- arrayview-0.40.0/tests/test_1d_arrays.py +55 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_api.py +90 -3
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_browser.py +883 -23
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_cli.py +6 -2
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_dicom.py +74 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_launch_blackbox.py +3 -1
- arrayview-0.40.0/tests/test_launch_diagnostics.py +122 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_lifecycle_contract.py +110 -6
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_nifti_series.py +20 -3
- arrayview-0.40.0/tests/test_render_failure_reporting.py +74 -0
- arrayview-0.40.0/tests/test_render_progress.py +275 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_rgb_pixel_art.py +23 -7
- arrayview-0.40.0/tests/test_tutorial.py +60 -0
- arrayview-0.40.0/tests/test_tutorial_browser.py +197 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_view_component_integration.py +29 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_view_launch_execution.py +5 -0
- arrayview-0.40.0/tests/test_window_level_preview.py +101 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/ui_audit.py +3 -3
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/visual_smoke.py +53 -47
- arrayview-0.38.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.38.1/tests/test_launch_diagnostics.py +0 -47
- {arrayview-0.38.1 → arrayview-0.40.0}/.gitignore +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/LICENSE +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/src/arrayview/todo.md +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_config.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_drop_import.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_instance_registry.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_interactions.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_io.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_launch_plan.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_launch_trace.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_session_spec.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_torch.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_value_formatting.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.38.1 → arrayview-0.40.0}/tests/v_anim_frames/.gitkeep +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arrayview
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.40.0
|
|
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
|
|
@@ -43,7 +43,10 @@ Arrayview lets you scroll through multi-dimensional arrays.
|
|
|
43
43
|
|
|
44
44
|
Open it from the shell, from Python, Julia, or Matlab, or inside a Jupyter notebook. Use it locally or over SSH.
|
|
45
45
|
|
|
46
|
-
If you work in VS Code, you can open arrays directly from the explorer
|
|
46
|
+
If you work in VS Code, you can open arrays directly from the explorer — click an
|
|
47
|
+
array file, or right-click a folder and choose **Open Folder in ArrayView** to
|
|
48
|
+
review a DICOM series or a whole directory of arrays as one stack. With Remote
|
|
49
|
+
SSH or a tunnel, it works the same way.
|
|
47
50
|
|
|
48
51
|
It is meant to feel simple but there's more to it than meets the eye.
|
|
49
52
|
|
|
@@ -6,7 +6,10 @@ Arrayview lets you scroll through multi-dimensional arrays.
|
|
|
6
6
|
|
|
7
7
|
Open it from the shell, from Python, Julia, or Matlab, or inside a Jupyter notebook. Use it locally or over SSH.
|
|
8
8
|
|
|
9
|
-
If you work in VS Code, you can open arrays directly from the explorer
|
|
9
|
+
If you work in VS Code, you can open arrays directly from the explorer — click an
|
|
10
|
+
array file, or right-click a folder and choose **Open Folder in ArrayView** to
|
|
11
|
+
review a DICOM series or a whole directory of arrays as one stack. With Remote
|
|
12
|
+
SSH or a tunnel, it works the same way.
|
|
10
13
|
|
|
11
14
|
It is meant to feel simple but there's more to it than meets the eye.
|
|
12
15
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Turn a launch failure into a concise, actionable terminal message.
|
|
2
|
+
|
|
3
|
+
One line saying what went wrong, one line saying what to do about it. The
|
|
4
|
+
severity colour is the signal that matters:
|
|
5
|
+
|
|
6
|
+
orange something you can clear yourself right now — reload the window,
|
|
7
|
+
run it again, kill a stale server
|
|
8
|
+
red something that needs a real change — the file, the arrays, or a
|
|
9
|
+
bug worth a traceback
|
|
10
|
+
|
|
11
|
+
`SETUP` is the exception to all of that: the run before it *worked*, and the
|
|
12
|
+
only thing left is a step the user has to take by hand. Nothing went wrong, so
|
|
13
|
+
it is not written as though something did — no "failed to open" headline, no
|
|
14
|
+
traceback hint, and none of the raw internal text.
|
|
15
|
+
|
|
16
|
+
The patterns below are matched against the message text the opener and the
|
|
17
|
+
backend actually produce; they are taken from observed failures rather than
|
|
18
|
+
invented, so a new failure string falls through to the red catch-all with a
|
|
19
|
+
`--trace` hint rather than being mislabelled as harmless.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
ACTION = "action" # orange — user can clear it
|
|
29
|
+
FATAL = "fatal" # red — needs a real fix
|
|
30
|
+
SETUP = "setup" # orange — nothing failed; one manual step is outstanding
|
|
31
|
+
|
|
32
|
+
_RESET = "\033[0m"
|
|
33
|
+
_RED = "\033[1;31m"
|
|
34
|
+
_ORANGE = "\033[1;38;5;208m"
|
|
35
|
+
_DIM = "\033[2m"
|
|
36
|
+
|
|
37
|
+
_RELOAD = 'Reload the VS Code window (Ctrl/Cmd+Shift+P → "Developer: Reload Window").'
|
|
38
|
+
_RETRY = "Run the same command again."
|
|
39
|
+
|
|
40
|
+
# (pattern, severity, what happened, what to do)
|
|
41
|
+
_DIAGNOSES: list[tuple[str, str, str, str]] = [
|
|
42
|
+
# ── First run after the opener is installed — not a failure ───────────
|
|
43
|
+
# Must stay ahead of the stale-opener pattern below: the local wording
|
|
44
|
+
# ("reload this VS Code window once, then retry") matches both, and a
|
|
45
|
+
# just-installed opener is not a stale one.
|
|
46
|
+
(r"opener was installed|installed its vs ?code opener|"
|
|
47
|
+
r"updated its vs ?code opener", SETUP,
|
|
48
|
+
"The VS Code opener is installed.",
|
|
49
|
+
'Reload this VS Code window once (Ctrl/Cmd+Shift+P → "Developer: Reload '
|
|
50
|
+
'Window"), then run the same command again.'),
|
|
51
|
+
|
|
52
|
+
# ── Opener/extension state — all clearable ────────────────────────────
|
|
53
|
+
(r"stale arrayview opener|reload this vs ?code window", ACTION,
|
|
54
|
+
"This VS Code window is running an older ArrayView opener than the one "
|
|
55
|
+
"installed.",
|
|
56
|
+
_RELOAD),
|
|
57
|
+
(r"another arrayview backend now owns this port", ACTION,
|
|
58
|
+
"A newer launch took over the port, so the session this tab wanted is "
|
|
59
|
+
"gone.",
|
|
60
|
+
_RETRY),
|
|
61
|
+
(r"required extension|extension version|opener v\d", ACTION,
|
|
62
|
+
"The installed opener and the Python package disagree on version.",
|
|
63
|
+
_RELOAD + " If it persists: uv tool install --reinstall arrayview"),
|
|
64
|
+
|
|
65
|
+
# ── Transport — the route, not the data ───────────────────────────────
|
|
66
|
+
(r"backend stopped answering", ACTION,
|
|
67
|
+
"The route to the backend stopped answering before the viewer was ready "
|
|
68
|
+
"(usually a cold tunnel, not a dead server).",
|
|
69
|
+
_RETRY + " If it repeats, reload the VS Code window."),
|
|
70
|
+
(r"could not connect to the arrayview backend|ws-open", ACTION,
|
|
71
|
+
"The viewer loaded but its WebSocket never connected.",
|
|
72
|
+
_RELOAD + " If it persists, the port is blocked or the tunnel is down."),
|
|
73
|
+
(r"viewer route stopped answering|route .*not answering", ACTION,
|
|
74
|
+
"The forwarded route stopped answering; the backend itself is likely "
|
|
75
|
+
"fine.",
|
|
76
|
+
_RETRY),
|
|
77
|
+
|
|
78
|
+
# ── Timing — slow, not broken ─────────────────────────────────────────
|
|
79
|
+
(r"did not render a frame|did not become ready|did not start the viewer",
|
|
80
|
+
ACTION,
|
|
81
|
+
"The viewer opened but showed no frame before the deadline — usually a "
|
|
82
|
+
"slow first load.",
|
|
83
|
+
_RETRY + " If the same file always does this, it may be too large to "
|
|
84
|
+
"render in one go."),
|
|
85
|
+
(r"panel closed before|closed before its first frame", ACTION,
|
|
86
|
+
"The viewer tab was closed (or replaced by a preview tab) before it "
|
|
87
|
+
"rendered.",
|
|
88
|
+
_RETRY + " Leave the tab open while it loads."),
|
|
89
|
+
(r"signal expired|expired before a panel|hard timeout after", ACTION,
|
|
90
|
+
"The opener was still busy with an earlier request and this one aged "
|
|
91
|
+
"out.",
|
|
92
|
+
_RETRY),
|
|
93
|
+
|
|
94
|
+
# ── Ports and stale processes ─────────────────────────────────────────
|
|
95
|
+
(r"could not find a free port|no free arrayview port|address already in use",
|
|
96
|
+
ACTION,
|
|
97
|
+
"No free port was available — earlier ArrayView servers are probably "
|
|
98
|
+
"still running.",
|
|
99
|
+
"See them with `arrayview instances`, clear them with `arrayview stop`, "
|
|
100
|
+
"then retry."),
|
|
101
|
+
(r"could not register the session|registration", ACTION,
|
|
102
|
+
"The server started but the session could not be registered.",
|
|
103
|
+
_RETRY + " If it repeats: arrayview stop, then try again."),
|
|
104
|
+
|
|
105
|
+
# ── The data itself — a real change is needed ─────────────────────────
|
|
106
|
+
(r"pickled \(object\) data|allow_pickle", FATAL,
|
|
107
|
+
"This .npy holds pickled Python objects, not a plain array.",
|
|
108
|
+
"Load it yourself and pass the array, or re-save with "
|
|
109
|
+
"np.save(..., arr) on a numeric array."),
|
|
110
|
+
(r"could not open this file|no data left in file|failed to load|"
|
|
111
|
+
r"cannot load|unsupported file", FATAL,
|
|
112
|
+
"The file could not be read as an array.",
|
|
113
|
+
"Check the path and that the file is complete and not truncated."),
|
|
114
|
+
(r"dtype mismatch", FATAL,
|
|
115
|
+
"The arrays you passed have different dtypes.",
|
|
116
|
+
"Compare needs matching dtypes — convert one before loading."),
|
|
117
|
+
(r"shape mismatch|expected \(|spatial shape|shapes? .*differ", FATAL,
|
|
118
|
+
"The arrays you passed have incompatible shapes.",
|
|
119
|
+
"Compare needs matching spatial shapes — check both files."),
|
|
120
|
+
(r"cannot start server", FATAL,
|
|
121
|
+
"The backend server could not start at all.",
|
|
122
|
+
"Re-run with --trace for the underlying error."),
|
|
123
|
+
|
|
124
|
+
# ── Bad arguments, caught before anything starts ──────────────────────
|
|
125
|
+
(r"file not found", FATAL,
|
|
126
|
+
"That file does not exist.",
|
|
127
|
+
"Check the path — and that the share it lives on is mounted."),
|
|
128
|
+
(r"not a directory", FATAL,
|
|
129
|
+
"That path is not a directory.",
|
|
130
|
+
"Point --stack at a directory of arrays."),
|
|
131
|
+
(r"invalid vector ?field", FATAL,
|
|
132
|
+
"The vector field file could not be used with this array.",
|
|
133
|
+
"It needs the array's spatial shape plus a trailing components axis."),
|
|
134
|
+
(r"could not discover masks|could not match collection", FATAL,
|
|
135
|
+
"The pattern matched no files.",
|
|
136
|
+
"Check the pattern and run with --dry-run to see what it resolves to."),
|
|
137
|
+
(r"invalid launch request", FATAL,
|
|
138
|
+
"The launch arguments did not form a valid request.",
|
|
139
|
+
"Re-run with --diagnose to see how the launch was planned."),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
_COMPILED = [(re.compile(p, re.I), sev, what, fix) for p, sev, what, fix in _DIAGNOSES]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _colour_enabled(stream) -> bool:
|
|
146
|
+
if os.environ.get("NO_COLOR"):
|
|
147
|
+
return False
|
|
148
|
+
if os.environ.get("ARRAYVIEW_FORCE_COLOR"):
|
|
149
|
+
return True
|
|
150
|
+
return bool(getattr(stream, "isatty", lambda: False)())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def diagnose(message: str) -> tuple[str, str, str]:
|
|
154
|
+
"""Classify a failure message → (severity, what happened, what to do)."""
|
|
155
|
+
text = (message or "").strip()
|
|
156
|
+
for pattern, severity, what, fix in _COMPILED:
|
|
157
|
+
if pattern.search(text):
|
|
158
|
+
return severity, what, fix
|
|
159
|
+
return (
|
|
160
|
+
FATAL,
|
|
161
|
+
"ArrayView failed to open the display.",
|
|
162
|
+
"Re-run with --trace (or ARRAYVIEW_TRACE=1) for the full traceback.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def format_failure(message: str, *, colour: bool = True) -> str:
|
|
167
|
+
"""Render the diagnosis as the block printed to stderr."""
|
|
168
|
+
severity, what, fix = diagnose(message)
|
|
169
|
+
detail = (message or "").strip().removeprefix("[ArrayView] ").rstrip()
|
|
170
|
+
head = (_RED if severity == FATAL else _ORANGE) if colour else ""
|
|
171
|
+
dim = _DIM if colour else ""
|
|
172
|
+
end = _RESET if colour else ""
|
|
173
|
+
lines = [
|
|
174
|
+
f"{head}[ArrayView] {what}{end}",
|
|
175
|
+
f"{head} → {fix}{end}",
|
|
176
|
+
]
|
|
177
|
+
# A pending setup step is not a failure report: the instruction is the
|
|
178
|
+
# whole message. Echoing the internal wording underneath it is what made
|
|
179
|
+
# "the opener is installed, reload once" read like something broke.
|
|
180
|
+
if severity == SETUP:
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
# Keep the raw text when it says more than the headline does.
|
|
183
|
+
if detail and detail.lower() not in what.lower():
|
|
184
|
+
lines.append(f"{dim} ({detail}){end}")
|
|
185
|
+
return "\n".join(lines)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def print_failure(message: str, stream=None) -> None:
|
|
189
|
+
stream = stream if stream is not None else sys.stderr
|
|
190
|
+
print(format_failure(message, colour=_colour_enabled(stream)), file=stream)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def format_notice(message: str, *, colour: bool = True) -> str:
|
|
194
|
+
"""Render a one-line ACTION notice.
|
|
195
|
+
|
|
196
|
+
Same orange as an ACTION failure, because it carries the same meaning —
|
|
197
|
+
something the user asked for is not happening — but the launch continues,
|
|
198
|
+
so there is no cause/fix block to print.
|
|
199
|
+
"""
|
|
200
|
+
head = _ORANGE if colour else ""
|
|
201
|
+
end = _RESET if colour else ""
|
|
202
|
+
return f"{head}[ArrayView] {message}{end}"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def print_notice(message: str, stream=None) -> None:
|
|
206
|
+
stream = stream if stream is not None else sys.stdout
|
|
207
|
+
print(
|
|
208
|
+
format_notice(message, colour=_colour_enabled(stream)),
|
|
209
|
+
file=stream,
|
|
210
|
+
flush=True,
|
|
211
|
+
)
|
|
@@ -1290,12 +1290,47 @@ def load_dir_collection(
|
|
|
1290
1290
|
return data, spatial_meta, overlay_items, summary
|
|
1291
1291
|
|
|
1292
1292
|
|
|
1293
|
+
def _dicom_series_collection(path, *, load="lazy", stack="auto"):
|
|
1294
|
+
"""Stack a directory holding several DICOM series, one case per series.
|
|
1295
|
+
|
|
1296
|
+
Right-clicking a study folder in the VS Code Explorer hands over the parent
|
|
1297
|
+
of a set of per-case DICOM folders. There is no single series to show, and
|
|
1298
|
+
``--series`` is unreachable from that gesture, so the load failed with
|
|
1299
|
+
"Multiple DICOM series found ... Select one with --series INDEX". Build the
|
|
1300
|
+
collection instead: one stack element per series, which is what "open this
|
|
1301
|
+
folder" means for that layout.
|
|
1302
|
+
|
|
1303
|
+
Grouping by series rather than by subfolder is what the DICOM headers
|
|
1304
|
+
actually say. It gives the same answer for the usual one-series-per-folder
|
|
1305
|
+
layout and a better one when a folder holds two series or a series is split
|
|
1306
|
+
across folders.
|
|
1307
|
+
|
|
1308
|
+
Returns ``None`` when the directory holds a single series: the ordinary
|
|
1309
|
+
one-series load handles that and keeps its spatial metadata, which a
|
|
1310
|
+
stacked collection cannot carry.
|
|
1311
|
+
"""
|
|
1312
|
+
from ._dicom import discover_dicom_series
|
|
1313
|
+
|
|
1314
|
+
series = discover_dicom_series(path)
|
|
1315
|
+
if len(series) < 2:
|
|
1316
|
+
return None
|
|
1317
|
+
# One member per series is enough to name it: loading any slice of a DICOM
|
|
1318
|
+
# series loads the whole series it belongs to.
|
|
1319
|
+
return _series_from_file_matrix(
|
|
1320
|
+
[[item["headers"][0].path] for item in series], load=load, stack=stack
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
|
|
1293
1324
|
def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
1294
1325
|
"""Build a lazy series from a directory of supported array files.
|
|
1295
1326
|
|
|
1296
|
-
Walks *path* recursively
|
|
1297
|
-
immediate parent folder (= patient).
|
|
1298
|
-
|
|
1327
|
+
Walks *path* recursively and groups files of any supported format by
|
|
1328
|
+
immediate parent folder (= patient).
|
|
1329
|
+
|
|
1330
|
+
One file per patient folder → 4D ``(*vol, P)``. When a folder holds
|
|
1331
|
+
several arrays side by side there is no per-folder case to key on, so every
|
|
1332
|
+
file becomes its own case — the same collection ``--stack`` builds for that
|
|
1333
|
+
directory.
|
|
1299
1334
|
|
|
1300
1335
|
If every file is NIfTI (.nii/.nii.gz), delegates to
|
|
1301
1336
|
``_load_nifti_series`` for canonical reorientation. Otherwise builds
|
|
@@ -1304,6 +1339,8 @@ def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
|
1304
1339
|
Returns ``(series, spatial_meta)``.
|
|
1305
1340
|
"""
|
|
1306
1341
|
# ── collect files by parent directory ──────────────────────────
|
|
1342
|
+
from ._dicom import is_dicom_source
|
|
1343
|
+
|
|
1307
1344
|
patients: dict[str, list[str]] = {}
|
|
1308
1345
|
all_nifti = True
|
|
1309
1346
|
|
|
@@ -1312,6 +1349,14 @@ def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
|
1312
1349
|
os.path.join(root, f)
|
|
1313
1350
|
for f in files
|
|
1314
1351
|
if _get_ext(f) in _SUPPORTED_EXTS
|
|
1352
|
+
# ".dcm" is necessary but not sufficient — every other .dcm
|
|
1353
|
+
# consumer (load_data, load_data_with_meta, _peek_file_shape's
|
|
1354
|
+
# directory branch) validates via is_dicom_source before
|
|
1355
|
+
# treating a file as real DICOM. Do the same here, otherwise a
|
|
1356
|
+
# stray file that merely has a .dcm extension gets counted as a
|
|
1357
|
+
# patient's series member and blows up load_data() downstream
|
|
1358
|
+
# instead of being silently ignored like any other unsupported file.
|
|
1359
|
+
if _get_ext(f) != ".dcm" or is_dicom_source(os.path.join(root, f))
|
|
1315
1360
|
)
|
|
1316
1361
|
if supported:
|
|
1317
1362
|
patients[root] = supported
|
|
@@ -1327,24 +1372,24 @@ def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
|
1327
1372
|
f"Supported: {', '.join(sorted(_SUPPORTED_EXTS))}"
|
|
1328
1373
|
)
|
|
1329
1374
|
|
|
1375
|
+
# A folder holding several arrays side by side is a flat collection: one
|
|
1376
|
+
# case per file. This used to raise and tell the user to retype the command
|
|
1377
|
+
# with --stack, which made "open this folder" unreachable from any caller
|
|
1378
|
+
# that can only hand over a path (VS Code Explorer, drag-and-drop, the
|
|
1379
|
+
# /load route). Route it through the same builder --stack uses so both
|
|
1380
|
+
# spellings produce the same session instead of one of them failing.
|
|
1381
|
+
if any(len(files) != 1 for files in patients.values()):
|
|
1382
|
+
flat = sorted(fpath for files in patients.values() for fpath in files)
|
|
1383
|
+
return _series_from_file_matrix(
|
|
1384
|
+
[[fpath] for fpath in flat], load=load, stack=stack
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1330
1387
|
# NIfTI-only → preserve canonical reorientation + lazy dataobj slicing
|
|
1331
1388
|
if all_nifti:
|
|
1332
1389
|
return _load_nifti_series(path, load=load, stack=stack)
|
|
1333
1390
|
|
|
1334
1391
|
# ── build file matrix ──────────────────────────────────────────
|
|
1335
|
-
|
|
1336
|
-
file_matrix: list[list[str]] = []
|
|
1337
|
-
|
|
1338
|
-
for pdir in patient_dirs:
|
|
1339
|
-
files = patients[pdir]
|
|
1340
|
-
if len(files) == 1:
|
|
1341
|
-
file_matrix.append([files[0]])
|
|
1342
|
-
else:
|
|
1343
|
-
raise ValueError(
|
|
1344
|
-
f"Folder {pdir!r} contains {len(files)} files: "
|
|
1345
|
-
f"{[os.path.basename(f) for f in files]}. "
|
|
1346
|
-
"Use --stack with explicit file patterns for multi-file folders."
|
|
1347
|
-
)
|
|
1392
|
+
file_matrix = [[patients[pdir][0]] for pdir in sorted(patients)]
|
|
1348
1393
|
|
|
1349
1394
|
return _series_from_file_matrix(file_matrix, load=load, stack=stack)
|
|
1350
1395
|
|
|
@@ -1476,9 +1521,20 @@ def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select
|
|
|
1476
1521
|
"""
|
|
1477
1522
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1478
1523
|
|
|
1479
|
-
|
|
1524
|
+
# A ".zarr" store is itself a directory on disk. It must be excluded from
|
|
1525
|
+
# the generic "directory of files to stack" path below, or the walk finds
|
|
1526
|
+
# nothing matching a supported single-file extension inside it (zarr's own
|
|
1527
|
+
# chunk/metadata files don't) and raises instead of opening the store.
|
|
1528
|
+
is_zarr_dir = os.path.isdir(filepath) and _get_ext(filepath) == ".zarr"
|
|
1529
|
+
if (
|
|
1530
|
+
(os.path.isdir(filepath) and not is_zarr_dir) or filepath.lower().endswith(".dcm")
|
|
1531
|
+
) and is_dicom_source(filepath):
|
|
1532
|
+
if os.path.isdir(filepath) and select is None:
|
|
1533
|
+
collection = _dicom_series_collection(filepath, load=load, stack=stack)
|
|
1534
|
+
if collection is not None:
|
|
1535
|
+
return collection
|
|
1480
1536
|
return load_dicom_series(filepath, select=select)
|
|
1481
|
-
if os.path.isdir(filepath):
|
|
1537
|
+
if os.path.isdir(filepath) and not is_zarr_dir:
|
|
1482
1538
|
return _load_file_series(filepath, load=load, stack=stack)
|
|
1483
1539
|
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
1484
1540
|
return _load_nifti_with_meta(filepath)
|
|
@@ -1491,7 +1547,7 @@ def load_data(filepath, key=None):
|
|
|
1491
1547
|
|
|
1492
1548
|
data, _meta = load_dicom_series(filepath)
|
|
1493
1549
|
return data
|
|
1494
|
-
if os.path.isdir(filepath):
|
|
1550
|
+
if os.path.isdir(filepath) and _get_ext(filepath) != ".zarr":
|
|
1495
1551
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1496
1552
|
|
|
1497
1553
|
if is_dicom_source(filepath):
|
|
@@ -1592,6 +1648,16 @@ def load_data(filepath, key=None):
|
|
|
1592
1648
|
"Select one in the viewer or pass a key."
|
|
1593
1649
|
)
|
|
1594
1650
|
else:
|
|
1651
|
+
# DICOM files routinely carry no extension at all. Every path that
|
|
1652
|
+
# reaches here by extension has already been ruled out, so a header
|
|
1653
|
+
# read is cheap and is the only thing that can still recognise one —
|
|
1654
|
+
# this is how a per-series representative path from
|
|
1655
|
+
# `_dicom_series_collection` resolves back to its series.
|
|
1656
|
+
from ._dicom import is_dicom_source, load_dicom_series
|
|
1657
|
+
|
|
1658
|
+
if os.path.isfile(filepath) and is_dicom_source(filepath):
|
|
1659
|
+
data, _meta = load_dicom_series(filepath)
|
|
1660
|
+
return data
|
|
1595
1661
|
raise ValueError(
|
|
1596
1662
|
"Unsupported format. Supported: .npy, .npz, .nii/.nii.gz, .zarr, "
|
|
1597
1663
|
".pt/.pth, .h5/.hdf5, .tif/.tiff, .mat"
|
|
@@ -1646,6 +1712,13 @@ FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat"])
|
|
|
1646
1712
|
def _peek_file_shape(fpath: str, ext: str):
|
|
1647
1713
|
"""Try to return shape quickly without loading the full array. Returns None on failure."""
|
|
1648
1714
|
try:
|
|
1715
|
+
# ".zarr" stores are directories, but they're a single array, not a
|
|
1716
|
+
# collection to walk and stack — read metadata via zarr, not the
|
|
1717
|
+
# directory-of-files branch below.
|
|
1718
|
+
if ext in (".zarr", ".zarr.zip"):
|
|
1719
|
+
import zarr
|
|
1720
|
+
|
|
1721
|
+
return list(zarr.open(fpath, mode="r").shape)
|
|
1649
1722
|
if os.path.isdir(fpath):
|
|
1650
1723
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1651
1724
|
|