0

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
wjandrea
  • 28,235
  • 9
  • 60
  • 81
user3808269
  • 1,321
  • 3
  • 21
  • 40

1 Answers1

3

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

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Wow! :) Thank-you sir for the incredible reply. I am going to study this and then study your response some more and then probably check this as the answer. Thank-you @Martijin. Thank-you for providing the video of the program executing. That is awesome man. – user3808269 Apr 10 '19 at 21:26
  • What does the thread.locking() method do? I noticed this is called in two of the methods by way of the `self._lock` statement. I do not necessarily have to have a multi-threaded environment. This is interesting code. Appreciate it man. :) – user3808269 Apr 10 '19 at 21:37
  • Ah, found a pretty clear explanation at the following URL: https://www.bogotobogo.com/python/Multithread/python_multithreading_Using_Locks_with_statement_Context_Manager.php – user3808269 Apr 10 '19 at 22:36
  • Is it necessary to use the log10 call from the Math module? That seems over complicated. I am not this advanced of a Pythonista. :/ Thank-you though for writing this cool program man. – user3808269 Apr 11 '19 at 00:30
  • @user3870315 it’s a method to calculate the width of a number, using basic math. E.g the floor of Log10 of 100 is 2, and of 999, but 1000 is 3. Add 1 and you have the number of didgits. – Martijn Pieters Apr 11 '19 at 06:30
  • @user3870315 there are other ways, but log10 is the mathematical method and takes near constant time (`len(str(....))` takes linear time as you generate the string then count digits one by one). – Martijn Pieters Apr 11 '19 at 06:33