initial working implementation of ProgressBar2

This commit is contained in:
Martinez 2016-02-21 13:15:29 +01:00
parent b691bb9b25
commit 3a0f77ab96

View file

@ -42,13 +42,21 @@ def lexical_join(words, oxford=False):
# --------------------------------------------------
class _ProgressBarChannel:
def __init__(self, unit, top, prefix_base2, precision):
def __init__(self, unit, top, precision, use_prefixes=True, prefix_base2=False, min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7):
self.unit = unit
self.top = top
self.dynamic_top = top is None
self.prefix_base2 = prefix_base2
self.precision = precision
self.use_prefixes = use_prefixes
self.min_width = {
"raw": min_width_raw,
"rate": min_width_rate,
"percent": min_width_percent,
"time": min_width_time
}
self.value = 0
self._value = 0
self.set()
if prefix_base2:
@ -56,11 +64,42 @@ class _ProgressBarChannel:
def set(self, value=None):
if value is not None:
self.value = value
self._value = value
self.ratio = self.value / self.top
s = Unit(self.value, self.unit, format="%.{:d}f pU".format(self.precision))
self.text = str(s).split(" ", 1)
self.ratio = self._value / self.top if self.top else None
def _render(self, value, unit=None, just=None):
if unit is None:
unit = self.unit
u = Unit(value, unit, format="%.{:d}f pU".format(self.precision))
if not self.use_prefixes or just == "percent":
u._prefixes = (('', 0),) # Unit needs fixin'
s = str(u)
if just:
s = s.rjust(self.min_width[just])
return s
@property
def value(self):
return self._render(self._value, just="raw")
@property
def inverse(self):
return self._render(self.top - self._value, just="raw")
def divide(self, amount):
"""
Divides the amount into amount * ratio, amount * (1 - ratio) and rounds to integers.
"""
A = round(amount * self.ratio)
return (A, amount - A)
class ProgressBar2:
"""
@ -68,7 +107,7 @@ class ProgressBar2:
Each atom consists of three characters: a literal §, an operator and a channel.
An operator is one of:
% - a percentage (value from 0 to 100, padded to three characters)
% - a percentage (value from 0 to 100, padded with spaces to three characters)
r - displays the raw value of the channel
z - displays the maximum (total) value
s - displays the rate of change in units/s
@ -80,7 +119,7 @@ class ProgressBar2:
at 100% it fills the entirety of the remaining space
A channel ID as a lowercase letter is the channel's actual value. An uppercase letter
is that channel's complement.
is that channel's complement. Operations z, s, m and h cannot have a complement.
Use §§ to display a literal §.
@ -93,7 +132,7 @@ class ProgressBar2:
Output: 53 % (21.0/39.6 kf) [---------------------> ] 27.1 f/s [233/439 Mio] (ETA 00:11:26)
"""
def __init__(self, format, channels, width=None, space_before_unit=True):
def __init__(self, format, channels, width=None):
"""
Initialize the ProgressBar.
@ -130,8 +169,7 @@ class ProgressBar2:
self.format = format
self.channels = {id: _ProgressBarChannel(**conf) for id, conf in channels.items()}
self.space_before_unit = space_before_unit
self.time_start = time.time()
self.time_start = None
self.width = width
def set(self, channel_id, value):
@ -142,6 +180,11 @@ class ProgressBar2:
c = self.channels[channel_id]
c.set(value)
if c.ratio is not None:
for channel in self.channels.values():
if channel.dynamic_top:
channel.top = channel._value / c.ratio
def render(self, clear=True, stream=sys.stderr):
"""
Renders the progressbar into a stream. If clear is True, clears the previous content first.
@ -149,18 +192,83 @@ class ProgressBar2:
width = self.width or get_terminal_size()[1]
raw = self.format
t = time.time()
for tag in re.findall("§.[a-zA-Z]", raw):
if not self.time_start:
self.time_start = t
for tag in re.findall("§[%rzsmhtT][a-zA-Z]", raw):
op = tag[1]
ch = tag[2]
channel = self.channels[ch.lower()]
inverse = ch.isupper()
# display literal §s
if op == "%":
ratio = 1 - channel.ratio if inverse else channel.ratio
raw = raw.replace(tag, channel._render(ratio * 100, "%", "percent"), 1)
elif op == "r": # display the current value
raw = raw.replace(tag, channel.inverse if inverse else channel.value, 1)
elif op == "z": # display the top value
if inverse:
raise ValueError("inverse of the top value is not defined: {:s}".format(tag))
if channel.top:
s = channel._render(channel.top, just="raw")
else:
s = "- {:s}".format(channel.unit).rjust(channel.min_width["raw"])
raw = raw.replace(tag, s, 1)
elif op.lower() == "t":
t_elapsed = t - self.time_start
t_to_end = (t_elapsed * (channel.top - channel._value) / channel._value) if channel._value > 0 else None
raw = raw.replace(tag, self._render_time(t_to_end if inverse else t_elapsed, op.isupper(), channel.min_width["time"]), 1)
elif op in "smh":
if inverse:
raise ValueError("the inverse of rates is not defined: {:s}".format(tag))
t_elapsed = t - self.time_start
if op == "s":
t_unit = "s"
t_div = 1
elif op == "m":
t_unit = "min"
t_div = 60
elif op == "h":
t_unit = "h"
t_div = 3600
if t_elapsed > 0:
rate = t_div * channel._value / t_elapsed
s = channel._render(rate, channel.unit + "/" + t_unit, "rate")
else:
s = "- {:s}/{:s}".format(channel.unit, t_unit).rjust(channel.min_width["rate"])
raw = raw.replace(tag, s, 1)
# all that remains are bars
free_space = width - len(re.sub("§[^%rzsmhtT][a-zA-Z]", "", raw))
for tag in re.findall("§[^%rzsmhtT][a-zA-Z]", raw):
op = tag[1]
ch = tag[2]
channel = self.channels[ch.lower()]
lengths = channel.divide(free_space)
raw = raw.replace(tag, op * lengths[1 if ch.isupper() else 0], 1)
# render literal §s
raw = raw.replace("§§", "§")
# pave over previous render and draw the new one
if clear:
stream.write("\r" + "_" * width)
stream.write("\r" + " " * width)
stream.write("\r" + raw)
@ -179,6 +287,20 @@ class ProgressBar2:
print()
def _render_time(self, seconds, hhmmss=False, just=0):
if hhmmss:
if seconds is None:
output = "--:--:--"
else:
mm, ss = divmod(int(seconds), 60)
hh, mm = divmod(mm, 60)
output = "{:02d}:{:02d}:{:02d}".format(hh, mm, ss)
else:
output = "- s" if seconds is None else "{:d} s".format(int(seconds))
return output.rjust(just)
class ProgressBar:
'''
An animated progress bar.
@ -203,6 +325,8 @@ class ProgressBar:
self.old_len = 0
self.t_start = None
_print("over.core.text.ProgressBar is deprecated and will be replaced by ProgressBar2 soon", prefix.warn)
def draw(self):
if not self.t_start:
self.t_start = time.time()
@ -401,116 +525,6 @@ def paragraph(text, width=0, indent=0, prefix=None, stamp=None):
return '\n'.join(lines_tmp) # put lines together
# --------------------------------------------------
# def strtrim(text, length, mode=0, dots=True, colors=True):
# '''
# str text text to trim
# int length desired length, >1
# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right
# bool dots add an ellipsis to the point of cutting
# bool colors dont count color tags into length; also turns the ellipsis blue
# '''
# if len(text) <= length:
# return text
# if length <= 3:
# dots = False
# if dots:
# length -= 3
# if mode == -1:
# if dots:
# return '...' + cut(text, length, 1, colors)
# else:
# return cut(text, length, 1, colors)
# elif mode == 0:
# if length%2 == 1:
# part1 = cut(text, length/2+1, 0, colors)
# else:
# part1 = cut(text, length/2, 0, colors)
# part2 = cut(text, length/2, 1, colors)
# if dots:
# part1 += '...'
# return part1 + part2
# else:
# if dots:
# return cut(text, length, 0, colors) + '...'
# else:
# return cut(text, length, 0, colors)
# --------------------------------------------------
# def textfilter(text, set=None):
# '''
# str text text to filter
# dict set {to_replace: replacement}
# Text filter that replaces occurences of to_replace keys with their respective values.
# Defaults to filtering of 'bad' characters if no translational dictionary is provided.
# '''
# if not set:
# set = badchars
# for to_replace in set.keys():
# text = text.replace(to_replace, set[to_replace])
# return text
# --------------------------------------------------
# def cut(text, size, end=0, colors=True):
# '''
# str text text to cut
# int size how many chars to return, >=0
# int end return chars from 0 = left, 1 = right
# bool colors skip color tags
# '''
# out = ''
# strlen = len(text)
# if end == 0:
# ptr = 0 # go from left
# else:
# ptr = strlen - 1
# while size:
# if end == 0:
# if colors and text[ptr] == '<' and strlen - ptr >= 4 and text[ptr+1] == 'C' and text[ptr+3] == '>': # we have a tag
# out += text[ptr:ptr+4]
# ptr += 4
# else:
# out += text[ptr]
# ptr += 1
# size -= 1
# else:
# if colors and text[ptr] == '>' and ptr >= 4 and text[ptr-2] == 'C' and text[ptr-3] == '<': # we have a tag
# out = text[ptr-3:ptr+1] + out
# ptr -= 4
# else:
# out = text[ptr] + out
# ptr -= 1
# size -= 1
# # reached end ?
# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1):
# break
# return out
# --------------------------------------------------
class prefix:
info = (' ', 'INFO')
debug = (' §b?§/', '§bDEBG§/')
@ -659,25 +673,36 @@ _print = Output('over.core.text', stream=sys.stderr)
if __name__ == "__main__":
pb = ProgressBar2(
"§%a (§ra/§ma) [§-a>§ A] §sa [§rb/§rB] (ETA §tA)",
"§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)",
{
"a": {
"unit": "f",
"prefix_base2": False,
"top": 39600,
"precision": 1
"precision": 1,
"min_width_raw": 10,
"min_width_rate": 10,
# "min_width_percent": 7, # 7 is already the default
"min_width_time": 0
},
"b": {
"unit": "o",
"prefix_base2": True,
"top": 3200,
"precision": 0
"precision": 0,
"min_width_raw": 10,
"min_width_rate": 10,
"min_width_time": 10
}
},
80
}
)
pb.set("a", 12000)
pb.set("b", 1500)
pb.render()
ratios = [0, 0.1, 0.25, 0.4, 0.6, 0.8, 0.95, 1.0]
for ratio in ratios:
pb.set("a", ratio * pb.channels["a"].top)
pb.set("b", ratio * pb.channels["b"].top)
pb.render()
time.sleep(0.25)
pb.end(not True)