Skip to content

Commit

Permalink
Reposition bars below a closed bar
Browse files Browse the repository at this point in the history
When a bar is closed it is moved to pos 0, which means all the other
bars below it must now be given a new position number to have their
output still go to the same screen line.
  • Loading branch information
mjpieters committed Aug 25, 2023
1 parent 4c956c2 commit 07c1559
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 35 deletions.
69 changes: 63 additions & 6 deletions tests/tests_tqdm.py
Expand Up @@ -1281,20 +1281,78 @@ def test_position():
t3.update(1)
t4.update(1)
res = [m[0] for m in RE_pos.findall(our_file.getvalue())]
exres = ['\r1.pos0 bar: 0%',
'\n\r2.pos1 bar: 0%',
'\n\n\r3.pos2 bar: 0%',
exres = [*exres,
'\r2.pos1 bar: 0%',
'\n\n\r3.pos2 bar: 0%',
'\n\n\r4.pos2 bar: 0%',
'\r1.pos0 bar: 10%',
'\n\n\r3.pos2 bar: 10%',
'\n\r4.pos2 bar: 10%']
'\n\r3.pos2 bar: 10%',
'\n\n\r4.pos2 bar: 10%']
pos_line_diff(res, exres)
t4.close()
t3.close()
t1.close()


@mark.skipif(nt_and_no_colorama, reason="Windows without colorama")
@mark.parametrize('leave', [
True, # all bars remain
False, # no bars remain
None # only first bar remains
])
def test_position_leave(leave: bool):
"""Test leaving of nested positioned progress bars"""
our_file = StringIO()
kwargs = {
'file': our_file,
'miniters': 1,
'mininterval': 0,
'maxinterval': 0,
'leave': leave,
}
for _ in trange(2, desc='pos0 bar', position=0, **kwargs):
t2 = tqdm(total=2, desc='pos1 bar', position=1, **kwargs)
t2.update()
t3 = tqdm(total=2, desc='pos2 bar', position=2, **kwargs)
t3.update()
# complete t2 before t3
t2.update()
t2.close()
t3.update()
t3.close()

out = our_file.getvalue()
res = [m[0] for m in RE_pos.findall(out)]
# Bar 2 being left from the screen means bar 3 needs extra newline when
# positioning. If it is not left, then bar 3 needs to be cleared in its old
# position and redrawn in gap left by bar 2.
if leave:
bar2left, bar3move = '\n', []
else:
bar2left, bar3move = '', ['\n\n\r ', '\r\x1b[A\x1b[A']
innerex = ['\n\rpos1 bar: 0%',
'\n\rpos1 bar: 50%',
'\n\n\rpos2 bar: 0%',
'\n\n\rpos2 bar: 50%',
'\n\rpos1 bar: 100%',
'\rpos1 bar: 100%' if leave else '\n\r ',
*bar3move,
bar2left + '\n\rpos2 bar: 50%',
'\n\rpos2 bar: 100%',
'\rpos2 bar: 100%' if leave else '\n\r ']
# Bar 1 being left on screen adds an extra newline to the output
# that then shows up as part of the next res line.
bar1left = '\n' if leave else ''
exres = ['\rpos0 bar: 0%',
*innerex,
bar1left + '\rpos0 bar: 50%',
*innerex,
bar1left + '\rpos0 bar: 100%',
'\rpos0 bar: 100%' if leave is not False else '\r ',
'\n' if leave is not False else '\r']
pos_line_diff(res, exres)


def test_set_description():
"""Test set description"""
with closing(StringIO()) as our_file:
Expand Down Expand Up @@ -1895,7 +1953,6 @@ def test_screen_shape():
assert "one" in res
assert "two" in res
assert "three" in res
assert "\n\n" not in res
assert "more hidden" in res
# double-check ncols
assert all(len(i) == 50 for i in get_bar(res)
Expand Down
81 changes: 52 additions & 29 deletions tqdm/std.py
Expand Up @@ -715,6 +715,27 @@ def _decr_instances(cls, instance):
inst = min(instances, key=lambda i: i.pos)
inst.clear(nolock=True)
inst.pos = abs(instance.pos)
else:
# renumber remaining bars with positions below this bar so
# they maintain their positions
apos = abs(instance.pos)
readjust = [
(inst.pos, inst)
for inst in cls._instances
if not inst.disable and abs(getattr(inst, "pos", apos)) > apos
]
for pos, inst in sorted(readjust, key=lambda pi: -abs(pi[0])):
newpos = inst.pos + (1 if pos < 0 else -1)
if newpos == 0 and inst.leave is None:
# any bars now moving to pos=0 should not be left on
# screen if `leave` was set to `None`.
inst.leave = False
if not inst.leave:
# Clear the old position before moving the bar so we
# don't leave any artefacts on screen.
inst.clear(nolock=True)
inst.pos = newpos
inst.display()

@classmethod
def write(cls, s, file=None, end="\n", nolock=False):
Expand Down Expand Up @@ -1271,41 +1292,43 @@ def close(self):
# Prevent multiple closures
self.disable = True

# decrement instance pos and remove from internal set
pos = abs(self.pos)
self._decr_instances(self)
try:
if self.last_print_t < self.start_t + self.delay:
# haven't ever displayed; nothing to clear
return

if self.last_print_t < self.start_t + self.delay:
# haven't ever displayed; nothing to clear
return
# GUI mode
if getattr(self, 'sp', None) is None:
return

# GUI mode
if getattr(self, 'sp', None) is None:
return
# annoyingly, _supports_unicode isn't good enough
def fp_write(s):
self.fp.write(str(s))

# annoyingly, _supports_unicode isn't good enough
def fp_write(s):
self.fp.write(str(s))
try:
fp_write('')
except ValueError as e:
if 'closed' in str(e):
return
raise # pragma: no cover

try:
fp_write('')
except ValueError as e:
if 'closed' in str(e):
return
raise # pragma: no cover
pos = abs(self.pos)
leave = pos == 0 if self.leave is None else self.leave

leave = pos == 0 if self.leave is None else self.leave
with self._lock:
if leave:
# stats for overall rate (no weighted average)
self._ema_dt = lambda: None
self.display(pos=0)
fp_write('\n')
else:
# clear previous display
if self.display(msg='', pos=pos) and not pos:
fp_write('\r')

with self._lock:
if leave:
# stats for overall rate (no weighted average)
self._ema_dt = lambda: None
self.display(pos=0)
fp_write('\n')
else:
# clear previous display
if self.display(msg='', pos=pos) and not pos:
fp_write('\r')
finally:
# decrement instance pos and remove from internal set
self._decr_instances(self)

def clear(self, nolock=False):
"""Clear current bar display."""
Expand Down

0 comments on commit 07c1559

Please sign in to comment.