Skip to content

Commit d957bef

Browse files
committed
Fix unknown-length redraw and animated marker fill at finish
- ProgressBar._needs_update now redraws unknown-length bars on every value change, so a non-time-sensitive widget set (e.g. the format_label example's FormatLabel) animates instead of jumping straight from the start value to the final value. - AnimatedMarker keeps a filled bar full when finished instead of collapsing to a single marker character, fixing the filling_bar_animated_marker and color_bar_animated_marker_example examples emptying out at 100%. - Isolate the SIGWINCH overlapping-bars regression test from global _ResizeRegistry state so the handler-restore branch is exercised deterministically regardless of suite ordering, restoring 100% coverage. Adds targeted regression tests for each.
1 parent 607fd70 commit d957bef

5 files changed

Lines changed: 78 additions & 4 deletions

File tree

progressbar/bar.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,11 @@ def _needs_update(self):
946946
elif self.poll_interval and delta > self.poll_interval:
947947
# Needs to redraw timers and animations
948948
return True
949+
elif self.max_value is base.UnknownLength:
950+
# There's no terminal-width threshold to compute for an unknown
951+
# length, so redraw whenever the value advanced (still rate
952+
# limited by the min_poll_interval check above)
953+
return self.value != self.previous_value
949954

950955
# Update if value increment is not large enough to
951956
# add more bars to progressbar (according to current

progressbar/widgets.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,11 @@ def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None):
854854
finished.
855855
"""
856856
if progress.end_time:
857+
# When finished, keep a filling marker full instead of
858+
# collapsing to a single character; a plain marker has no fill
859+
# so it falls back to its default character.
860+
if self.fill:
861+
return self.fill(progress, data, width)
857862
return self.default
858863

859864
marker = self.markers[data['updates'] % len(self.markers)]

tests/test_progressbar.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,23 +159,44 @@ def write(self, value: str) -> int:
159159
def test_sigwinch_restored_with_overlapping_bars() -> None:
160160
# Regression: A5 - with two live bars, finishing them in creation
161161
# order left a dangling handler installed.
162-
original = signal.getsignal(signal.SIGWINCH)
162+
from progressbar.bar import _ResizeRegistry
163+
164+
saved_handler = signal.getsignal(signal.SIGWINCH)
165+
# Isolate the global registry so the assertions don't depend on bars
166+
# left registered (and a handler left installed) by other tests
167+
saved_bars = list(_ResizeRegistry.bars)
168+
saved_prev = _ResizeRegistry.previous_handler
169+
_ResizeRegistry.bars.clear()
170+
_ResizeRegistry.previous_handler = None
171+
172+
# Start from a known sentinel handler so we can tell apart "still
173+
# installed" from "restored" without depending on global state
174+
signal.signal(signal.SIGWINCH, signal.SIG_IGN)
163175
try:
164176
bar1 = progressbar.ProgressBar(max_value=5, fd=io.StringIO())
165177
bar1.start()
166178
bar2 = progressbar.ProgressBar(max_value=5, fd=io.StringIO())
167179
bar2.start()
168180

181+
# The first bar installs the shared handler
182+
assert signal.getsignal(signal.SIGWINCH) is not signal.SIG_IGN
183+
169184
# A resize signal is dispatched to all live bars
170185
signal.raise_signal(signal.SIGWINCH)
171186
assert isinstance(bar1.term_width, int)
172187
assert isinstance(bar2.term_width, int)
173188

174189
bar1.update(5)
175190
bar1.finish()
191+
# The handler must stay installed while bar2 is still live
192+
assert signal.getsignal(signal.SIGWINCH) is not signal.SIG_IGN
193+
176194
bar2.update(5)
177195
bar2.finish()
178-
179-
assert signal.getsignal(signal.SIGWINCH) is original
196+
# The last bar to finish restores the previous handler
197+
assert signal.getsignal(signal.SIGWINCH) is signal.SIG_IGN
180198
finally:
181-
signal.signal(signal.SIGWINCH, original)
199+
for restored_bar in saved_bars:
200+
_ResizeRegistry.bars.add(restored_bar)
201+
_ResizeRegistry.previous_handler = saved_prev
202+
signal.signal(signal.SIGWINCH, saved_handler)

tests/test_unknown_length.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,23 @@ def test_unknown_length_at_start() -> None:
2828
for w in pb2.widgets:
2929
print(type(w), repr(w))
3030
assert any(isinstance(w, progressbar.Bar) for w in pb2.widgets)
31+
32+
33+
def test_unknown_length_redraws_on_value_change() -> None:
34+
# With an unknown length and a non-time-sensitive widget (no
35+
# `INTERVAL`), the bar still needs to redraw whenever the value
36+
# advances; otherwise it would only ever show the start and finish
37+
# values. See the `format_label` example.
38+
pb = progressbar.ProgressBar(
39+
widgets=[progressbar.FormatLabel('%(value)d')],
40+
max_value=progressbar.UnknownLength,
41+
).start()
42+
43+
assert pb.poll_interval is None
44+
pb.previous_value = 2
45+
pb.value = 3
46+
# Make sure the min_poll_interval rate limit is not what blocks us
47+
pb._last_update_timer -= 10
48+
assert pb._needs_update() is True
49+
50+
pb.finish()

tests/test_widgets.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,26 @@ def test_bar_widget_respects_min_value() -> None:
247247
bar.start()
248248
assert '#' not in bar.fd.getvalue()
249249
bar.finish(dirty=True)
250+
251+
252+
def test_animated_marker_fill_stays_full_when_finished() -> None:
253+
# Regression: a Bar filled by an AnimatedMarker(fill=...) collapsed to a
254+
# single marker character at finish() because the end_time branch
255+
# short-circuited before applying the fill. The finished bar must stay
256+
# full instead of emptying out at 100%.
257+
bar = progressbar.ProgressBar(
258+
widgets=[progressbar.Bar(marker=progressbar.AnimatedMarker(fill='#'))],
259+
max_value=10,
260+
fd=io.StringIO(),
261+
term_width=60,
262+
)
263+
bar.start()
264+
for i in range(11):
265+
bar.update(i)
266+
bar.finish()
267+
268+
last_line = [
269+
line for line in bar.fd.getvalue().split('\n') if line.strip()
270+
][-1]
271+
# term_width 60 leaves ~58 fill characters; the collapse bug left ~1
272+
assert last_line.count('#') > 40, repr(last_line)

0 commit comments

Comments
 (0)