Skip to content

Commit 3f27058

Browse files
authored
Add BatchRunner for orchestrated batch operations (#2)
This pull request introduces the new `BatchRunner` class to the `nbatch` package, providing a higher-level API for orchestrating batch operations with support for threading, progress callbacks, and cancellation. The documentation and package metadata have been updated to reflect this addition, and new optional dependencies and usage examples are included for better integration with napari and Qt-based workflows. **Major new feature: BatchRunner integration** - Introduced the `BatchRunner` class for orchestrating batch operations, including threading, progress/cancellation callbacks, and seamless napari integration. The API is now documented in both `README.md` and `src/nbatch/__init__.py`, and the class is exported in the package's public interface. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R20-R24) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L184-R246) [[3]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R332-R365) [[4]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58R17-R19) [[5]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58L43-R57) [[6]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58R69) [[7]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58R82-R83) **Documentation and usage improvements** - Expanded the `README.md` with recommended usage patterns for `BatchRunner`, including widget integration examples, and clarified how to use napari's `@thread_worker` directly for advanced users. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L184-R246) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R332-R365) - Updated the top-level docstring in `src/nbatch/__init__.py` with new examples and a description of `BatchRunner`. [[1]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58R17-R19) [[2]](diffhunk://#diff-b47af7882e75362ef6c3f4f6ee9347b3b112d3e049324f680ad320f40c74ed58L43-R57) **Dependency and packaging enhancements** - Added optional dependencies for napari and Qt (`napari`, `pyqt6`) and grouped them under `[project.optional-dependencies]` in `pyproject.toml`, with an `all` group for convenience. Development dependencies now include these extras for full test coverage. - Added the `Framework :: napari` classifier to better indicate napari integration. **Minor code and typing cleanups** - Updated type checks in `_decorator.py` and `_discovery.py` to use the `list | tuple` syntax. [[1]](diffhunk://#diff-b8b54f8f8e15eb73a90baac496cccec0e224de523851c1953a65d884abd4679dL175-R177) [[2]](diffhunk://#diff-f7448bdb0f8849649110e4176d1907acb18430dd31ea6fb0b695031a24b28cf1L190-R190) - Clarified that the core package has no napari/Qt dependencies, only using them optionally.
2 parents c6d303c + 13d58e1 commit 3f27058

File tree

7 files changed

+1201
-6
lines changed

7 files changed

+1201
-6
lines changed

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ nbatch provides a foundation for batch processing operations. It's designed to w
1717

1818
- **`@batch` decorator** - Transform single-item functions into batch-capable functions
1919
- **`BatchContext`** - Track progress through batch operations
20+
- **`BatchRunner`** - Orchestrate batch operations with threading, progress callbacks, and cancellation
2021
- **`discover_files()`** - Flexible file discovery with natural sorting (like file explorers)
2122
- **`batch_logger`** - Scoped logging for batch operations with headers/footers
2223
- **Minimal dependencies** - Only requires natsort for natural file ordering
24+
- **Optional napari integration** - Uses napari's threading when available, falls back to standard threads
2325

2426
## Installation
2527

@@ -181,7 +183,67 @@ Batch processing completed at 2025-01-29 10:35:00
181183

182184
## Integration with napari
183185

184-
nbatch is designed to work seamlessly with napari's `@thread_worker`:
186+
### Using BatchRunner (Recommended)
187+
188+
`BatchRunner` provides clean orchestration for widgets with threading, progress callbacks, and cancellation:
189+
190+
```python
191+
from nbatch import batch, BatchRunner
192+
193+
# Define your processing function (pure, testable)
194+
@batch(on_error='continue')
195+
def process_image(path, model, output_dir):
196+
result = model.predict(load_image(path))
197+
save_result(result, output_dir / path.name)
198+
return result
199+
200+
# In your widget class
201+
class MyWidget:
202+
def __init__(self, viewer):
203+
self._viewer = viewer
204+
205+
# Create runner once - reusable for all batches
206+
self.runner = BatchRunner(
207+
on_item_complete=self._on_item_complete,
208+
on_complete=self._on_batch_complete,
209+
on_error=self._on_item_error,
210+
on_cancel=self._on_cancelled,
211+
)
212+
213+
self._run_button.clicked.connect(self.run_batch)
214+
self._cancel_button.clicked.connect(self.runner.cancel)
215+
216+
def _on_item_complete(self, result, ctx):
217+
"""Called after each item completes."""
218+
self._progress_bar.setValue(ctx.index + 1)
219+
# Optionally add result to viewer
220+
if result is not None:
221+
self._viewer.add_image(result, name=f"Result {ctx.index}")
222+
223+
def _on_batch_complete(self):
224+
self._progress_bar.label = "Complete!"
225+
226+
def _on_item_error(self, ctx, exception):
227+
self._progress_bar.label = f"Error on {ctx.item.name}"
228+
229+
def _on_cancelled(self):
230+
self._progress_bar.label = "Cancelled"
231+
232+
def run_batch(self):
233+
"""Triggered by 'Run' button - just one line!"""
234+
self._progress_bar.max = len(self.files)
235+
self.runner.run(
236+
process_image,
237+
self.files,
238+
model=self.model,
239+
output_dir=self.output_dir,
240+
log_file=self.output_dir / "batch.log",
241+
)
242+
```
243+
244+
### Using @thread_worker directly
245+
246+
For more control, use napari's `@thread_worker` with the `@batch` decorator:
185247

186248
```python
187249
from napari.qt.threading import thread_worker
@@ -267,6 +329,40 @@ def batch_logger(
267329
) -> Generator[BatchLogger, None, None]: ...
268330
```
269331

332+
### `BatchRunner`
333+
334+
```python
335+
class BatchRunner:
336+
def __init__(
337+
self,
338+
on_item_complete: Callable[[Any, BatchContext], None] | None = None,
339+
on_complete: Callable[[], None] | None = None,
340+
on_error: Callable[[BatchContext, Exception], None] | None = None,
341+
on_cancel: Callable[[], None] | None = None,
342+
): ...
343+
344+
def run(
345+
self,
346+
func: Callable,
347+
items: Any,
348+
*args,
349+
threaded: bool = True,
350+
log_file: str | Path | None = None,
351+
log_header: Mapping[str, object] | None = None,
352+
patterns: str | Sequence[str] = '*',
353+
recursive: bool = False,
354+
**kwargs,
355+
) -> None: ...
356+
357+
def cancel(self) -> None: ...
358+
359+
@property
360+
def is_running(self) -> bool: ...
361+
362+
@property
363+
def was_cancelled(self) -> bool: ...
364+
```
365+
270366
## Contributing
271367

272368
Contributions are welcome! Please ensure tests pass before submitting a pull request:

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ authors = [
1111
]
1212
classifiers = [
1313
"Development Status :: 2 - Pre-Alpha",
14+
"Framework :: napari",
1415
"Intended Audience :: Developers",
1516
"Operating System :: OS Independent",
1617
"Programming Language :: Python",
@@ -31,11 +32,25 @@ dependencies = [
3132
"natsort",
3233
]
3334

35+
[project.optional-dependencies]
36+
napari = [
37+
"napari",
38+
]
39+
qtpy-backend = [
40+
"pyqt6",
41+
]
42+
all = [
43+
"nbatch[napari]",
44+
"nbatch[qtpy-backed]",
45+
]
46+
3447
[dependency-groups]
3548
dev = [
3649
"tox-uv",
3750
"pytest", # https://docs.pytest.org/en/latest/contents.html
3851
"pytest-cov", # https://pytest-cov.readthedocs.io/en/latest/
52+
"pytest-qt", # Qt testing utilities (qtbot fixture)
53+
"napari[pyqt6]", # Include napari and Qt for full test coverage
3954
]
4055

4156
[project.entry-points."napari.manifest"]

src/nbatch/__init__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
This package provides a lightweight foundation for batch
44
processing operations. It's designed to work with napari plugins but has
5-
no napari or Qt dependencies itself.
5+
no napari or Qt dependencies in the core modules.
66
77
Core Components
88
---------------
@@ -14,6 +14,9 @@
1414
Discover files from paths, directories, or iterables.
1515
batch_logger : context manager
1616
Scoped logging for batch operations with optional file output.
17+
BatchRunner : class
18+
Orchestrates batch operations with threading, progress, and cancellation.
19+
Uses napari's threading when available, falls back to standard threads.
1720
1821
Examples
1922
--------
@@ -40,8 +43,18 @@
4043
4144
>>> from nbatch import batch_logger
4245
>>> with batch_logger(log_file="output/log.txt", header={"Files": 100}) as log:
43-
... for result, ctx in process(files, with_context=True):
46+
... for result, ctx in process(files):
4447
... log(ctx, f"Processed: {result}")
48+
49+
With BatchRunner (for widgets):
50+
51+
>>> from nbatch import BatchRunner
52+
>>> runner = BatchRunner(
53+
... on_item_complete=lambda result, ctx: progress_bar.setValue(ctx.index + 1),
54+
... on_complete=lambda: print("Done!"),
55+
... )
56+
>>> runner.run(process, files) # Threaded, non-blocking
57+
>>> runner.cancel() # Cancel if needed
4558
"""
4659

4760
try:
@@ -53,6 +66,7 @@
5366
from nbatch._decorator import batch
5467
from nbatch._discovery import discover_files, is_batch_input
5568
from nbatch._logging import BatchLogger, batch_logger
69+
from nbatch._runner import BatchRunner
5670

5771
__all__ = [
5872
# Core decorator
@@ -65,4 +79,6 @@
6579
# Logging
6680
'batch_logger',
6781
'BatchLogger',
82+
# Runner
83+
'BatchRunner',
6884
]

src/nbatch/_decorator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ def wrapper(
172172
items = discover_files(
173173
first_arg, patterns=patterns, recursive=recursive
174174
)
175-
elif isinstance(first_arg, (list, tuple)):
175+
elif isinstance(first_arg, list | tuple):
176176
# Could be paths or other items
177-
if first_arg and isinstance(first_arg[0], (str, Path)):
177+
if first_arg and isinstance(first_arg[0], str | Path):
178178
items = discover_files(first_arg)
179179
else:
180180
items = list(first_arg)

src/nbatch/_discovery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,6 @@ def is_batch_input(item: object) -> bool:
187187
>>> is_batch_input("single.tif")
188188
False
189189
"""
190-
if isinstance(item, (list, tuple)):
190+
if isinstance(item, list | tuple):
191191
return True
192192
return isinstance(item, Path) and item.is_dir()

0 commit comments

Comments
 (0)