Skip to content

feat: parallel execution verbs with reliable progress bars - #327

Merged
wolph merged 19 commits into
developfrom
feature/parallel-execution
Aug 14, 2026
Merged

feat: parallel execution verbs with reliable progress bars#327
wolph merged 19 commits into
developfrom
feature/parallel-execution

Conversation

@wolph

@wolph wolph commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Summary

One verb family runs a function -- or a shell command -- over a batch of items on threads, processes, or asyncio, with a progress bar that keeps animating even while long tasks are in flight:

results = progressbar.map(fetch, urls, workers=8)            # threads
results = progressbar.map(crunch, files, pool='process')     # processes
results = await progressbar.amap(fetch, urls, concurrency=8) # asyncio (sync fns auto-wrapped)
procs   = progressbar.run('gzip -k {}', files, workers=4)    # progress-bar'd xargs -P
progressbar.map(crunch, files, workers=4, bar='multi')       # overall + per-task sub-bars

Full surface: map / imap / imap_unordered / starmap / thread_map / process_map / as_completed / amap / aimap / aimap_unordered / gather / run / @parallel / Pool / AsyncPool / current_task_bar.

Design

  • Two thin engines over one display contract. A concurrent.futures engine and an asyncio engine share a Display protocol (plain aggregate bar, MultiBar-backed multi with auto-managed per-task bars, False, or a caller-supplied instance).
  • Stdlib-faithful semantics. imap is ordered/lazy/results-only like multiprocessing.Pool.imap; gather keeps asyncio's exact contract including return_exceptions; as_completed is a superset of the stdlib function; executor construction kwargs (initializer, initargs, mp_context, max_tasks_per_child, thread_name_prefix) pass through.
  • Keep-alive guarantee. Completion events flow through a done-queue (O(1) per event); poll timeouts tick the bar so ETA/timers/spinners animate during minutes-long tasks. One poll_interval knob drives both the coordinator and the bar.
  • Bounded memory. Inputs are consumed lazily with windowed submission (buffersize, default 4x workers) -- million-item and generator inputs run flat. Process pools auto-chunk (~16 chunks/worker, capped) so tiny items aren't pathological.
  • Specified failure contracts. Fail-fast default with on_error='return' opt-out; overall timeout that cancels without waiting for stragglers; Ctrl-C cancels pending work, leaves the bar's final state on its own line, and re-raises. Documented honestly: running tasks cannot be killed.
  • Typo-proof kwargs. Unknown bar keywords raise TypeError instead of being swallowed by the mixin chain.
  • The implementation package is private (progressbar/_parallel/) because progressbar.parallel is the public decorator; namespace-only names (map, gather, ...) stay out of __all__ so star-imports never shadow builtins, guarded by extended test_init_exports.

Testing

  • 181 new tests; full suite 1088 passed / 14 skipped (version gates).
  • 100% branch coverage on progressbar/_parallel/; ruff and pyright clean.
  • Keep-alive, error/timeout/interrupt contracts, generator cleanup on early break, process-pool pickling (incl. decorated functions), duplicate-label multi-bar keys, and brace-safe run() templates (awk '{print $1}') are all pinned by tests.
  • Docs: new howto (docs/howto/parallel-execution.rst), reference page, README section; sphinx -W build clean.

Design spec and adversarial-review history (internal + tqdm/alive-progress/mpire comparison) in docs/superpowers/specs/2026-08-14-parallel-execution-design.md (untracked by convention).

Phase 2 (seams reserved, no API breakage): process-worker sub-bars via an initializer-installed queue reporter; ExceptionGroup collect mode once the floor is 3.11.

Copilot AI lite review requested due to automatic review settings August 14, 2026 08:07

def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
"""The original, undecorated call."""
...
self, *iterables: typing.Any, **kwargs: typing.Any
) -> list[typing.Any]:
"""Parallel ordered map over the iterables; see the module verb."""
...
self, *iterables: typing.Any, **kwargs: typing.Any
) -> typing.Generator[typing.Any, None, None]:
"""Lazy ordered results; see the module verb."""
...
self, *iterables: typing.Any, **kwargs: typing.Any
) -> typing.Generator[tuple[typing.Any, typing.Any], None, None]:
"""Completion-order pairs; see the module verb."""
...
self, iterable: typing.Any, **kwargs: typing.Any
) -> list[typing.Any]:
"""Parallel map over pre-tupled arguments; see the module verb."""
...
if self._external is not None:
return self._external
if self._executor is None:
self._executor, _owned, _workers = resolve_executor(
if self._external is not None:
return self._external
if self._executor is None:
self._executor, _owned, _workers = resolve_executor(
) -> list[typing.Any]:
"""`run` a shell command per item on this pool's executor."""
# Deferred import: _shell imports this module.
from . import _shell

import pytest

import progressbar

def test_bound_method_rejected(self) -> None:
class _Thing:
def method(self) -> None: ...

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new parallel-execution feature set to progressbar, providing sync (concurrent.futures) and asyncio engines that drive progress bars reliably (including keep-alive ticking), plus a shell run() helper, reusable Pool/AsyncPool, and a @parallel decorator. This integrates into the public API via lazy exports while keeping star-import-unsafe names out of __all__, and is backed by extensive tests and new documentation.

Changes:

  • Introduces sync + asyncio parallel engines under progressbar/_parallel/ with a shared display contract (plain/multi/False/instance).
  • Adds public-facing helpers (Pool, AsyncPool, parallel, current_task_bar, plus namespace-only verbs like map, amap, run, etc.) and updates API/export snapshot.
  • Adds comprehensive tests and new docs/howto + reference material for parallel execution.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_parallel_shell.py Tests for brace-safe shell templating and run() behavior.
tests/test_parallel_process.py Process/interpreter pool behavior, chunking, and executor passthrough tests.
tests/test_parallel_pool.py Tests for reusable sync Pool lifecycle, defaults, and verbs.
tests/test_parallel_map.py Tests for ordered sync map execution and bar behavior.
tests/test_parallel_imap.py Tests for lazy imap and completion-order imap_unordered, including early-break cleanup.
tests/test_parallel_errors.py Tests for fail-fast, on_error, timeout, and interrupt contracts in sync engine.
tests/test_parallel_display.py Tests for display backends (plain, multi, False, instance) and keep-alive ticks.
tests/test_parallel_decorator.py Tests for @parallel decorator binding, async/sync verbs, and picklability.
tests/test_parallel_common.py Tests for shared helpers (totals, chunking, bar-kwargs validation, task-bar context).
tests/test_parallel_async.py Tests for asyncio engine (amap/aimap/gather), keep-alive, cancellation, and call strategy.
tests/test_parallel_aliases.py Tests for starmap, tqdm-style aliases, and progress-aware as_completed.
tests/test_init_exports.py Extends export-list consistency checks to include namespace-only exports.
tests/api_surface_snapshot.json Updates snapshot to include new public API surface additions.
README.rst Adds a “Parallel execution” section with examples and link to docs.
progressbar/_parallel/_sync.py Implements sync parallel coordination, executor resolution, verbs, and reusable Pool.
progressbar/_parallel/_shell.py Implements brace-safe command templating and run() helper on threads.
progressbar/_parallel/_display.py Implements display protocol + backends (PlainDisplay, MultiDisplay, NullDisplay).
progressbar/_parallel/_decorator.py Implements @parallel decorator that attaches batch verbs to functions.
progressbar/_parallel/_common.py Shared utilities (chunking, totals, defaults, bar kwarg validation, task-bar contextvar).
progressbar/_parallel/_async.py Implements asyncio coordination engine plus AsyncPool, amap/aimap/gather.
progressbar/_parallel/init.py Aggregates parallel implementation exports for lazy re-exporting.
progressbar/init.py Adds lazy exports + namespace-only mapping for parallel verbs and types.
docs/reference/parallel.rst Adds API reference page for parallel execution surface and shared keywords.
docs/reference/index.rst Links new parallel reference page into docs index.
docs/howto/parallel-execution.rst Adds how-to guide covering usage patterns, errors/timeouts, pools, decorator, and run().
docs/howto/index.rst Links new parallel how-to page into docs index.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

_sync,
)

#: A tiny portable command: exit with the given code.
Comment on lines +36 to +42
def _pool_kind(pool: typing.Any) -> str:
"""Map a `pool=` argument to 'thread'/'process'/'interpreter'."""
if isinstance(pool, str):
return pool
if isinstance(pool, concurrent.futures.ProcessPoolExecutor):
return 'process'
return 'thread'

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18f7bcf218

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +221 to +222
for task in self.in_flight:
task.cancel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve gather siblings after an exception

When progressbar.gather(..., return_exceptions=False) encounters one failed awaitable, execute_async enters its finally block and this loop cancels every remaining child. Native asyncio.gather propagates the first exception while allowing the other submitted awaitables to continue, so the advertised drop-in replacement can unexpectedly abort side-effecting sibling operations; the gather path needs different exception cleanup from amap.

Useful? React with 👍 / 👎.

Comment on lines +316 to +317
self._check_deadline()
future = self._next_done()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck the deadline after waiting for completion

When a task finishes after timeout but before the current poll_interval wait expires, the deadline was checked only before blocking, so the completed future is accepted; if it was the final future, the loop exits successfully without another check. For example, a 50 ms task with timeout=10 ms and poll_interval=100 ms returns normally instead of timing out. The async loop has the same ordering around _next_done(), so both paths should bound the wait by the remaining deadline or recheck immediately afterward.

Useful? React with 👍 / 👎.

Comment on lines +99 to +101
@functools.wraps(verb, assigned=('__doc__',), updated=())
def bound(*iterables: typing.Any, **kwargs: typing.Any) -> typing.Any:
return verb(fn, *iterables, **{**config, **kwargs})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Filter decorator defaults by execution backend

A decorator configured as documented with @parallel(workers=4, pool='process') forwards pool to .amap() as well; execute_async then treats it as a bar keyword and raises TypeError instead of running. Conversely, an async-only default such as concurrency breaks the attached sync verbs. Since every decorated function exposes both verb families, backend-specific defaults need to be filtered or translated before dispatch.

Useful? React with 👍 / 👎.

Comment on lines +138 to +140
else:
while len(self.in_flight) < self.window and self._launch_one():
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-positive execution windows

With a nonempty input and concurrency=0 or a negative value, this condition launches no tasks, self.in_flight remains empty, and amap silently returns [] as though the batch succeeded. The synchronous loop behaves identically for buffersize <= 0; these limits should be validated as positive rather than dropping every input item.

Useful? React with 👍 / 👎.

Comment thread tests/test_perf_budget.py Fixed
@wolph
wolph merged commit 885dd78 into develop Aug 14, 2026
15 of 16 checks passed
@wolph
wolph deleted the feature/parallel-execution branch August 14, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants