Using click.progressbar with multiprocessing in Python Using click.progressbar with multiprocessing in Python numpy numpy

Using click.progressbar with multiprocessing in Python


accepted answer says it's impossible with click and it'd require 'non trivial amount of code to make it work'.

While it's true, there is another module with this functionality out of the box: tqdmhttps://github.com/tqdm/tqdm which does exatly what you need.

You can do nested progress bars in docs https://github.com/tqdm/tqdm#nested-progress-bars etc.


I see two issues in your code.

The first one explains why your progress bars are often showing 100% rather than their real progress. You're calling bar.update(i) which advances the bar's progress by i steps, when I think you want to be updating by one step. A better approach would be to pass the iterable to the progressbar function and let it do the updating automatically:

with click.progressbar(atoms, label='erasing close atoms') as bar:    for atom in bar:        erased = False        coord = np.array(atom[6])        # ...

However, this still won't work with multiple processes iterating at once, each with its own progress bar due to the second issue with your code. The click.progressbar documentation states the following limitation:

No printing must happen or the progress bar will be unintentionally destroyed.

This means that whenever one of your progress bars updates itself, it will break all of the other active progress bars.

I don't think there is an easy fix for this. It's very hard to interactively update a multiple-line console output (you basically need to be using curses or a similar "console GUI" library with support from your OS). The click module does not have that capability, it can only update the current line. Your best hope would probably be to extend the click.progressbar design to output multiple bars in columns, like:

CPU1: [######      ] 52%   CPU2: [###        ] 30%    CPU3: [########  ] 84%

This would require a non-trivial amount of code to make it work (especially when the updates are coming from multiple processes), but it's not completely impractical.


For anybody coming to this later. I created this which seems to work okay. It overrides click.ProgressBar fairly minimally, although I had to override an entire method for only a few lines of code at the bottom of the method. This is using \x1b[1A\x1b[2K to clear the progress bars before rewriting them so may be environment dependent.

#!/usr/bin/env pythonimport timefrom typing import Dictimport clickfrom click._termui_impl import ProgressBar as ClickProgressBar, BEFORE_BARfrom click._compat import term_lenclass ProgressBar(ClickProgressBar):    def render_progress(self, in_collection=False):        # This is basically a copy of the default render_progress with the addition of in_collection        # param which is only used at the very bottom to determine how to echo the bar        from click.termui import get_terminal_size        if self.is_hidden:            return        buf = []        # Update width in case the terminal has been resized        if self.autowidth:            old_width = self.width            self.width = 0            clutter_length = term_len(self.format_progress_line())            new_width = max(0, get_terminal_size()[0] - clutter_length)            if new_width < old_width:                buf.append(BEFORE_BAR)                buf.append(" " * self.max_width)                self.max_width = new_width            self.width = new_width        clear_width = self.width        if self.max_width is not None:            clear_width = self.max_width        buf.append(BEFORE_BAR)        line = self.format_progress_line()        line_len = term_len(line)        if self.max_width is None or self.max_width < line_len:            self.max_width = line_len        buf.append(line)        buf.append(" " * (clear_width - line_len))        line = "".join(buf)        # Render the line only if it changed.        if line != self._last_line and not self.is_fast():            self._last_line = line            click.echo(line, file=self.file, color=self.color, nl=in_collection)            self.file.flush()        elif in_collection:            click.echo(self._last_line, file=self.file, color=self.color, nl=in_collection)            self.file.flush()class ProgressBarCollection(object):    def __init__(self, bars: Dict[str, ProgressBar], bar_template=None, width=None):        self.bars = bars        if bar_template or width:            for bar in self.bars.values():                if bar_template:                    bar.bar_template = bar_template                if width:                    bar.width = width    def __enter__(self):        self.render_progress()        return self    def __exit__(self, exc_type, exc_val, exc_tb):        self.render_finish()    def render_progress(self, clear=False):        if clear:            self._clear_bars()        for bar in self.bars.values():            bar.render_progress(in_collection=True)    def render_finish(self):        for bar in self.bars.values():            bar.render_finish()    def update(self, bar_name: str, n_steps: int):        self.bars[bar_name].make_step(n_steps)        self.render_progress(clear=True)    def _clear_bars(self):        for _ in range(0, len(self.bars)):            click.echo('\x1b[1A\x1b[2K', nl=False)def progressbar_collection(bars: Dict[str, ProgressBar]):    return ProgressBarCollection(bars, bar_template="%(label)s  [%(bar)s]  %(info)s", width=36)@click.command()def cli():    with click.progressbar(length=10, label='bar 0') as bar:        for i in range(0, 10):            time.sleep(1)            bar.update(1)    click.echo('------')    with ProgressBar(iterable=None, length=10, label='bar 1', bar_template="%(label)s  [%(bar)s]  %(info)s") as bar:        for i in range(0, 10):            time.sleep(1)            bar.update(1)    click.echo('------')    bar2 = ProgressBar(iterable=None, length=10, label='bar 2')    bar3 = ProgressBar(iterable=None, length=10, label='bar 3')    with progressbar_collection({'bar2': bar2, 'bar3': bar3}) as bar_collection:        for i in range(0, 10):            time.sleep(1)            bar_collection.update('bar2', 1)        for i in range(0, 10):            time.sleep(1)            bar_collection.update('bar3', 1)if __name__ == "__main__":    cli()