From 3b47e7b013d23b9be50b7128da022722de295c35 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Mon, 1 Dec 2025 23:06:09 -0600 Subject: [PATCH 1/2] lazy napari checks for runner --- src/nbatch/_runner.py | 26 +++++++++++++++++--------- tests/test_runner.py | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/nbatch/_runner.py b/src/nbatch/_runner.py index 24df94a..93a3ba0 100644 --- a/src/nbatch/_runner.py +++ b/src/nbatch/_runner.py @@ -23,14 +23,21 @@ # Module-level logger _logger = logging.getLogger('nbatch.runner') -# Check for napari availability -try: - from napari.qt.threading import create_worker +# Lazy napari availability check - don't import at module level +_HAS_NAPARI: bool | None = None - HAS_NAPARI = True -except ImportError: - HAS_NAPARI = False - create_worker = None + +def _check_napari() -> bool: + """Lazily check for napari availability.""" + global _HAS_NAPARI + if _HAS_NAPARI is None: + try: + import napari.qt.threading # noqa: F401 + + _HAS_NAPARI = True + except ImportError: + _HAS_NAPARI = False + return _HAS_NAPARI class BatchRunner: @@ -163,7 +170,7 @@ def cancel(self) -> None: self._cancel_requested = True self._was_cancelled = True # Set immediately for threaded cases # Store local reference to avoid race condition with _handle_finished - worker = self._worker if HAS_NAPARI else None + worker = self._worker if _check_napari() else None # If using napari worker, request quit if worker is not None: @@ -235,7 +242,7 @@ def run( if self._on_start is not None: self._on_start(len(items_list)) - if threaded and HAS_NAPARI: + if threaded and _check_napari(): self._run_napari_threaded( func, items_list, args, kwargs, log_file, log_header ) @@ -285,6 +292,7 @@ def _run_napari_threaded( log_header: Mapping[str, object] | None, ) -> None: """Run batch using napari's create_worker for Qt-safe threading.""" + from napari.qt.threading import create_worker def _worker_func(): """Generator function for napari worker.""" diff --git a/tests/test_runner.py b/tests/test_runner.py index 3b01fc0..c796c88 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -492,7 +492,7 @@ def test_run_threaded_fallback_no_napari(self, monkeypatch): # Force the fallback path by pretending napari isn't available import nbatch._runner as runner_module - monkeypatch.setattr(runner_module, 'HAS_NAPARI', False) + monkeypatch.setattr(runner_module, '_HAS_NAPARI', False) results = [] completed = [] From c2554a781f1de337515fe88440d4bcd67871e606 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Mon, 1 Dec 2025 23:44:00 -0600 Subject: [PATCH 2/2] inline napari import --- src/nbatch/_runner.py | 43 ++++++++++++++++--------------------------- tests/test_runner.py | 15 +++++++++++---- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/nbatch/_runner.py b/src/nbatch/_runner.py index 93a3ba0..995d8b6 100644 --- a/src/nbatch/_runner.py +++ b/src/nbatch/_runner.py @@ -23,22 +23,6 @@ # Module-level logger _logger = logging.getLogger('nbatch.runner') -# Lazy napari availability check - don't import at module level -_HAS_NAPARI: bool | None = None - - -def _check_napari() -> bool: - """Lazily check for napari availability.""" - global _HAS_NAPARI - if _HAS_NAPARI is None: - try: - import napari.qt.threading # noqa: F401 - - _HAS_NAPARI = True - except ImportError: - _HAS_NAPARI = False - return _HAS_NAPARI - class BatchRunner: """Orchestrates batch operations with threading, progress, and cancellation. @@ -170,10 +154,11 @@ def cancel(self) -> None: self._cancel_requested = True self._was_cancelled = True # Set immediately for threaded cases # Store local reference to avoid race condition with _handle_finished - worker = self._worker if _check_napari() else None + # Check for quit method to determine if it's a napari worker + worker = self._worker - # If using napari worker, request quit - if worker is not None: + # If using napari worker (has quit method), request quit + if worker is not None and hasattr(worker, 'quit'): with contextlib.suppress(RuntimeError): worker.quit() @@ -242,14 +227,18 @@ def run( if self._on_start is not None: self._on_start(len(items_list)) - if threaded and _check_napari(): - self._run_napari_threaded( - func, items_list, args, kwargs, log_file, log_header - ) - elif threaded: - self._run_thread_fallback( - func, items_list, args, kwargs, log_file, log_header - ) + # Try napari threading first, fall back to standard threading + if threaded: + try: + import napari.qt.threading # noqa: F401 + + self._run_napari_threaded( + func, items_list, args, kwargs, log_file, log_header + ) + except ImportError: + self._run_thread_fallback( + func, items_list, args, kwargs, log_file, log_header + ) else: self._run_sync( func, items_list, args, kwargs, log_file, log_header diff --git a/tests/test_runner.py b/tests/test_runner.py index c796c88..017109b 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -489,10 +489,17 @@ class TestBatchRunnerThreaded: def test_run_threaded_fallback_no_napari(self, monkeypatch): """Test threaded execution using fallback (concurrent.futures).""" - # Force the fallback path by pretending napari isn't available - import nbatch._runner as runner_module + # Force the fallback path by making napari import fail + import builtins - monkeypatch.setattr(runner_module, '_HAS_NAPARI', False) + real_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == 'napari.qt.threading' or name.startswith('napari'): + raise ImportError('napari not available') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', mock_import) results = [] completed = [] @@ -506,7 +513,7 @@ def process(item): on_complete=lambda: completed.append(True), ) - # Run threaded (will use fallback since we monkeypatched HAS_NAPARI) + # Run threaded (will use fallback since napari import fails) runner.run(process, [1, 2, 3], threaded=True) # Wait for completion