VibecoderMcSwaggins commited on
Commit
4b42170
·
unverified ·
1 Parent(s): 9d33523

fix(docker): use subprocess bridge for Python version isolation (#18)

Browse files

PROBLEM: DeepISLES requires Python 3.8 (conda env), but Gradio 6 needs
Python 3.10+. They cannot coexist in the same Python process.

SOLUTION: Shell out architecture
- Gradio app runs in system Python (3.10+)
- DeepISLES runs via subprocess in conda env (3.8)
- New adapter script bridges the two environments

Changes:
- Dockerfile: Install deps to system Python, not conda env
- direct.py: Rewritten to use subprocess + conda run
- scripts/deepisles_adapter.py: New adapter for subprocess invocation
- tests: Updated error message expectations

This fixes the BUILD_ERROR on HF Spaces caused by trying to install
Gradio 6 into Python 3.8.

Dockerfile CHANGED
@@ -31,9 +31,10 @@ WORKDIR /home/user/demo
31
  # Copy requirements first for better layer caching
32
  COPY --chown=1000:1000 requirements.txt /home/user/demo/requirements.txt
33
 
34
- # Install Python dependencies INTO THE CONDA ENVIRONMENT
35
- # DeepISLES uses 'isles_ensemble' conda env - we must install there
36
- RUN /bin/bash -c "source activate isles_ensemble && pip install --no-cache-dir -r requirements.txt"
 
37
 
38
  # Copy application source code and package files
39
  COPY --chown=1000:1000 pyproject.toml /home/user/demo/pyproject.toml
@@ -41,9 +42,13 @@ COPY --chown=1000:1000 README.md /home/user/demo/README.md
41
  COPY --chown=1000:1000 src/ /home/user/demo/src/
42
  COPY --chown=1000:1000 app.py /home/user/demo/app.py
43
 
44
- # Install the package itself INTO THE CONDA ENVIRONMENT
 
 
 
 
45
  # Using --no-deps since requirements.txt already installed dependencies
46
- RUN /bin/bash -c "source activate isles_ensemble && pip install --no-cache-dir --no-deps -e ."
47
 
48
  # Set environment variable to indicate we're running in HF Spaces
49
  # This allows the app to detect runtime environment and use direct invocation
@@ -67,10 +72,9 @@ USER user
67
  # Expose the Gradio port
68
  EXPOSE 7860
69
 
70
- # IMPORTANT: DeepISLES uses a conda environment called 'isles_ensemble'
71
- # All PyTorch/nnU-Net dependencies are ONLY available in that conda env
72
- # We must run our app inside that environment to access DeepISLES imports
73
  ENTRYPOINT []
74
 
75
- # Run our Gradio app inside the isles_ensemble conda environment
76
- CMD ["/bin/bash", "-c", "source activate isles_ensemble && python -m stroke_deepisles_demo.ui.app"]
 
 
31
  # Copy requirements first for better layer caching
32
  COPY --chown=1000:1000 requirements.txt /home/user/demo/requirements.txt
33
 
34
+ # Install Python dependencies into SYSTEM Python (NOT conda env)
35
+ # DeepISLES conda env is Python 3.8, but Gradio 6 needs Python 3.10+
36
+ # We'll shell out to conda env for inference only
37
+ RUN pip install --no-cache-dir -r requirements.txt
38
 
39
  # Copy application source code and package files
40
  COPY --chown=1000:1000 pyproject.toml /home/user/demo/pyproject.toml
 
42
  COPY --chown=1000:1000 src/ /home/user/demo/src/
43
  COPY --chown=1000:1000 app.py /home/user/demo/app.py
44
 
45
+ # Copy adapter script for subprocess invocation of DeepISLES
46
+ # This script runs in the conda env (Py3.8) and is called via subprocess
47
+ COPY --chown=1000:1000 scripts/deepisles_adapter.py /app/deepisles_adapter.py
48
+
49
+ # Install the package itself into SYSTEM Python
50
  # Using --no-deps since requirements.txt already installed dependencies
51
+ RUN pip install --no-cache-dir --no-deps -e .
52
 
53
  # Set environment variable to indicate we're running in HF Spaces
54
  # This allows the app to detect runtime environment and use direct invocation
 
72
  # Expose the Gradio port
73
  EXPOSE 7860
74
 
75
+ # Reset ENTRYPOINT from base image
 
 
76
  ENTRYPOINT []
77
 
78
+ # Run Gradio app with SYSTEM Python (Py3.10+)
79
+ # DeepISLES inference is invoked via subprocess into conda env
80
+ CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
scripts/deepisles_adapter.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """DeepISLES adapter script for subprocess invocation.
3
+
4
+ This script runs inside the isles_ensemble conda environment (Python 3.8)
5
+ and is called by our Gradio app (Python 3.10+) via subprocess.
6
+
7
+ Usage:
8
+ conda run -n isles_ensemble python scripts/deepisles_adapter.py \
9
+ --dwi /path/to/dwi.nii.gz \
10
+ --adc /path/to/adc.nii.gz \
11
+ --output /path/to/output \
12
+ [--flair /path/to/flair.nii.gz] \
13
+ [--fast]
14
+
15
+ Note: This script intentionally uses Python 3.8 compatible syntax and
16
+ os.path functions (not pathlib) for compatibility with DeepISLES environment.
17
+ """
18
+
19
+ import argparse
20
+ import os
21
+ import sys
22
+
23
+ # Add DeepISLES to path
24
+ sys.path.insert(0, "/app")
25
+
26
+ from src.isles22_ensemble import IslesEnsemble
27
+
28
+
29
+ def main() -> None:
30
+ """Run DeepISLES inference with command-line arguments."""
31
+ parser = argparse.ArgumentParser(description="DeepISLES inference adapter")
32
+ parser.add_argument("--dwi", required=True, help="Path to DWI NIfTI file")
33
+ parser.add_argument("--adc", required=True, help="Path to ADC NIfTI file")
34
+ parser.add_argument("--output", required=True, help="Output directory")
35
+ parser.add_argument("--flair", default=None, help="Path to FLAIR NIfTI file")
36
+ parser.add_argument("--fast", action="store_true", help="Fast mode (SEALS only)")
37
+ parser.add_argument("--ensemble-path", default="/app", help="Path to DeepISLES repo")
38
+
39
+ args = parser.parse_args()
40
+
41
+ # Validate inputs exist (using os.path for Py3.8 compatibility)
42
+ if not os.path.exists(args.dwi): # noqa: PTH110
43
+ print(f"ERROR: DWI file not found: {args.dwi}", file=sys.stderr)
44
+ sys.exit(1)
45
+ if not os.path.exists(args.adc): # noqa: PTH110
46
+ print(f"ERROR: ADC file not found: {args.adc}", file=sys.stderr)
47
+ sys.exit(1)
48
+ if args.flair and not os.path.exists(args.flair): # noqa: PTH110
49
+ print(f"ERROR: FLAIR file not found: {args.flair}", file=sys.stderr)
50
+ sys.exit(1)
51
+
52
+ # Create output directory (using os.makedirs for Py3.8 compatibility)
53
+ os.makedirs(args.output, exist_ok=True) # noqa: PTH103
54
+
55
+ # Run inference
56
+ print("Running DeepISLES inference...")
57
+ print(f" DWI: {args.dwi}")
58
+ print(f" ADC: {args.adc}")
59
+ print(f" FLAIR: {args.flair}")
60
+ print(f" Output: {args.output}")
61
+ print(f" Fast mode: {args.fast}")
62
+
63
+ stroke_segm = IslesEnsemble()
64
+ stroke_segm.predict_ensemble(
65
+ ensemble_path=args.ensemble_path,
66
+ input_dwi_path=args.dwi,
67
+ input_adc_path=args.adc,
68
+ input_flair_path=args.flair,
69
+ output_path=args.output,
70
+ fast=args.fast,
71
+ skull_strip=False,
72
+ save_team_outputs=False,
73
+ results_mni=False,
74
+ parallelize=True,
75
+ )
76
+
77
+ print(f"DeepISLES inference complete. Output: {args.output}")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
src/stroke_deepisles_demo/inference/direct.py CHANGED
@@ -1,57 +1,37 @@
1
- """Direct DeepISLES invocation without Docker.
2
 
3
- This module provides direct Python invocation of DeepISLES when running
4
- inside the DeepISLES Docker image (e.g., on HF Spaces). This avoids
5
- Docker-in-Docker which is not supported on HF Spaces.
 
 
6
 
7
  Usage:
8
- When running in HF Spaces, our container is based on isleschallenge/deepisles,
9
- which has all DeepISLES dependencies pre-installed. This module imports
10
- and calls DeepISLES directly.
11
 
12
  See:
13
  - https://github.com/ezequieldlrosa/DeepIsles
14
  - docs/specs/07-hf-spaces-deployment.md
 
15
  """
16
 
17
  from __future__ import annotations
18
 
19
- import os
20
- import sys
21
  import time
22
  from dataclasses import dataclass
23
- from pathlib import Path
24
 
25
  from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
26
  from stroke_deepisles_demo.core.logging import get_logger
27
- from stroke_deepisles_demo.inference.deepisles import find_prediction_mask
28
 
29
  logger = get_logger(__name__)
30
 
31
-
32
- def _get_deepisles_search_paths() -> list[str]:
33
- """Get paths to search for DeepISLES modules.
34
-
35
- Checks DEEPISLES_PATH environment variable first, then falls back to
36
- common installation locations.
37
- """
38
- paths = []
39
-
40
- # Check environment variable first (set in Dockerfile)
41
- env_path = os.environ.get("DEEPISLES_PATH")
42
- if env_path:
43
- paths.append(env_path)
44
-
45
- # Add common installation locations (excluding any already added via env var)
46
- fallback_paths = [
47
- "/app", # Default location in isleschallenge/deepisles Docker image
48
- "/DeepIsles",
49
- "/opt/deepisles",
50
- "/home/user/DeepIsles",
51
- ]
52
- paths.extend(p for p in fallback_paths if p not in paths)
53
-
54
- return paths
55
 
56
 
57
  @dataclass(frozen=True)
@@ -62,53 +42,6 @@ class DirectInvocationResult:
62
  elapsed_seconds: float
63
 
64
 
65
- def _ensure_deepisles_importable() -> str:
66
- """
67
- Ensure DeepISLES modules are importable by adding to sys.path.
68
-
69
- Returns:
70
- Path where DeepISLES was found
71
-
72
- Raises:
73
- DeepISLESError: If DeepISLES cannot be found
74
- """
75
- search_paths = _get_deepisles_search_paths()
76
-
77
- for path in search_paths:
78
- path_obj = Path(path)
79
- if path_obj.exists():
80
- # Log what we find for debugging
81
- logger.info("Checking path %s - exists: True", path)
82
- if (path_obj / "src").exists():
83
- src_contents = list((path_obj / "src").iterdir())[:10]
84
- logger.info(" /src contents: %s", [f.name for f in src_contents])
85
- else:
86
- logger.info(" /src does NOT exist")
87
- # Check what IS in this directory
88
- contents = list(path_obj.iterdir())[:10]
89
- logger.info(" directory contents: %s", [f.name for f in contents])
90
-
91
- if path not in sys.path:
92
- sys.path.insert(0, path)
93
- try:
94
- # Test import (only available in DeepISLES Docker image)
95
- from src.isles22_ensemble import IslesEnsemble # noqa: F401
96
-
97
- logger.info("Found DeepISLES at %s", path)
98
- return path
99
- except ImportError as e:
100
- logger.warning("Import failed at %s: %s", path, e)
101
- continue
102
- else:
103
- logger.info("Checking path %s - exists: False", path)
104
-
105
- raise DeepISLESError(
106
- "DeepISLES modules not found. Direct invocation requires running "
107
- "inside the DeepISLES Docker image. Searched paths: "
108
- f"{search_paths}"
109
- )
110
-
111
-
112
  def validate_input_files(
113
  dwi_path: Path,
114
  adc_path: Path,
@@ -133,6 +66,39 @@ def validate_input_files(
133
  raise MissingInputError(f"FLAIR file not found: {flair_path}")
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def run_deepisles_direct(
137
  dwi_path: Path,
138
  adc_path: Path,
@@ -140,17 +106,18 @@ def run_deepisles_direct(
140
  *,
141
  flair_path: Path | None = None,
142
  fast: bool = True,
143
- skull_strip: bool = False,
144
- parallelize: bool = True,
145
- save_team_outputs: bool = False,
146
- results_mni: bool = False,
 
147
  ) -> DirectInvocationResult:
148
  """
149
- Run DeepISLES segmentation via direct Python invocation.
150
 
151
- This function calls the DeepISLES IslesEnsemble.predict_ensemble() method
152
- directly, bypassing Docker. It's used when running inside the DeepISLES
153
- container on HF Spaces.
154
 
155
  Args:
156
  dwi_path: Path to DWI NIfTI file (b=1000)
@@ -158,10 +125,11 @@ def run_deepisles_direct(
158
  output_dir: Directory for output files
159
  flair_path: Optional path to FLAIR NIfTI file
160
  fast: If True, use SEALS model only (faster, no FLAIR needed)
161
- skull_strip: If True, perform skull stripping
162
- parallelize: If True, run models in parallel
163
- save_team_outputs: If True, save individual team outputs
164
- results_mni: If True, output results in MNI space
 
165
 
166
  Returns:
167
  DirectInvocationResult with path to prediction mask
@@ -184,52 +152,76 @@ def run_deepisles_direct(
184
  # Validate inputs
185
  validate_input_files(dwi_path, adc_path, flair_path)
186
 
187
- # Ensure DeepISLES is importable
188
- deepisles_path = _ensure_deepisles_importable()
189
-
190
- # Import DeepISLES (only available in DeepISLES Docker image)
191
- try:
192
- from src.isles22_ensemble import IslesEnsemble
193
- except ImportError as e:
194
- raise DeepISLESError(f"Failed to import DeepISLES: {e}") from e
195
-
196
  # Create output directory
197
  output_dir.mkdir(parents=True, exist_ok=True)
198
 
199
  logger.info(
200
- "Running DeepISLES direct invocation: dwi=%s, adc=%s, flair=%s, fast=%s",
201
  dwi_path,
202
  adc_path,
203
  flair_path,
204
  fast,
205
  )
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  try:
208
- # Initialize the ensemble
209
- stroke_segm = IslesEnsemble()
210
-
211
- # Run prediction
212
- stroke_segm.predict_ensemble(
213
- ensemble_path=deepisles_path,
214
- input_dwi_path=str(dwi_path),
215
- input_adc_path=str(adc_path),
216
- input_flair_path=str(flair_path) if flair_path else None,
217
- output_path=str(output_dir),
218
- skull_strip=skull_strip,
219
- fast=fast,
220
- save_team_outputs=save_team_outputs,
221
- results_mni=results_mni,
222
- parallelize=parallelize,
223
  )
224
- except Exception as e:
225
- logger.exception("DeepISLES inference failed")
226
- raise DeepISLESError(f"DeepISLES inference failed: {e}") from e
227
 
228
- # Find the prediction mask (using shared function from deepisles module)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  prediction_path = find_prediction_mask(output_dir)
230
 
231
  elapsed = time.time() - start_time
232
- logger.info("DeepISLES direct invocation completed in %.2fs", elapsed)
233
 
234
  return DirectInvocationResult(
235
  prediction_path=prediction_path,
 
1
+ """Direct DeepISLES invocation via subprocess.
2
 
3
+ This module provides subprocess-based invocation of DeepISLES when running
4
+ on HF Spaces. We use subprocess because:
5
+ - DeepISLES runs in a conda env with Python 3.8
6
+ - Our Gradio app requires Python 3.10+ for modern dependencies
7
+ - The two environments are incompatible, so we bridge via subprocess
8
 
9
  Usage:
10
+ The subprocess calls /app/deepisles_adapter.py inside the isles_ensemble
11
+ conda environment, which imports and runs IslesEnsemble.
 
12
 
13
  See:
14
  - https://github.com/ezequieldlrosa/DeepIsles
15
  - docs/specs/07-hf-spaces-deployment.md
16
+ - scripts/deepisles_adapter.py
17
  """
18
 
19
  from __future__ import annotations
20
 
21
+ import subprocess
 
22
  import time
23
  from dataclasses import dataclass
24
+ from pathlib import Path # noqa: TC003 - used at runtime in dataclass and functions
25
 
26
  from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
27
  from stroke_deepisles_demo.core.logging import get_logger
 
28
 
29
  logger = get_logger(__name__)
30
 
31
+ # Path to conda and adapter script in the Docker container
32
+ CONDA_PATH = "/opt/conda/bin/conda"
33
+ ADAPTER_SCRIPT = "/app/deepisles_adapter.py"
34
+ CONDA_ENV_NAME = "isles_ensemble"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
 
37
  @dataclass(frozen=True)
 
42
  elapsed_seconds: float
43
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def validate_input_files(
46
  dwi_path: Path,
47
  adc_path: Path,
 
66
  raise MissingInputError(f"FLAIR file not found: {flair_path}")
67
 
68
 
69
+ def find_prediction_mask(output_dir: Path) -> Path:
70
+ """
71
+ Find the prediction mask in DeepISLES output directory.
72
+
73
+ DeepISLES outputs the lesion mask as 'lesion_msk.nii.gz'.
74
+
75
+ Args:
76
+ output_dir: DeepISLES output directory
77
+
78
+ Returns:
79
+ Path to the prediction mask NIfTI file
80
+
81
+ Raises:
82
+ DeepISLESError: If no prediction mask found
83
+ """
84
+ # DeepISLES outputs lesion_msk.nii.gz
85
+ expected_path = output_dir / "lesion_msk.nii.gz"
86
+ if expected_path.exists():
87
+ return expected_path
88
+
89
+ # Fall back to searching for any NIfTI file
90
+ nifti_files = list(output_dir.glob("*.nii.gz"))
91
+ nifti_files = [
92
+ f for f in nifti_files if not any(x in f.name.lower() for x in ["dwi", "adc", "flair"])
93
+ ]
94
+ if nifti_files:
95
+ return nifti_files[0]
96
+
97
+ raise DeepISLESError(
98
+ f"No prediction mask found in {output_dir}. Expected 'lesion_msk.nii.gz' or similar."
99
+ )
100
+
101
+
102
  def run_deepisles_direct(
103
  dwi_path: Path,
104
  adc_path: Path,
 
106
  *,
107
  flair_path: Path | None = None,
108
  fast: bool = True,
109
+ skull_strip: bool = False, # noqa: ARG001 - kept for API compatibility
110
+ parallelize: bool = True, # noqa: ARG001 - kept for API compatibility
111
+ save_team_outputs: bool = False, # noqa: ARG001 - kept for API compatibility
112
+ results_mni: bool = False, # noqa: ARG001 - kept for API compatibility
113
+ timeout: float = 1800, # 30 minutes
114
  ) -> DirectInvocationResult:
115
  """
116
+ Run DeepISLES segmentation via subprocess into conda environment.
117
 
118
+ This function calls the deepisles_adapter.py script inside the
119
+ isles_ensemble conda environment via subprocess. This bridges the
120
+ Python version gap (Gradio needs 3.10+, DeepISLES needs 3.8).
121
 
122
  Args:
123
  dwi_path: Path to DWI NIfTI file (b=1000)
 
125
  output_dir: Directory for output files
126
  flair_path: Optional path to FLAIR NIfTI file
127
  fast: If True, use SEALS model only (faster, no FLAIR needed)
128
+ skull_strip: If True, perform skull stripping (not passed to subprocess)
129
+ parallelize: If True, run models in parallel (not passed to subprocess)
130
+ save_team_outputs: If True, save individual team outputs (not passed)
131
+ results_mni: If True, output results in MNI space (not passed)
132
+ timeout: Maximum seconds to wait for inference
133
 
134
  Returns:
135
  DirectInvocationResult with path to prediction mask
 
152
  # Validate inputs
153
  validate_input_files(dwi_path, adc_path, flair_path)
154
 
 
 
 
 
 
 
 
 
 
155
  # Create output directory
156
  output_dir.mkdir(parents=True, exist_ok=True)
157
 
158
  logger.info(
159
+ "Running DeepISLES via subprocess: dwi=%s, adc=%s, flair=%s, fast=%s",
160
  dwi_path,
161
  adc_path,
162
  flair_path,
163
  fast,
164
  )
165
 
166
+ # Build command to run adapter script in conda environment
167
+ # Using: conda run -n isles_ensemble python /app/deepisles_adapter.py ...
168
+ cmd = [
169
+ CONDA_PATH,
170
+ "run",
171
+ "-n",
172
+ CONDA_ENV_NAME,
173
+ "python",
174
+ ADAPTER_SCRIPT,
175
+ "--dwi",
176
+ str(dwi_path.resolve()),
177
+ "--adc",
178
+ str(adc_path.resolve()),
179
+ "--output",
180
+ str(output_dir.resolve()),
181
+ ]
182
+
183
+ if flair_path is not None:
184
+ cmd.extend(["--flair", str(flair_path.resolve())])
185
+
186
+ if fast:
187
+ cmd.append("--fast")
188
+
189
+ logger.info("Subprocess command: %s", " ".join(cmd))
190
+
191
  try:
192
+ result = subprocess.run(
193
+ cmd,
194
+ capture_output=True,
195
+ text=True,
196
+ timeout=timeout,
197
+ cwd="/app", # Run from DeepISLES directory
 
 
 
 
 
 
 
 
 
198
  )
 
 
 
199
 
200
+ # Log output
201
+ if result.stdout:
202
+ logger.info("DeepISLES stdout:\n%s", result.stdout)
203
+ if result.stderr:
204
+ logger.warning("DeepISLES stderr:\n%s", result.stderr)
205
+
206
+ # Check for failure
207
+ if result.returncode != 0:
208
+ raise DeepISLESError(
209
+ f"DeepISLES inference failed with exit code {result.returncode}. "
210
+ f"stderr: {result.stderr}"
211
+ )
212
+
213
+ except subprocess.TimeoutExpired as e:
214
+ raise DeepISLESError(f"DeepISLES inference timed out after {timeout} seconds") from e
215
+ except FileNotFoundError as e:
216
+ raise DeepISLESError(
217
+ f"Failed to run DeepISLES subprocess: {e}. Is conda available at /opt/conda/bin/conda?"
218
+ ) from e
219
+
220
+ # Find the prediction mask
221
  prediction_path = find_prediction_mask(output_dir)
222
 
223
  elapsed = time.time() - start_time
224
+ logger.info("DeepISLES subprocess completed in %.2fs", elapsed)
225
 
226
  return DirectInvocationResult(
227
  prediction_path=prediction_path,
tests/inference/test_direct.py CHANGED
@@ -143,9 +143,7 @@ class TestRunDeepISLESDirect:
143
  with pytest.raises(MissingInputError):
144
  run_deepisles_direct(dwi, adc, output)
145
 
146
- def test_deepisles_not_available_raises(
147
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
148
- ) -> None:
149
  """Raises DeepISLESError when DeepISLES not available."""
150
  from stroke_deepisles_demo.inference.direct import run_deepisles_direct
151
 
@@ -156,8 +154,7 @@ class TestRunDeepISLESDirect:
156
  dwi.touch()
157
  adc.touch()
158
 
159
- # Ensure DeepISLES is not importable
160
- monkeypatch.delenv("DEEPISLES_DIRECT_INVOCATION", raising=False)
161
-
162
- with pytest.raises(DeepISLESError, match="DeepISLES modules not found"):
163
  run_deepisles_direct(dwi, adc, output)
 
143
  with pytest.raises(MissingInputError):
144
  run_deepisles_direct(dwi, adc, output)
145
 
146
+ def test_deepisles_not_available_raises(self, tmp_path: Path) -> None:
 
 
147
  """Raises DeepISLESError when DeepISLES not available."""
148
  from stroke_deepisles_demo.inference.direct import run_deepisles_direct
149
 
 
154
  dwi.touch()
155
  adc.touch()
156
 
157
+ # Subprocess will fail because conda/adapter not available locally
158
+ # The error message depends on what's missing (conda, /app dir, etc.)
159
+ with pytest.raises(DeepISLESError, match=r"(subprocess|conda|No such file)"):
 
160
  run_deepisles_direct(dwi, adc, output)