Progress Tracking

Pyplanner uses a global progress tracker to report what it is doing during PDF generation. The tracker is a singleton that can be configured once and then accessed from anywhere in the codebase.

Basic usage

The default tracker is silent (a QuietTracker). Call setup_tracker() to pick a visible one:

from pyplanner import setup_tracker, tracker

setup_tracker()  # auto-detect: tqdm on TTY, simple otherwise

with tracker("Generating PDF", total=3):
    with tracker().job("Step 1"):
        ...
    with tracker().job("Step 2"):
        ...
    tracker().job("Step 3")
    ...

tracker() returns the global singleton. When called with arguments it also sets the stage name and total job count, returning the tracker so you can use it as a context manager.

job() marks the start of a sub-step. It can be used as a context manager (with tracker().job("name"):), or as a plain call where the previous job is auto-finished when the next one starts or the stage exits.

Built-in trackers

QuietTracker

No output at all. The default.

SimpleProgressTracker

Prints the stage name on enter. Designed for non-TTY environments (pipes, CI logs).

TqdmTracker

Displays a tqdm progress bar. Chosen automatically when stdout is a TTY.

setup_tracker() picks the right one based on the environment:

setup_tracker(quiet=True)   # QuietTracker
setup_tracker()             # TqdmTracker on TTY, else Simple
setup_tracker(verbose=True) # same, but with per-job durations

Writing a custom tracker

The ProgressTracker protocol defines the interface. Your class does not need to inherit from it - it only needs to provide matching methods:

import logging
from contextlib import contextmanager

log = logging.getLogger(__name__)

class LoggingTracker:
    """Report progress via the logging module."""

    def __call__(self, stage_name, *, total=0):
        self._stage = stage_name
        return self

    def __enter__(self):
        log.info("Starting: %s", self._stage)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        log.info("Finished: %s", self._stage)

    @contextmanager
    def job(self, name):
        log.debug("  Job: %s", name)
        yield

Install it with setup_tracker():

from pyplanner import setup_tracker

setup_tracker(LoggingTracker())

All subsequent pyplanner operations will use your tracker.

Why a singleton?

An earlier design passed the tracker as a parameter through Planner.pdf(), optimize() and every helper function. The parameter-passing approach had several problems:

  • Every function signature gained a tracker parameter that most callers left as None.

  • Adding progress tracking to a new function required changing its signature and all call sites.

  • The tracker is a cross-cutting concern - it does not affect the return value or control flow.

The singleton is configured once at startup with setup_tracker() and then accessed from anywhere. This keeps internal APIs clean and makes it easy to add progress reporting to new functions without signature changes.

API reference

pyplanner.setup_tracker(instance: ProgressTracker | None = None, *, quiet: bool = False, verbose: bool = False) ProgressTracker

Install the global tracker.

If instance is given, use it directly (custom tracker). Otherwise create one based on the flags and environment:

  • quiet - QuietTracker (no output at all).

  • TTY - TqdmTracker.

  • Non-TTY (pipe/file) - SimpleProgressTracker.

Parameters:
  • instance – Custom tracker to install.

  • quiet – Suppress all output.

  • verbose – Print per-job durations after each stage.

pyplanner.tracker() ProgressTracker
pyplanner.tracker(stage_name: str, *, total: int = 0) ProgressTracker

Return the global tracker instance.

When called without arguments, returns the singleton directly. When called with arguments, forwards them to the tracker’s __call__ (which sets stage name / total) and returns the result. This allows:

with tracker("Generating PDF", total=5):
    ...

instead of the longer with tracker()("...", total=5):.

class pyplanner.ProgressTracker(*args, **kwargs)

Structural interface for all progress trackers.

Every tracker is used as a callable context manager:

setup_tracker(quiet=quiet, verbose=verbose)
with tracker("Building", total=n):
    with tracker().job("step-1"):
        ...
    tracker().job("step-2")
    ...

job() may be used as a plain call (the previous job is auto-finished when the next one starts or the stage exits) or as a context manager (the job finishes on block exit).

Implementations do not need to inherit from this protocol; they only need to provide matching methods.

job(name: str) AbstractContextManager[None]

Signal the start of a new job within the stage.

Returns a context manager. Can be used as a plain call (return value ignored) or with with.

class pyplanner.QuietTracker

No-op tracker for quiet mode.

Every method is a no-op so callers can use the same tracker interface without conditional checks.

class pyplanner.SimpleProgressTracker(*, verbose: bool = False)

Tracker for non-interactive output (pipes, files).

Prints the stage name once on enter. Job progress is silent. Used when stdout is not a TTY (e.g. piped to a file or another process).

class pyplanner.TqdmTracker(*, verbose: bool = False)

Progress tracker backed by tqdm.

Delegates all visual output to a tqdm progress bar.

job(name: str) _JobContext

Advance the bar by one step and update its label.

refresh() None

Refresh the tqdm bar (called by the background refresh thread under lock).