from __future__ import annotations
import gc
import io
import sys
import progressbar
fast_module = progressbar.fast
class TTY(io.StringIO):
def isatty(self) -> bool:
return True
def repaints(self) -> list[str]:
return [p for p in self.getvalue().split('\r') if p]
def test_fast_known_length_renders_and_completes():
fd = TTY()
bar = fast_module.FastProgressBar(max_value=1000, fd=fd)
out = list(bar(range(1000)))
assert out == list(range(1000))
assert bar.value == 1000
assert bar.percentage == 100.0
assert bar._finished
frames = fd.repaints()
assert frames, 'fast bar drew nothing'
last = frames[-1]
assert '100%' in last
assert '(1000 of 1000)' in last
assert '|' in last
assert 'Elapsed Time:' in last
def test_fast_elapsed_with_no_start_time():
"""Test _fast_elapsed returns 0 when start_time is None."""
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
assert bar._fast_elapsed() == 0.0
def test_fast_format_line_with_eta_calculation():
"""Test ETA calculation path with done > 0 and elapsed > 0."""
from datetime import datetime, timedelta
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
bar.start_time = datetime.now() - timedelta(seconds=2)
bar.value = 50
line = bar._format_line()
assert 'ETA:' in line
assert 'ETA: --:--:--' not in line
def test_fast_spinner_frames_cycle():
"""The spinner is exactly four frames and cycles through all of them.
Regression: the raw literal ``r'|/-\\'`` is 5 chars long (the escape is not
collapsed), so the intended four-frame cycle was fragile and length-coupled
to a hardcoded ``% 4``.
"""
from datetime import datetime, timedelta
assert len(fast_module._SPINNER_FRAMES) == 4
assert set(fast_module._SPINNER_FRAMES) == set('|/-\\')
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=progressbar.UnknownLength, fd=fd
)
bar.start_time = datetime(2020, 1, 1)
seen = []
for quarter in range(4):
bar.end_time = bar.start_time + timedelta(seconds=quarter / 4)
seen.append(bar._format_line().lstrip()[0])
assert seen == ['|', '/', '-', '\\']
def test_fast_format_line_uses_native_hook(monkeypatch):
"""The native `_format_fast_line` hook takes precedence when set."""
def stub(bar) -> str:
return 'NATIVE_HOOK_OUTPUT'
monkeypatch.setattr(fast_module, '_format_fast_line', stub)
fd = TTY()
bar = fast_module.FastProgressBar(max_value=100, fd=fd)
assert bar._format_line() == 'NATIVE_HOOK_OUTPUT'
def test_fast_unknown_length_renders_count_and_elapsed():
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=progressbar.UnknownLength, fd=fd
)
out = list(bar(iter(range(40))))
assert out == list(range(40))
assert bar.value == 39
last = fd.repaints()[-1]
assert 'Elapsed Time:' in last
assert '40' in last
assert ' of ' not in last
def test_fast_prefix_suffix_in_line_not_widgets():
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=10, fd=fd, prefix='load ', suffix=' done'
)
list(bar(range(10)))
assert bar.widgets == []
last = fd.repaints()[-1]
assert last.lstrip().startswith('load')
assert 'done' in last
def test_fast_empty_iterable():
fd = TTY()
bar = fast_module.FastProgressBar(max_value=0, fd=fd)
assert list(bar([])) == []
assert bar._finished
def test_fast_break_restores_streams():
real_out = sys.stdout
fd = TTY()
bar = fast_module.FastProgressBar(
max_value=1000, fd=fd, redirect_stdout=True
)
for i in bar(range(1000)):
if i == 5:
break
del bar
gc.collect()
assert sys.stdout is real_out
def test_fast_with_statement():
fd = TTY()
with fast_module.FastProgressBar(max_value=10, fd=fd) as bar:
out = list(bar(range(10)))
assert out == list(range(10))
assert bar._finished
def test_shortcut_dispatch(monkeypatch):
from progressbar import shortcuts
calls = {'fast': 0, 'full': 0}
class FastSpy(fast_module.FastProgressBar):
def __init__(self, *a, **k):
calls['fast'] += 1
super().__init__(*a, **k)
class FullSpy(progressbar.ProgressBar):
def __init__(self, *a, **k):
calls['full'] += 1
super().__init__(*a, **k)
monkeypatch.setattr(shortcuts.fast_module, 'FastProgressBar', FastSpy)
monkeypatch.setattr(shortcuts.bar, 'ProgressBar', FullSpy)
assert list(shortcuts.progressbar(range(3), fd=TTY())) == [0, 1, 2]
assert calls == {'fast': 1, 'full': 0}
list(
shortcuts.progressbar(
range(3), fd=TTY(), widgets=[progressbar.Percentage()]
)
)
assert calls == {'fast': 1, 'full': 1}
list(shortcuts.progressbar(range(3), fd=TTY(), fast=False))
assert calls == {'fast': 1, 'full': 2}
monkeypatch.setenv('PROGRESSBAR_DISABLE_FASTPATH', '1')
list(shortcuts.progressbar(range(3), fd=TTY()))
assert calls == {'fast': 1, 'full': 3}
monkeypatch.delenv('PROGRESSBAR_DISABLE_FASTPATH', raising=False)
list(shortcuts.progressbar(range(3), fd=TTY(), variables={'x': 1}))
assert calls == {'fast': 1, 'full': 4}
def test_full_bar_injects_prefix_suffix_widgets():
fd = TTY()
bar_ = progressbar.ProgressBar(
max_value=10, fd=fd, prefix='pre ', suffix=' suf'
)
list(bar_(range(10)))
assert bar_.widgets
last = fd.repaints()[-1]
assert 'pre' in last
assert 'suf' in last
def test_import_progressbar_is_lazy():
import subprocess
check = (
'import sys, progressbar\n'
'assert "progressbar.multi" not in sys.modules\n'
'assert progressbar.FastProgressBar is not None\n'
'print("ok")\n'
)
out = subprocess.run(
[sys.executable, '-c', check], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr
assert 'ok' in out.stdout
def test_fast_path_does_not_import_widgets_or_colors():
import subprocess
check = (
'import sys, progressbar\n'
'list(progressbar.progressbar(range(10)))\n'
'assert "progressbar.widgets" not in sys.modules\n'
'assert "progressbar.terminal.colors" not in sys.modules\n'
'print("ok")\n'
)
out = subprocess.run(
[sys.executable, '-c', check], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr
assert 'ok' in out.stdout