from __future__ import print_function
import sys
import math
import os
[docs]class ProgressBar(object):
""" Progress bar for telling the user where a task is up to
"""
MAX_LENGTH_IN_CHARS = 60
__slots__ = (
"_number_of_things", "_currently_completed", "_destination",
"_chars_per_thing", "_chars_done", "_string",
"_step_character", "_end_character", "_in_bad_terminal"
)
def __init__(self, total_number_of_things_to_do,
string_describing_what_being_progressed,
step_character="=", end_character="|"):
try:
self._number_of_things = int(total_number_of_things_to_do)
except TypeError:
# Might be dealing with general iterable; better not be infinite
self._number_of_things = len(list(total_number_of_things_to_do))
self._currently_completed = 0
self._chars_per_thing = None
self._chars_done = 0
self._string = string_describing_what_being_progressed
self._destination = sys.stderr
self._step_character = step_character
self._end_character = end_character
# Determine if we are in a "bad" terminal i.e. one that doesn't handle
# carriage return correctly
self._in_bad_terminal = "PROGRESS_GOOD_TERMINAL" not in os.environ
self._create_initial_progress_bar(
string_describing_what_being_progressed)
[docs] def update(self, amount_to_add=1):
""" Update the progress bar by a given amount
:param amount_to_add:
:rtype: None
"""
if self._currently_completed + amount_to_add > self._number_of_things:
raise Exception("too many update steps")
self._currently_completed += amount_to_add
self._check_differences()
def _print_overwritten_line(self, string):
print("\r" + string, end="", file=self._destination)
def _print_distance_indicator(self, description):
if description is not None:
print(description, file=self._destination)
# Find the mid point
mid_point = ProgressBar.MAX_LENGTH_IN_CHARS / 2
# The space between 0% and 50% is the mid-point minus the width of
# 0% and ~half the width of 50%
first_space = mid_point - 4
# The space between 50% and 100% is the mid-point minus the rest of
# the width of 50% and the width of 100%
second_space = mid_point - 5
# Print the progress bar itself
print(
"{}0%{}50%{}100%{}".format(
self._end_character, " " * first_space,
" " * second_space, self._end_character),
end="", file=self._destination)
if self._in_bad_terminal:
print("", file=self._destination)
print(" ", end="", file=self._destination)
def _print_progress(self, length):
chars_to_print = length
if not self._in_bad_terminal:
self._print_overwritten_line(self._end_character)
else:
chars_to_print = length - self._chars_done
for _ in range(int(chars_to_print)):
print(self._step_character, end='', file=self._destination)
self._destination.flush()
def _print_progress_done(self):
self._print_progress(ProgressBar.MAX_LENGTH_IN_CHARS)
if not self._in_bad_terminal:
print(self._end_character, file=self._destination)
else:
print("", file=self._destination)
def _create_initial_progress_bar(self, description):
if self._number_of_things == 0:
self._chars_per_thing = ProgressBar.MAX_LENGTH_IN_CHARS
else:
self._chars_per_thing = (ProgressBar.MAX_LENGTH_IN_CHARS /
float(self._number_of_things))
self._print_distance_indicator(description)
self._print_progress(0)
self._check_differences()
def _check_differences(self):
expected_chars_done = math.floor(
self._currently_completed * self._chars_per_thing)
if self._currently_completed == self._number_of_things:
expected_chars_done = ProgressBar.MAX_LENGTH_IN_CHARS
self._print_progress(expected_chars_done)
self._chars_done = expected_chars_done
[docs] def end(self):
""" Close the progress bar, updating whatever is left if needed
:rtype: None
"""
difference = self._number_of_things - self._currently_completed
self._currently_completed += difference
self._check_differences()
self._print_progress_done()
def __repr__(self):
return "progress bar for {}".format(self._string)
def __enter__(self):
""" Support method to use the with ProgressBar(...) as
This method does not have any parameters because any parameters in the
with ProgressBar(...) call have been passed to the __init__
Like the __new__ this method has to return self as in theory it
could pass back a different object. Welocome to Python
:return: The Progress bar
"""
return self
def __exit__(self, exty, exval, traceback):
self.end()
[docs] def over(self, collection):
""" Simple wrapper for the cases where the progress bar is being used\
to show progress through the iteration over a single collection.\
The progress bar should have been initialised to the size of the\
collection being iterated over.
:param collection:\
The base collection (any iterable) being iterated over
:return: An iterable. Expected to be directly used in a for.
"""
try:
for item in collection:
yield item
self.update()
finally:
self.end()
if __name__ == "__main__":
from time import sleep
demo = ProgressBar(
5, "Progress Bar Demonstration", step_character="-", end_character="!")
for _ in range(5):
sleep(1)
demo.update()
demo.end()
demo = ProgressBar(30, "Progress Bar Demonstration")
for _ in range(30):
sleep(0.1)
demo.update()
demo.end()
collection = [2, 3, 5, 7, 11, 13, 17]
demo = ProgressBar(collection, "Demo over a few primes")
for prime in demo.over(collection):
sleep(0.1)