Skip to content

Commit f94c191

Browse files
authored
Lazy napari checks for BatchRunner to improve import times (#5)
This pull request refactors how the code detects and handles the presence of the `napari` library for threaded batch operations. Instead of checking for `napari` at module import time, the code now tries to import `napari` only when needed, improving flexibility and testability. The test suite is also updated to simulate the absence of `napari` by monkeypatching the import mechanism. **Refactoring napari detection and threading logic:** * Removed the module-level `HAS_NAPARI` flag and deferred importing `napari` to runtime, allowing the code to try using napari threading if available and fall back to standard threading otherwise (`src/nbatch/_runner.py`) [[1]](diffhunk://#diff-0dd9aa30afa1d11d42cc399298b79ac401045ee0c5261caafff03f7a7ee41597L26-L34) [[2]](diffhunk://#diff-0dd9aa30afa1d11d42cc399298b79ac401045ee0c5261caafff03f7a7ee41597L238-R238). * Updated the `cancel` method to check for a `quit` method on the worker instead of relying on the `HAS_NAPARI` flag, making cancellation logic more robust and decoupled from napari-specific checks (`src/nbatch/_runner.py`). * Moved the import of `create_worker` into the `_run_napari_threaded` method so it only occurs when napari threading is actually used (`src/nbatch/_runner.py`). **Testing improvements:** * Updated the threaded fallback test to simulate napari's absence by monkeypatching Python's import mechanism, ensuring the fallback path is tested regardless of napari's actual installation (`tests/test_runner.py`) [[1]](diffhunk://#diff-55824c57bc66ac238570c2acc0d4904ab52df3131aae66c0ecefc2f08a4e8806L492-R502) [[2]](diffhunk://#diff-55824c57bc66ac238570c2acc0d4904ab52df3131aae66c0ecefc2f08a4e8806L509-R516).
1 parent d3a2ddb commit f94c191

File tree

2 files changed

+28
-24
lines changed

2 files changed

+28
-24
lines changed

src/nbatch/_runner.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,6 @@
2323
# Module-level logger
2424
_logger = logging.getLogger('nbatch.runner')
2525

26-
# Check for napari availability
27-
try:
28-
from napari.qt.threading import create_worker
29-
30-
HAS_NAPARI = True
31-
except ImportError:
32-
HAS_NAPARI = False
33-
create_worker = None
34-
3526

3627
class BatchRunner:
3728
"""Orchestrates batch operations with threading, progress, and cancellation.
@@ -163,10 +154,11 @@ def cancel(self) -> None:
163154
self._cancel_requested = True
164155
self._was_cancelled = True # Set immediately for threaded cases
165156
# Store local reference to avoid race condition with _handle_finished
166-
worker = self._worker if HAS_NAPARI else None
157+
# Check for quit method to determine if it's a napari worker
158+
worker = self._worker
167159

168-
# If using napari worker, request quit
169-
if worker is not None:
160+
# If using napari worker (has quit method), request quit
161+
if worker is not None and hasattr(worker, 'quit'):
170162
with contextlib.suppress(RuntimeError):
171163
worker.quit()
172164

@@ -235,14 +227,18 @@ def run(
235227
if self._on_start is not None:
236228
self._on_start(len(items_list))
237229

238-
if threaded and HAS_NAPARI:
239-
self._run_napari_threaded(
240-
func, items_list, args, kwargs, log_file, log_header
241-
)
242-
elif threaded:
243-
self._run_thread_fallback(
244-
func, items_list, args, kwargs, log_file, log_header
245-
)
230+
# Try napari threading first, fall back to standard threading
231+
if threaded:
232+
try:
233+
import napari.qt.threading # noqa: F401
234+
235+
self._run_napari_threaded(
236+
func, items_list, args, kwargs, log_file, log_header
237+
)
238+
except ImportError:
239+
self._run_thread_fallback(
240+
func, items_list, args, kwargs, log_file, log_header
241+
)
246242
else:
247243
self._run_sync(
248244
func, items_list, args, kwargs, log_file, log_header
@@ -285,6 +281,7 @@ def _run_napari_threaded(
285281
log_header: Mapping[str, object] | None,
286282
) -> None:
287283
"""Run batch using napari's create_worker for Qt-safe threading."""
284+
from napari.qt.threading import create_worker
288285

289286
def _worker_func():
290287
"""Generator function for napari worker."""

tests/test_runner.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,10 +489,17 @@ class TestBatchRunnerThreaded:
489489

490490
def test_run_threaded_fallback_no_napari(self, monkeypatch):
491491
"""Test threaded execution using fallback (concurrent.futures)."""
492-
# Force the fallback path by pretending napari isn't available
493-
import nbatch._runner as runner_module
492+
# Force the fallback path by making napari import fail
493+
import builtins
494494

495-
monkeypatch.setattr(runner_module, 'HAS_NAPARI', False)
495+
real_import = builtins.__import__
496+
497+
def mock_import(name, *args, **kwargs):
498+
if name == 'napari.qt.threading' or name.startswith('napari'):
499+
raise ImportError('napari not available')
500+
return real_import(name, *args, **kwargs)
501+
502+
monkeypatch.setattr(builtins, '__import__', mock_import)
496503

497504
results = []
498505
completed = []
@@ -506,7 +513,7 @@ def process(item):
506513
on_complete=lambda: completed.append(True),
507514
)
508515

509-
# Run threaded (will use fallback since we monkeypatched HAS_NAPARI)
516+
# Run threaded (will use fallback since napari import fails)
510517
runner.run(process, [1, 2, 3], threaded=True)
511518

512519
# Wait for completion

0 commit comments

Comments
 (0)