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
QuietTrackerNo output at all. The default.
SimpleProgressTrackerPrints the stage name on enter. Designed for non-TTY environments (pipes, CI logs).
TqdmTrackerDisplays 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
trackerparameter that most callers left asNone.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).