# -*- coding: utf-8 -*-
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
Text-mode progress bars
"""
from __future__ import division, print_function, unicode_literals
from six.moves import range
import collections
import locale
import math
import os
import struct
import sys
__all__ = ('ProgressBar', 'ProgressBarTheme')
# From http://stackoverflow.com/questions/566746
def getTerminalSize():
"""
returns (lines:int, cols:int)
"""
def ioctl_GWINSZ(fd):
# These two imports are only present on POSIX systems, so they must be
# guarded by a try block.
import fcntl
import termios
return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
# try stdin, stdout, stderr
for fd in (0, 1, 2):
try:
return ioctl_GWINSZ(fd)
except:
pass
# try os.ctermid()
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
try:
return ioctl_GWINSZ(fd)
finally:
os.close(fd)
except:
pass
# try environment variables
try:
return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS"))
except:
pass
# i give up. return default.
return (25, 80)
_ProgressBarTheme = collections.namedtuple(
'_ProgressBarTheme', 'sequence twiddle_sequence left_border right_border')
[docs]class ProgressBarTheme(_ProgressBarTheme):
[docs] def is_compatible_with_encoding(self, coding):
if not coding:
coding = locale.getpreferredencoding()
try:
for string in self:
string.encode(coding)
except UnicodeEncodeError:
return False
else:
return True
[docs] def is_compatible_with_stream(self, stream):
return self.is_compatible_with_encoding(stream.encoding)
default_unicode_theme = ProgressBarTheme(
' ▏▎▍▌▋▊▉█', ' ▏▎▍▌▋▊▉██▉▊▋▌▍▎▏ ', '▐', '▌')
default_ascii_theme = ProgressBarTheme(
' .:!|', ' ..', ' |', u'| ')
[docs]class ProgressBar:
"""Display a text progress bar.
A final line feed is printed when the ProgressBar is garbage collected.
Explicitly deleting the object can force a line feed when desired. As
an alternative, using the ProgressBar as a context manager will ensure
a final line feed is printed when the code block within which the
ProgressBar is being used exits.
Example:
>>> with ProgressBar(max=3) as pb:
... pb.update(1)
... pb.update(2)
... pb.update(3)
...
"""
def __init__(
self, text='Working', max=1, value=0, textwidth=24, fid=None,
theme=None):
if fid is None:
self.fid = sys.stderr
if hasattr(self.fid, 'fileno'):
self.isatty = os.isatty(self.fid.fileno())
else:
self.isatty = False
if theme is None:
if self.isatty and default_unicode_theme.is_compatible_with_stream(
self.fid) and 'xterm' in os.environ.get('TERM', ''):
theme = default_unicode_theme
else:
theme = default_ascii_theme
self.text = text
self.max = max
self.value = value
self.textwidth = textwidth
self.sequence = ('',) + tuple(theme.sequence)
self.twiddle_sequence = tuple(
theme.twiddle_sequence[-i:] + theme.twiddle_sequence[:-i]
for i in range(len(theme.twiddle_sequence)))
self.left_border = theme.left_border
self.right_border = theme.right_border
self.twiddle = 0
self.linefed = False
[docs] def iterate(self, iterable, format="%s", print_every=1):
"""Use as a target of a for-loop to issue a progress update for every
iteration. For example:
progress = ProgressBar()
for text in progress.iterate(["foo", "bar", "bat"]):
...
"""
# If iterable has a definite length, then set the maximum value of the
# progress bar. Else, set the maximum value to -1 so that the progress
# bar displays indeterminate progress (scrolling dots).
try:
length = len(iterable)
except TypeError:
self.max = -1
else:
self.max = length
# Iterate over the input, updating the progress bar for each element.
for i, item in enumerate(iterable):
yield item
if i % print_every == 0:
self.update(i + 1, format % item)
[docs] def show(self):
"""Redraw the text progress bar."""
# Be silent if writing to a tty
if not self.isatty:
return
if len(self.text) > self.textwidth:
label = self.text[:self.textwidth]
else:
label = self.text.rjust(self.textwidth)
terminalSize = getTerminalSize()
if terminalSize is None:
terminalSize = 80
else:
terminalSize = terminalSize[1]
barWidth = terminalSize - self.textwidth - len(self.left_border) \
- len(self.right_border) - 7
if self.value is None or self.value < 0:
pattern = self.twiddle_sequence[
self.twiddle % len(self.twiddle_sequence)]
self.twiddle += 1
barSymbols = (pattern * int(math.ceil(barWidth / len(self.twiddle_sequence))))[0:barWidth]
progressFractionText = ' '
else:
progressFraction = max(0.0, min(1.0, float(self.value) / self.max))
fMinor, iMajor = math.modf(progressFraction * barWidth)
iMajor = int(iMajor)
iMinor = int(math.ceil(fMinor * (len(self.sequence) - 1)))
iMajorMinor = int(math.ceil(progressFraction * barWidth))
barSymbols = (
(self.sequence[-1] * iMajor)
+ self.sequence[iMinor]
+ (self.sequence[1] * (barWidth - iMajorMinor)))
progressFractionText = ('%.1f%%' % (100 * progressFraction)).rjust(6)
print(
'\r\x1B[1m', label, '\x1B[0m', self.left_border, '\x1B[36m',
barSymbols, '\x1B[0m', self.right_border, progressFractionText,
sep='', end='', file=self.fid)
self.fid.flush()
self.linefed = False
[docs] def update(self, value=None, text=None):
"""Redraw the progress bar, optionally changing the value and text
and return the (possibly new) value. For I/O performance, the
progress bar might not be written to the terminal if the text does
not change and the value changes by too little. Use .show() to
force a redraw."""
redraw = False
if text is not None:
redraw = text != self.text
self.text = text
if value is not None:
redraw |= self.max == 0 or round(value / (0.0003 * self.max)) != \
round(self.value / (0.0003 * self.max))
self.value = value
if redraw:
self.show()
return self.value
[docs] def increment(self, delta=1, text=None):
"""Redraw the progress bar, incrementing the value by delta
(default=1) and optionally changing the text. Returns the
ProgressBar's new value. See also .update()."""
return self.update(value=min(self.max, self.value + delta), text=text)
[docs] def linefeed(self):
# Be silent if writing to a tty
if not self.isatty:
return
if not self.linefed:
print(file=self.fid)
self.fid.flush()
self.linefed = True
def __enter__(self):
self.show()
return self
def __exit__(self, exc_type, exc_value, tb):
try:
self.linefeed()
except:
pass
def __del__(self):
self.linefeed()
def demo():
"""Demonstrate progress bar."""
from time import sleep
maxProgress = 1000
with ProgressBar(max=maxProgress) as progressbar:
for i in range(-100, maxProgress):
sleep(0.01)
progressbar.update(i + 1)
progressbar2 = ProgressBar(max=maxProgress)
for s in progressbar2.iterate(list(range(maxProgress))):
sleep(0.01)
for s in progressbar2.iterate(
list(range(maxProgress)), format='iteration %d'):
sleep(0.01)
if __name__ == '__main__':
demo()