arrayview 0.37.1__tar.gz → 0.39.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.37.1 → arrayview-0.39.0}/PKG-INFO +1 -1
- {arrayview-0.37.1 → arrayview-0.39.0}/pyproject.toml +1 -2
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_io.py +27 -3
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_launch_plan.py +0 -1
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_launcher.py +20 -5
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_platform.py +70 -5
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_preferences.py +4 -1
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_query.py +1 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_viewer.html +101 -9
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_vscode_browser.py +6 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_vscode_extension.py +36 -15
- arrayview-0.39.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/conftest.py +23 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_api.py +138 -3
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_invocation_contract_matrix.py +0 -4
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_launch_blackbox.py +3 -1
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_launch_plan.py +0 -1
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_lifecycle_contract.py +23 -2
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_rgb_pixel_art.py +23 -7
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_view_launch_execution.py +5 -0
- arrayview-0.37.1/src/arrayview/_codex_open.py +0 -137
- arrayview-0.37.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.37.1/tests/test_codex_open.py +0 -160
- {arrayview-0.37.1 → arrayview-0.39.0}/.gitignore +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/LICENSE +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/README.md +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/src/arrayview/todo.md +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_browser.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_cli.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_config.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_dicom.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_drop_import.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_instance_registry.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_interactions.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_io.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_launch_trace.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_nifti_series.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_session_spec.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_torch.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_value_formatting.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/ui_audit.py +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.37.1 → arrayview-0.39.0}/tests/visual_smoke.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arrayview"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.39.0"
|
|
8
8
|
description = "Fast multi-dimensional array viewer"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -43,7 +43,6 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[project.scripts]
|
|
45
45
|
arrayview = "arrayview._launcher:arrayview"
|
|
46
|
-
arrayview-codex = "arrayview._codex_open:main"
|
|
47
46
|
|
|
48
47
|
[tool.hatch.build.targets.wheel]
|
|
49
48
|
packages = ["src/arrayview"]
|
|
@@ -1304,6 +1304,8 @@ def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
|
1304
1304
|
Returns ``(series, spatial_meta)``.
|
|
1305
1305
|
"""
|
|
1306
1306
|
# ── collect files by parent directory ──────────────────────────
|
|
1307
|
+
from ._dicom import is_dicom_source
|
|
1308
|
+
|
|
1307
1309
|
patients: dict[str, list[str]] = {}
|
|
1308
1310
|
all_nifti = True
|
|
1309
1311
|
|
|
@@ -1312,6 +1314,14 @@ def _load_file_series(path, *, load="lazy", stack="auto"):
|
|
|
1312
1314
|
os.path.join(root, f)
|
|
1313
1315
|
for f in files
|
|
1314
1316
|
if _get_ext(f) in _SUPPORTED_EXTS
|
|
1317
|
+
# ".dcm" is necessary but not sufficient — every other .dcm
|
|
1318
|
+
# consumer (load_data, load_data_with_meta, _peek_file_shape's
|
|
1319
|
+
# directory branch) validates via is_dicom_source before
|
|
1320
|
+
# treating a file as real DICOM. Do the same here, otherwise a
|
|
1321
|
+
# stray file that merely has a .dcm extension gets counted as a
|
|
1322
|
+
# patient's series member and blows up load_data() downstream
|
|
1323
|
+
# instead of being silently ignored like any other unsupported file.
|
|
1324
|
+
if _get_ext(f) != ".dcm" or is_dicom_source(os.path.join(root, f))
|
|
1315
1325
|
)
|
|
1316
1326
|
if supported:
|
|
1317
1327
|
patients[root] = supported
|
|
@@ -1476,9 +1486,16 @@ def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select
|
|
|
1476
1486
|
"""
|
|
1477
1487
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1478
1488
|
|
|
1479
|
-
|
|
1489
|
+
# A ".zarr" store is itself a directory on disk. It must be excluded from
|
|
1490
|
+
# the generic "directory of files to stack" path below, or the walk finds
|
|
1491
|
+
# nothing matching a supported single-file extension inside it (zarr's own
|
|
1492
|
+
# chunk/metadata files don't) and raises instead of opening the store.
|
|
1493
|
+
is_zarr_dir = os.path.isdir(filepath) and _get_ext(filepath) == ".zarr"
|
|
1494
|
+
if (
|
|
1495
|
+
(os.path.isdir(filepath) and not is_zarr_dir) or filepath.lower().endswith(".dcm")
|
|
1496
|
+
) and is_dicom_source(filepath):
|
|
1480
1497
|
return load_dicom_series(filepath, select=select)
|
|
1481
|
-
if os.path.isdir(filepath):
|
|
1498
|
+
if os.path.isdir(filepath) and not is_zarr_dir:
|
|
1482
1499
|
return _load_file_series(filepath, load=load, stack=stack)
|
|
1483
1500
|
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
1484
1501
|
return _load_nifti_with_meta(filepath)
|
|
@@ -1491,7 +1508,7 @@ def load_data(filepath, key=None):
|
|
|
1491
1508
|
|
|
1492
1509
|
data, _meta = load_dicom_series(filepath)
|
|
1493
1510
|
return data
|
|
1494
|
-
if os.path.isdir(filepath):
|
|
1511
|
+
if os.path.isdir(filepath) and _get_ext(filepath) != ".zarr":
|
|
1495
1512
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1496
1513
|
|
|
1497
1514
|
if is_dicom_source(filepath):
|
|
@@ -1646,6 +1663,13 @@ FULL_LOAD_EXTS = frozenset([".pt", ".pth", ".tif", ".tiff", ".mat"])
|
|
|
1646
1663
|
def _peek_file_shape(fpath: str, ext: str):
|
|
1647
1664
|
"""Try to return shape quickly without loading the full array. Returns None on failure."""
|
|
1648
1665
|
try:
|
|
1666
|
+
# ".zarr" stores are directories, but they're a single array, not a
|
|
1667
|
+
# collection to walk and stack — read metadata via zarr, not the
|
|
1668
|
+
# directory-of-files branch below.
|
|
1669
|
+
if ext in (".zarr", ".zarr.zip"):
|
|
1670
|
+
import zarr
|
|
1671
|
+
|
|
1672
|
+
return list(zarr.open(fpath, mode="r").shape)
|
|
1649
1673
|
if os.path.isdir(fpath):
|
|
1650
1674
|
from ._dicom import is_dicom_source, load_dicom_series
|
|
1651
1675
|
|
|
@@ -44,6 +44,21 @@ import arrayview._platform as _platform_mod # for mutable globals
|
|
|
44
44
|
_vscode_mod_cache = None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _detached_child_kwargs() -> dict[str, object]:
|
|
48
|
+
"""Popen options placing a spawned child outside our own signal reach.
|
|
49
|
+
|
|
50
|
+
``start_new_session`` is POSIX-only, so passing ``sys.platform != "win32"``
|
|
51
|
+
left every Windows spawn in the parent's console process group. A console
|
|
52
|
+
control event raised for that group reaches *every* process attached to it,
|
|
53
|
+
so a child being shut down can deliver a KeyboardInterrupt to the process
|
|
54
|
+
that spawned it — under pytest, to the test runner itself. Both branches
|
|
55
|
+
state the same intent: the child gets its own group.
|
|
56
|
+
"""
|
|
57
|
+
if sys.platform == "win32":
|
|
58
|
+
return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
|
|
59
|
+
return {"start_new_session": True}
|
|
60
|
+
|
|
61
|
+
|
|
47
62
|
def _trace_launch_event(event: str, **attrs: object) -> None:
|
|
48
63
|
"""Emit an opt-in launch event without loading tracing on normal paths."""
|
|
49
64
|
if not os.environ.get("ARRAYVIEW_LAUNCH_TRACE"):
|
|
@@ -495,7 +510,7 @@ def _open_webview(
|
|
|
495
510
|
stdout=subprocess.DEVNULL,
|
|
496
511
|
stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
|
|
497
512
|
close_fds=True,
|
|
498
|
-
|
|
513
|
+
**_detached_child_kwargs(),
|
|
499
514
|
)
|
|
500
515
|
|
|
501
516
|
# URL mode — direct load (used when shell_port not provided)
|
|
@@ -545,7 +560,7 @@ def _open_webview(
|
|
|
545
560
|
stdout=subprocess.DEVNULL,
|
|
546
561
|
stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
|
|
547
562
|
close_fds=True,
|
|
548
|
-
|
|
563
|
+
**_detached_child_kwargs(),
|
|
549
564
|
)
|
|
550
565
|
|
|
551
566
|
|
|
@@ -2062,7 +2077,7 @@ def _handle_cli_spawned_daemon(
|
|
|
2062
2077
|
"stdout": subprocess.DEVNULL,
|
|
2063
2078
|
"stderr": subprocess.DEVNULL,
|
|
2064
2079
|
"close_fds": True,
|
|
2065
|
-
|
|
2080
|
+
**_detached_child_kwargs(),
|
|
2066
2081
|
}
|
|
2067
2082
|
if os.environ.get("ARRAYVIEW_LAUNCH_TRACE"):
|
|
2068
2083
|
from arrayview._launch_trace import trace_child_environment
|
|
@@ -3998,7 +4013,7 @@ def _view_subprocess(
|
|
|
3998
4013
|
stdout=subprocess.DEVNULL,
|
|
3999
4014
|
stderr=subprocess.DEVNULL,
|
|
4000
4015
|
close_fds=True,
|
|
4001
|
-
|
|
4016
|
+
**_detached_child_kwargs(),
|
|
4002
4017
|
)
|
|
4003
4018
|
if not _wait_for_spawned_server(daemon_proc, port, timeout=15.0):
|
|
4004
4019
|
raise RuntimeError(
|
|
@@ -5439,7 +5454,7 @@ def arrayview():
|
|
|
5439
5454
|
stdout=subprocess.DEVNULL,
|
|
5440
5455
|
stderr=subprocess.DEVNULL,
|
|
5441
5456
|
close_fds=True,
|
|
5442
|
-
|
|
5457
|
+
**_detached_child_kwargs(),
|
|
5443
5458
|
)
|
|
5444
5459
|
if not _wait_for_spawned_server(proc, args.port, timeout=15.0):
|
|
5445
5460
|
_terminate_owned_process(proc)
|
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
|
|
@@ -450,6 +451,42 @@ def _process_is_alive(pid: object) -> bool:
|
|
|
450
451
|
return False
|
|
451
452
|
|
|
452
453
|
|
|
454
|
+
def _superseding_window_registration(
|
|
455
|
+
claimed_ids: list[str],
|
|
456
|
+
signal_dir: str,
|
|
457
|
+
read: Callable[[str], tuple[str, dict] | None],
|
|
458
|
+
) -> tuple[str, dict] | None:
|
|
459
|
+
"""Return the live window that replaced one of ``claimed_ids``, if unique.
|
|
460
|
+
|
|
461
|
+
Each registration lists the window ids it took over from, so a terminal left
|
|
462
|
+
holding a pre-reload id resolves to its own window rather than to a sibling
|
|
463
|
+
that merely shares a server root. Two live windows claiming one id would be a
|
|
464
|
+
contradiction, so an ambiguous answer is refused instead of guessed.
|
|
465
|
+
"""
|
|
466
|
+
if not claimed_ids:
|
|
467
|
+
return None
|
|
468
|
+
wanted = set(claimed_ids)
|
|
469
|
+
try:
|
|
470
|
+
filenames = os.listdir(signal_dir)
|
|
471
|
+
except OSError:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
matches: list[tuple[str, dict]] = []
|
|
475
|
+
for filename in filenames:
|
|
476
|
+
if not filename.startswith("window-") or not filename.endswith(".json"):
|
|
477
|
+
continue
|
|
478
|
+
candidate = read(filename[len("window-") : -len(".json")])
|
|
479
|
+
if candidate is None:
|
|
480
|
+
continue
|
|
481
|
+
superseded = candidate[1].get("supersedes")
|
|
482
|
+
if not isinstance(superseded, list):
|
|
483
|
+
continue
|
|
484
|
+
if wanted.intersection(item for item in superseded if isinstance(item, str)):
|
|
485
|
+
matches.append(candidate)
|
|
486
|
+
|
|
487
|
+
return matches[0] if len(matches) == 1 else None
|
|
488
|
+
|
|
489
|
+
|
|
453
490
|
def _exact_vscode_window_registration(
|
|
454
491
|
ipc: str | None = None,
|
|
455
492
|
) -> tuple[str, dict] | None:
|
|
@@ -461,6 +498,11 @@ def _exact_vscode_window_registration(
|
|
|
461
498
|
remote registration is unsafe. When direct window evidence is unavailable,
|
|
462
499
|
match the live extension-host executable to the exact server root named by
|
|
463
500
|
the terminal's NLS configuration, and only accept a unique match.
|
|
501
|
+
|
|
502
|
+
Reloading a window rotates its IPC socket and re-injects ``ARRAYVIEW_WINDOW_ID``
|
|
503
|
+
into new terminals only, so a terminal that predates the reload keeps naming a
|
|
504
|
+
window id that no longer registers. The extension records the ids it replaced,
|
|
505
|
+
which turns that stale name back into an exact match on the window that owns it.
|
|
464
506
|
"""
|
|
465
507
|
signal_dir = os.path.expanduser("~/.arrayview")
|
|
466
508
|
|
|
@@ -479,16 +521,23 @@ def _exact_vscode_window_registration(
|
|
|
479
521
|
return None
|
|
480
522
|
return window_id, registration
|
|
481
523
|
|
|
524
|
+
claimed_ids: list[str] = []
|
|
482
525
|
registration = None
|
|
483
526
|
if ipc:
|
|
484
527
|
window_id = hashlib.sha256(ipc.encode()).hexdigest()[:16]
|
|
485
528
|
if reg := _read(window_id):
|
|
486
529
|
return reg
|
|
530
|
+
claimed_ids.append(window_id)
|
|
487
531
|
registration = reg
|
|
488
532
|
if registration is None:
|
|
489
533
|
window_id = os.environ.get("ARRAYVIEW_WINDOW_ID")
|
|
490
|
-
if window_id
|
|
491
|
-
|
|
534
|
+
if window_id:
|
|
535
|
+
if reg := _read(window_id):
|
|
536
|
+
return reg
|
|
537
|
+
claimed_ids.append(window_id)
|
|
538
|
+
|
|
539
|
+
if reg := _superseding_window_registration(claimed_ids, signal_dir, _read):
|
|
540
|
+
return reg
|
|
492
541
|
|
|
493
542
|
exact_cli = _current_vscode_remote_cli()
|
|
494
543
|
if exact_cli is None:
|
|
@@ -532,9 +581,25 @@ def _exact_vscode_registration_remote(ipc: str) -> bool | None:
|
|
|
532
581
|
both can exist in a local desktop window.
|
|
533
582
|
"""
|
|
534
583
|
exact = _exact_vscode_window_registration(ipc)
|
|
535
|
-
if exact is
|
|
536
|
-
return
|
|
537
|
-
|
|
584
|
+
if exact is None or "remoteName" not in exact[1]:
|
|
585
|
+
return None
|
|
586
|
+
# _exact_vscode_window_registration's ARRAYVIEW_WINDOW_ID fallback trusts
|
|
587
|
+
# that id's own file directly and unconditionally — other callers (signal
|
|
588
|
+
# targeting, extension-version checks) need it to still resolve a window
|
|
589
|
+
# when ipc is unavailable or has legitimately drifted (e.g. a tunnel
|
|
590
|
+
# re-sockets without the window itself changing). Answering "is this
|
|
591
|
+
# remote" is riskier: a wrong placement picks the wrong display path
|
|
592
|
+
# outright. So here specifically, a raw env-var match with nothing to
|
|
593
|
+
# corroborate it against a live, unmatched ipc is refused rather than
|
|
594
|
+
# trusted — fall through to the caller's other signals instead.
|
|
595
|
+
env_window_id = os.environ.get("ARRAYVIEW_WINDOW_ID")
|
|
596
|
+
if (
|
|
597
|
+
ipc
|
|
598
|
+
and exact[0] == env_window_id
|
|
599
|
+
and exact[0] != hashlib.sha256(ipc.encode()).hexdigest()[:16]
|
|
600
|
+
):
|
|
601
|
+
return None
|
|
602
|
+
return exact[1].get("remoteName") is not None
|
|
538
603
|
|
|
539
604
|
|
|
540
605
|
def _has_live_vscode_remote_registration() -> bool:
|
|
@@ -42,7 +42,10 @@ def _response_payload() -> dict:
|
|
|
42
42
|
value for value in schema["window"][key] if value != "native"
|
|
43
43
|
]
|
|
44
44
|
if vscode_remote:
|
|
45
|
-
|
|
45
|
+
# Native and browser don't make sense from a headless remote host;
|
|
46
|
+
# "none" is an internal fallback only, hidden from every row above —
|
|
47
|
+
# this hardcoded replacement must keep hiding it too.
|
|
48
|
+
schema["window"]["vscode"] = ["vscode"]
|
|
46
49
|
|
|
47
50
|
overrides = {}
|
|
48
51
|
if os.environ.get("ARRAYVIEW_WINDOW", "").strip():
|
|
@@ -6159,6 +6159,8 @@
|
|
|
6159
6159
|
let rangeLocked = true; // when true, d-set limits never auto-recompute on navigation
|
|
6160
6160
|
let vminLocked = false; // lock lower bound independently
|
|
6161
6161
|
let vmaxLocked = false; // lock upper bound independently
|
|
6162
|
+
let symmetricLocked = false; // tie vmin = -vmax so zero stays centred
|
|
6163
|
+
let _dmenuSymmetricMirroring = false; // re-entrancy guard for the mirror write
|
|
6162
6164
|
let excludeZeros = false; // exclude zero-valued voxels from histogram computation
|
|
6163
6165
|
let spatialMeta = null; // {voxel_sizes, axis_labels, is_oblique} or null
|
|
6164
6166
|
let rasResampleActive = false;
|
|
@@ -20118,6 +20120,8 @@
|
|
|
20118
20120
|
|
|
20119
20121
|
// ── Brightness/Contrast drag (Cmd+Shift+drag) and scroll ─────────
|
|
20120
20122
|
let _bcDrag = null; // { startX, startY, initCenter, initWidth }
|
|
20123
|
+
let _bcPendingDrag = null; // { startX, startY } — armed plain-left press, not yet a drag
|
|
20124
|
+
const _BC_DRAG_DEADZONE = 5; // CSS px a press must travel before it counts as windowing
|
|
20121
20125
|
let _bcScrollTimer = null; // debounce timer for scroll-based BC
|
|
20122
20126
|
let _bcScrollAnchor = null; // { center, width, accumX, accumY } for scroll accumulation
|
|
20123
20127
|
let _bcUpdateTimer = null; // throttle timer for server updates during drag
|
|
@@ -20183,6 +20187,12 @@
|
|
|
20183
20187
|
startX: e.clientX, startY: e.clientY,
|
|
20184
20188
|
initCenter: (vmin + vmax) / 2,
|
|
20185
20189
|
initWidth: vmax - vmin,
|
|
20190
|
+
// A locked vmin or a symmetric lock removes a degree of
|
|
20191
|
+
// freedom, so the drag has to collapse to one axis — see
|
|
20192
|
+
// _bcUpdateDrag.
|
|
20193
|
+
vminLocked: _dmenuEffectiveVminLocked(),
|
|
20194
|
+
symmetric: _dmenuEffectiveSymmetricLocked(),
|
|
20195
|
+
lockedLo: vmin,
|
|
20186
20196
|
};
|
|
20187
20197
|
canvas.style.cursor = 'crosshair';
|
|
20188
20198
|
_bcExpandHist();
|
|
@@ -20193,12 +20203,46 @@
|
|
|
20193
20203
|
const dy = e.clientY - _bcDrag.startY;
|
|
20194
20204
|
// Sensitivity: scale by initial width so drag feels proportional
|
|
20195
20205
|
const sens = _bcDrag.initWidth / 300;
|
|
20196
|
-
|
|
20197
|
-
|
|
20206
|
+
let newWidth = _bcDrag.initWidth - dy * sens * 2;
|
|
20207
|
+
let newCenter;
|
|
20208
|
+
if (_bcDrag.symmetric) {
|
|
20209
|
+
// Bounds are tied to ±half-width, so the centre is fixed at
|
|
20210
|
+
// zero and only the width is free — vertical-only, same shape
|
|
20211
|
+
// as the pinned-vmin case below but anchored differently.
|
|
20212
|
+
newWidth = Math.max(newWidth, Math.abs(_bcDrag.initWidth) * 1e-3 || 1e-9);
|
|
20213
|
+
newCenter = 0;
|
|
20214
|
+
} else if (_bcDrag.vminLocked) {
|
|
20215
|
+
// vmin is pinned (manual lock, qMRI role, or zero floor), so
|
|
20216
|
+
// vmax is the only free bound and centre/width are no longer
|
|
20217
|
+
// independent. The horizontal axis has nothing to move: the
|
|
20218
|
+
// drag collapses to vertical-only, growing the window upward
|
|
20219
|
+
// from the pinned vmin. Clamped so vmax can never cross it.
|
|
20220
|
+
newWidth = Math.max(newWidth, Math.abs(_bcDrag.initWidth) * 1e-3 || 1e-9);
|
|
20221
|
+
newCenter = _bcDrag.lockedLo + newWidth / 2;
|
|
20222
|
+
} else {
|
|
20223
|
+
newCenter = _bcDrag.initCenter - dx * sens;
|
|
20224
|
+
}
|
|
20198
20225
|
_bcApplyWindow(newCenter, newWidth);
|
|
20199
20226
|
_bcThrottledUpdate();
|
|
20200
20227
|
}
|
|
20228
|
+
// A plain left press arms a pending drag rather than starting one, so a
|
|
20229
|
+
// press that never travels stays a click-to-copy / pixel-info hold.
|
|
20230
|
+
// Past the deadzone it becomes window/level — the PACS convention for a
|
|
20231
|
+
// single 2D view. Ortho and the other multi-view panes have their own
|
|
20232
|
+
// canvases and keep left-drag for crosshair navigation.
|
|
20233
|
+
function _bcMaybePromotePending(e) {
|
|
20234
|
+
if (!_bcPendingDrag || _bcDrag) return;
|
|
20235
|
+
if (!(e.buttons & 1)) { _bcPendingDrag = null; return; }
|
|
20236
|
+
const origin = _bcPendingDrag;
|
|
20237
|
+
if (Math.hypot(e.clientX - origin.startX, e.clientY - origin.startY) < _BC_DRAG_DEADZONE) return;
|
|
20238
|
+
_bcPendingDrag = null;
|
|
20239
|
+
if (_pixelInfoHeld) { _pixelInfoHeld = false; hideAllPixelInfo(); }
|
|
20240
|
+
// Anchor to the press origin, not to here, so the window doesn't
|
|
20241
|
+
// jump by the deadzone distance on the first frame.
|
|
20242
|
+
_bcStartDrag({ clientX: origin.startX, clientY: origin.startY });
|
|
20243
|
+
}
|
|
20201
20244
|
function _bcEndDrag() {
|
|
20245
|
+
_bcPendingDrag = null;
|
|
20202
20246
|
if (!_bcDrag) return;
|
|
20203
20247
|
_bcDrag = null;
|
|
20204
20248
|
canvas.style.cursor = '';
|
|
@@ -20321,6 +20365,7 @@
|
|
|
20321
20365
|
if (!rectRoiMode && e.button === 0 && !e.shiftKey && !e.metaKey && !e.altKey
|
|
20322
20366
|
&& !_cropActive && !_segMode && !_rulerMode) {
|
|
20323
20367
|
_pixelInfoHeld = true;
|
|
20368
|
+
if (_bcCanDrag()) _bcPendingDrag = { startX: e.clientX, startY: e.clientY };
|
|
20324
20369
|
canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: e.clientX, clientY: e.clientY, buttons: 1, ctrlKey: e.ctrlKey, bubbles: true }));
|
|
20325
20370
|
setTimeout(() => {
|
|
20326
20371
|
if (!_pixelInfoHeld) return;
|
|
@@ -21437,7 +21482,7 @@
|
|
|
21437
21482
|
}
|
|
21438
21483
|
// Old export-btn and roi-content listeners removed — now handled by island actions
|
|
21439
21484
|
// Brightness/contrast: mousemove updates vmin/vmax
|
|
21440
|
-
canvas.addEventListener('mousemove', e => { _bcUpdateDrag(e); });
|
|
21485
|
+
canvas.addEventListener('mousemove', e => { _bcMaybePromotePending(e); _bcUpdateDrag(e); });
|
|
21441
21486
|
// Crop drag-handle: while dragging, update the matching edge; otherwise
|
|
21442
21487
|
// provide hover-cursor feedback when near an edge. Runs even when
|
|
21443
21488
|
// Alt is held so we still update during an accidental Alt hover,
|
|
@@ -34766,6 +34811,17 @@
|
|
|
34766
34811
|
return vminLocked || _dmenuPolicyLocksVmin();
|
|
34767
34812
|
}
|
|
34768
34813
|
|
|
34814
|
+
// Signed data whose zero is a reference point — difference images,
|
|
34815
|
+
// phase, field maps, statistical maps — only reads correctly when zero
|
|
34816
|
+
// sits at the centre of the colormap; window it asymmetrically and the
|
|
34817
|
+
// neutral colour drifts off zero, so sign can no longer be judged by
|
|
34818
|
+
// eye. This lock ties the bounds together to prevent that. It stays
|
|
34819
|
+
// opt-in rather than auto-engaging on any signed data, because plenty
|
|
34820
|
+
// of signed data (CT in Hounsfield units) has no symmetry about zero.
|
|
34821
|
+
function _dmenuEffectiveSymmetricLocked() {
|
|
34822
|
+
return symmetricLocked;
|
|
34823
|
+
}
|
|
34824
|
+
|
|
34769
34825
|
function _dmenuRows() {
|
|
34770
34826
|
const qmriPolicyLocksVmin = _dmenuQmriPolicyLocksVmin();
|
|
34771
34827
|
const zeroFloorLocksVmin = _dmenuZeroFloorLocksVmin();
|
|
@@ -34773,6 +34829,8 @@
|
|
|
34773
34829
|
return [
|
|
34774
34830
|
{ kind: 'lock', action: 'vmin', label: 'vmin', policyLocked: policyLocksVmin, zeroFloorLocked: zeroFloorLocksVmin, qmriPolicyLocked: qmriPolicyLocksVmin, note: _dmenuEffectiveVminLocked() ? 'locked' : '' },
|
|
34775
34831
|
{ kind: 'lock', action: 'vmax', label: 'vmax', note: vmaxLocked ? 'locked' : '' },
|
|
34832
|
+
{ kind: 'lock', action: 'symmetric', label: '±', note: _dmenuEffectiveSymmetricLocked() ? 'locked' : '',
|
|
34833
|
+
lockTitle: _dmenuEffectiveSymmetricLocked() ? 'symmetric: zero centred' : 'symmetric: unlocked' },
|
|
34776
34834
|
];
|
|
34777
34835
|
}
|
|
34778
34836
|
|
|
@@ -34799,11 +34857,13 @@
|
|
|
34799
34857
|
btn.setAttribute('aria-checked', locked ? 'true' : 'false');
|
|
34800
34858
|
if (idx === _dmenuSelectedIdx) btn.classList.add('is-selected');
|
|
34801
34859
|
if (locked) btn.classList.add('is-locked');
|
|
34802
|
-
const lockLabel = row.
|
|
34803
|
-
?
|
|
34804
|
-
: row.
|
|
34805
|
-
? `${row.label}: locked at 0`
|
|
34806
|
-
:
|
|
34860
|
+
const lockLabel = row.lockTitle
|
|
34861
|
+
? row.lockTitle
|
|
34862
|
+
: row.qmriPolicyLocked
|
|
34863
|
+
? `${row.label}: locked at 0 by qMRI role`
|
|
34864
|
+
: row.zeroFloorLocked
|
|
34865
|
+
? `${row.label}: locked at 0`
|
|
34866
|
+
: (locked ? `${row.label}: locked` : `${row.label}: unlocked`);
|
|
34807
34867
|
btn.title = lockLabel;
|
|
34808
34868
|
btn.setAttribute('aria-label', lockLabel);
|
|
34809
34869
|
btn.innerHTML = locked
|
|
@@ -34825,7 +34885,11 @@
|
|
|
34825
34885
|
});
|
|
34826
34886
|
return btn;
|
|
34827
34887
|
};
|
|
34828
|
-
return [
|
|
34888
|
+
return [
|
|
34889
|
+
makeLock(rows[0], 0, _dmenuEffectiveVminLocked()),
|
|
34890
|
+
makeLock(rows[1], 1, vmaxLocked),
|
|
34891
|
+
makeLock(rows[2], 2, _dmenuEffectiveSymmetricLocked()),
|
|
34892
|
+
];
|
|
34829
34893
|
}
|
|
34830
34894
|
|
|
34831
34895
|
function _dmenuSetBoundManual(action, value) {
|
|
@@ -34865,6 +34929,14 @@
|
|
|
34865
34929
|
if (compareMvActive) compareMvViews.forEach(v => { v.lockedVmax = manualVmax; });
|
|
34866
34930
|
if (compareQmriActive) compareQmriViews.forEach(v => { v.lockedVmax = manualVmax; });
|
|
34867
34931
|
}
|
|
34932
|
+
// Under a symmetric lock the bounds move together, so mirror the
|
|
34933
|
+
// write through the same per-mode fan-out above rather than
|
|
34934
|
+
// duplicating it. The guard stops the mirror from recursing.
|
|
34935
|
+
if (!_dmenuSymmetricMirroring && _dmenuEffectiveSymmetricLocked()) {
|
|
34936
|
+
_dmenuSymmetricMirroring = true;
|
|
34937
|
+
try { _dmenuSetBoundManual(action === 'vmin' ? 'vmax' : 'vmin', -value); }
|
|
34938
|
+
finally { _dmenuSymmetricMirroring = false; }
|
|
34939
|
+
}
|
|
34868
34940
|
}
|
|
34869
34941
|
|
|
34870
34942
|
function _dmenuClearPresetForManualRange(target) {
|
|
@@ -35351,6 +35423,26 @@
|
|
|
35351
35423
|
if (_dmenuApplyBoundPreset(row.action, row.index)) _dmenuCloseAfterSelection();
|
|
35352
35424
|
return;
|
|
35353
35425
|
}
|
|
35426
|
+
if (row.action === 'symmetric') {
|
|
35427
|
+
symmetricLocked = !symmetricLocked;
|
|
35428
|
+
if (symmetricLocked) {
|
|
35429
|
+
// Snap the current window symmetric straight away so the
|
|
35430
|
+
// toggle has a visible effect rather than only shaping
|
|
35431
|
+
// later adjustments. The widest bound sets the half-width
|
|
35432
|
+
// so nothing currently visible gets clipped away.
|
|
35433
|
+
const half = Math.max(
|
|
35434
|
+
Math.abs(Number(manualVmin ?? currentVmin)),
|
|
35435
|
+
Math.abs(Number(manualVmax ?? currentVmax)),
|
|
35436
|
+
);
|
|
35437
|
+
if (Number.isFinite(half) && half > 0) _dmenuSetBoundManual('vmax', half);
|
|
35438
|
+
}
|
|
35439
|
+
_dmenuRefreshAfterRangeChange();
|
|
35440
|
+
_dmenuPickerRender();
|
|
35441
|
+
_restartHistAutoDismiss();
|
|
35442
|
+
saveState();
|
|
35443
|
+
if (closeAfterSelection) _dmenuCloseAfterSelection();
|
|
35444
|
+
return;
|
|
35445
|
+
}
|
|
35354
35446
|
if (row.action === 'vmin') {
|
|
35355
35447
|
if (row.zeroFloorLocked) {
|
|
35356
35448
|
_dmenuZeroFloorVminUnlocked = true;
|
|
@@ -275,6 +275,12 @@ def _open_browser(
|
|
|
275
275
|
"ArrayView could not safely install its VS Code opener in "
|
|
276
276
|
"this exact remote window; no viewer request was sent."
|
|
277
277
|
)
|
|
278
|
+
elif _extension_state._VSCODE_EXT_NO_LIVE_WINDOW:
|
|
279
|
+
detail = (
|
|
280
|
+
"This terminal was opened before its VS Code window reloaded, "
|
|
281
|
+
"so it still points at a window that no longer exists. Open a "
|
|
282
|
+
"new terminal in the target window and retry."
|
|
283
|
+
)
|
|
278
284
|
elif _extension_state._VSCODE_EXT_RELOAD_REQUIRED:
|
|
279
285
|
detail = (
|
|
280
286
|
"ArrayView's VS Code opener was installed; reload this exact "
|
|
@@ -54,7 +54,8 @@ _VSCODE_EXT_INSTALLED = False # cached so we only check once per process
|
|
|
54
54
|
_VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
55
55
|
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
56
56
|
_VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
|
|
57
|
-
|
|
57
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
|
|
58
|
+
_VSCODE_EXT_VERSION = "0.14.90" # current bundled extension version
|
|
58
59
|
_VSCODE_CONFIGURED_PORTS: set[int] = set()
|
|
59
60
|
|
|
60
61
|
def _bundled_vscode_vsix_version(vsix_path: str) -> str | None:
|
|
@@ -357,7 +358,12 @@ def _run_extension_installer(command: list[str], env: dict[str, str]) -> subproc
|
|
|
357
358
|
stderr=subprocess.PIPE,
|
|
358
359
|
text=True,
|
|
359
360
|
env=env,
|
|
361
|
+
# Same intent on both platforms: the installer gets its own group, so
|
|
362
|
+
# neither its shutdown nor a console Ctrl-C crosses between us and it.
|
|
360
363
|
start_new_session=os.name != "nt",
|
|
364
|
+
creationflags=(
|
|
365
|
+
subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
366
|
+
),
|
|
361
367
|
)
|
|
362
368
|
try:
|
|
363
369
|
stdout, stderr = process.communicate(timeout=30)
|
|
@@ -471,9 +477,11 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
471
477
|
"""
|
|
472
478
|
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL
|
|
473
479
|
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_INSTALL_FAILED
|
|
480
|
+
global _VSCODE_EXT_NO_LIVE_WINDOW
|
|
474
481
|
_VSCODE_EXT_FRESH_INSTALL = False
|
|
475
482
|
_VSCODE_EXT_RELOAD_REQUIRED = False
|
|
476
483
|
_VSCODE_EXT_INSTALL_FAILED = False
|
|
484
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False
|
|
477
485
|
|
|
478
486
|
vsix_path = str(_pkg_files("arrayview").joinpath("arrayview-opener.vsix"))
|
|
479
487
|
if not os.path.isfile(vsix_path):
|
|
@@ -496,6 +504,31 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
496
504
|
and active_registration.get("extensionVersion") == active_version
|
|
497
505
|
else None
|
|
498
506
|
)
|
|
507
|
+
def _reject_unmatched_host(installed_version: str) -> bool:
|
|
508
|
+
"""Record why the live host was rejected, so callers can advise correctly.
|
|
509
|
+
|
|
510
|
+
A version disagreement is fixed by reloading; an unidentifiable window is
|
|
511
|
+
not. Conflating them tells the user to reload when the reload is what
|
|
512
|
+
stranded their terminal in the first place.
|
|
513
|
+
"""
|
|
514
|
+
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_NO_LIVE_WINDOW
|
|
515
|
+
if active_version is None:
|
|
516
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = True
|
|
517
|
+
_vprint(
|
|
518
|
+
f"[ArrayView] opener v{installed_version} is installed, but no live "
|
|
519
|
+
"VS Code window claims this terminal; open a new terminal in the "
|
|
520
|
+
"target window",
|
|
521
|
+
flush=True,
|
|
522
|
+
)
|
|
523
|
+
return False
|
|
524
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
525
|
+
_vprint(
|
|
526
|
+
f"[ArrayView] opener v{installed_version} is installed, but this VS Code "
|
|
527
|
+
"window is still running an older extension host; reload this window",
|
|
528
|
+
flush=True,
|
|
529
|
+
)
|
|
530
|
+
return False
|
|
531
|
+
|
|
499
532
|
if _extension_on_disk(ext_version, vsix_path, remote=is_remote):
|
|
500
533
|
active_matches = (
|
|
501
534
|
active_version == ext_version
|
|
@@ -503,13 +536,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
503
536
|
else active_version in (None, ext_version)
|
|
504
537
|
)
|
|
505
538
|
if not active_matches:
|
|
506
|
-
|
|
507
|
-
_vprint(
|
|
508
|
-
f"[ArrayView] opener v{ext_version} is installed, but this VS Code "
|
|
509
|
-
"window is still running an older extension host; reload this window",
|
|
510
|
-
flush=True,
|
|
511
|
-
)
|
|
512
|
-
return False
|
|
539
|
+
return _reject_unmatched_host(ext_version)
|
|
513
540
|
if not is_remote:
|
|
514
541
|
_remove_old_extension_versions(ext_version, remote=False)
|
|
515
542
|
_VSCODE_EXT_INSTALLED = True
|
|
@@ -527,13 +554,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
527
554
|
else active_version in (None, newer_version)
|
|
528
555
|
)
|
|
529
556
|
if not active_matches:
|
|
530
|
-
|
|
531
|
-
_vprint(
|
|
532
|
-
f"[ArrayView] newer opener v{newer_version} is installed, but this "
|
|
533
|
-
"VS Code window is still running an older extension host; reload this window",
|
|
534
|
-
flush=True,
|
|
535
|
-
)
|
|
536
|
-
return False
|
|
557
|
+
return _reject_unmatched_host(newer_version)
|
|
537
558
|
_VSCODE_EXT_INSTALLED = True
|
|
538
559
|
_vprint(
|
|
539
560
|
f"[ArrayView] newer extension v{newer_version} is installed — keeping it",
|
|
Binary file
|
|
@@ -11,6 +11,29 @@ import uvicorn
|
|
|
11
11
|
from arrayview._app import app
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Isolation
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def isolated_runtime_directory(tmp_path_factory, monkeypatch):
|
|
20
|
+
"""Give every test its own instance registry.
|
|
21
|
+
|
|
22
|
+
An in-process ``view()`` registers the *running interpreter* as an ArrayView
|
|
23
|
+
instance. Sharing one registry therefore lets a later test that shells out
|
|
24
|
+
to ``arrayview stop`` discover the test runner, verify it as a legitimately
|
|
25
|
+
owned instance, and SIGTERM it — pytest dies mid-file with exit 143 and no
|
|
26
|
+
failing test to point at. Subprocesses inherit the variable, so the whole
|
|
27
|
+
tree stays inside the sandbox.
|
|
28
|
+
|
|
29
|
+
It also keeps a local test run from reaching the developer's real registry
|
|
30
|
+
and stopping viewers they have open.
|
|
31
|
+
"""
|
|
32
|
+
monkeypatch.setenv(
|
|
33
|
+
"ARRAYVIEW_RUNTIME_DIR", str(tmp_path_factory.mktemp("runtime"))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
14
37
|
# ---------------------------------------------------------------------------
|
|
15
38
|
# Server
|
|
16
39
|
# ---------------------------------------------------------------------------
|