user3808269
user3808269

Reputation: 1351

Progress Bar Among Other Print Statements Not Working - Useful Progress Bar Not Found

Intro (Riding in on a Interstellar Office Chair)

I have a program that uses a progress bar to show the end-user if the program is working or not. This should tell the end-user how long they have to wait and if the program is still working. This question came after reading this interesting stack overflow thread: Text progress bar in terminal with block characters

Problems/Challenges

The first problem is the progress bar only works in a loop currently and just prints out the numbers in the range. This is not helpful. Why would anyone want a program with a progress bar that just shows the progress of a single loop but not what the rest of the program is doing.

The second problem is that when I have other print statements with the progress bar there are suddenly multiple progress bars printed to the command prompt instead of a single progress bar that appears to animate with each progress bar update.

How can I have the progress bar always at the bottom of the terminal with the other print statements above it?

Another challenge is I am using the Windows 7 operating-system (OS).

Code

Please check out the following example code I have for you:

import sys
import time
import threading

def progress_bar(progress):
    sys.stdout.write('\r[{0}] {1}'.format('#' * int(progress/10 * 10), progress))

def doThis():
    for i in range(10):
        print("Doing this.")

def doThat():
    for i in range(3):
        print("Doing that.")

def wrapUp():
    total = 2+ 2
    print("Total found")
    return total
         
if __name__ == "__main__":
  print("Starting in main...")
  progress_bar(1)
  print("\nOther print statement here.")
  print("Nice day, expensive day.")
  progress_bar(3)

  doThis()
  progress_bar(4)
  doThat()
  progress_bar(5)
  doThis()
  doThat()
  progress_bar(6)
  progress_bar(7)
  doThat()
  doThat()
  progress_bar(8)
  wrapUp()
  progress_bar(9)
  progress_bar(10)

What the Program Prints

   Starting in main...
[#] 1
Other print statement here.
Nice day, expensive day.
[###] 3Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
[####] 4Doing that.
Doing that.
Doing that.
[#####] 5Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing that.
Doing that.
Doing that.
[#######] 7Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
[########] 8Total found
[##########] 10

Upvotes: 0

Views: 1385

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121484

You have to do three things:

  • store progress bar state in a separate object you can call methods on to update the bar state in different loops.
  • when printing the progress bar, ensure that no newline is printed
  • when printing console output, first clear the current line, then re-draw the bar after printing.

You cover the first two, somewhat, but not the third. It's probably best to encapsulate control of the console in a class that manages the progress bar too, so it can handle clearing, printing and re-displaying all in a single location:

import builtins
import math
import sys
import threading

class ProgressConsole:
    def __init__(self, size, width=80, output=sys.stdout):
        self.size = size
        self.width = width
        self.current = 0
        self.output = output
        # [...] and space take 3 characters, plus max width of size (log10 + 1)
        self._bar_size = width - 4 - int(math.log10(size))
        self._bar_step = self._bar_size / self.size
        self._lock = threading.Lock()

    def print(self, *message):
        with self._lock:
            self._clear()
            builtins.print(*message, file=self.output)
            self._display()

    def increment(self, step=1):
        with self._lock:
            self.current = min(self.current + step, self.size)
            self._display()

    def _clear(self):
        self.output.write('\r')
        self.output.write(' ' * self.width)
        self.output.write('\r')

    def _display(self):
        bar = '#' * int(round(self._bar_step * self.current))
        blank = ' ' * (self._bar_size - len(bar))
        self.output.write(f"\r[{bar}{blank}] {self.current}")

I included a thread lock as your sample code imports threading, so I'm assuming you want to be able to use this in such an environment.

The above class uses a fixed width for the progress bar, and it's wiped by writing out a series of spaces before returning back to the left-most column with \r.

I also made the bar a fixed width so it fills from left to right, rather than grow across the screen.

Then ensure you 'print' to this object:

if __name__ == "__main__":
    progress_bar = ProgressConsole(10)
    print = progress_bar.print  # replace built-in with our own version

    print("Starting in main...")
    progress_bar.increment()
    print("\nOther print statement here.")
    print("Nice day, expensive day.")
    progress_bar.increment(2)

    doThis()
    progress_bar.increment()
    doThat()
    progress_bar.increment()
    doThis()
    doThat()
    progress_bar.increment(2)
    doThat()
    doThat()
    progress_bar.increment()
    wrapUp()
    progress_bar.increment(2)

The final output for the above is then:

Starting in main...

Other print statement here.
Nice day, expensive day.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing that.
Doing that.
Doing that.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing this.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Doing that.
Total found
[###########################################################################] 10

With a few random sleeps inserted, it looks like this when running:

progressbar running

Upvotes: 3

Related Questions