From aee8555ba782d303d191e1cd2cd3d7c2c1b96ffa Mon Sep 17 00:00:00 2001 From: Rupayon Haldar <80724680+rupayon123@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:48:38 -0400 Subject: [PATCH] Account for labels in the fast progress bar width --- .../performance-and-the-fast-path.rst | 5 ++ progressbar/fast.py | 4 +- tests/test_fast_default.py | 50 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/docs/explanation/performance-and-the-fast-path.rst b/docs/explanation/performance-and-the-fast-path.rst index 100e502e..d21b4626 100644 --- a/docs/explanation/performance-and-the-fast-path.rst +++ b/docs/explanation/performance-and-the-fast-path.rst @@ -42,6 +42,11 @@ gets the full ``ProgressBar`` instead. You can also construct renderer without going through the shortcut -- see :doc:`../reference/progressbar`. +The known-length fast bar reserves width for ``prefix`` (or ``desc``) +and ``suffix`` before sizing its bar. It uses ``custom_len`` to measure +the fixed text, so ANSI colors and custom display-width rules do not +consume extra bar columns. + This dispatch only happens inside ``progressbar.progressbar()``. Constructing ``ProgressBar()`` yourself always gets the full class. diff --git a/progressbar/fast.py b/progressbar/fast.py index 3fd71d62..4c737039 100644 --- a/progressbar/fast.py +++ b/progressbar/fast.py @@ -76,7 +76,9 @@ def _pure_format_fast_line(bar: FastProgressBar) -> str: eta = '--:--:--' left = f'{pct:3.0f}% {count} ' right = f' Elapsed Time: {elapsed_text} ETA: {eta}' - inner = max(width - len(left) - len(right) - 2, 0) + inner = max( + width - bar.custom_len(prefix + left + right + suffix) - 2, 0 + ) filled = int(inner * done / total) if total else inner barstr = '|' + '#' * filled + ' ' * (inner - filled) + '|' return f'{prefix}{left}{barstr}{right}{suffix}' diff --git a/tests/test_fast_default.py b/tests/test_fast_default.py index b9fc8700..eaaa92f3 100644 --- a/tests/test_fast_default.py +++ b/tests/test_fast_default.py @@ -4,6 +4,8 @@ import io import sys +import pytest + import progressbar # Alias (not a `from` import) so CodeQL doesn't flag `progressbar` as imported @@ -248,3 +250,51 @@ def test_fast_path_does_not_import_widgets_or_colors(): ) assert out.returncode == 0, out.stderr assert 'ok' in out.stdout + + +@pytest.mark.parametrize('width', [80, 110]) +@pytest.mark.parametrize( + ('prefix', 'suffix'), + [ + ('', ''), + ('load ', ''), + ('', ' done'), + ('load ', ' done'), + ('\x1b[31mload \x1b[0m', ' done'), + ], +) +def test_fast_labels_share_terminal_width(width, prefix, suffix): + fd = TTY() + bar = fast_module.FastProgressBar( + max_value=10, fd=fd, term_width=width, prefix=prefix, suffix=suffix + ) + assert list(bar(range(10))) == list(range(10)) + for frame in fd.repaints(): + assert progressbar.utils.len_color(frame.rstrip('\r\n')) == width + + +def test_fast_labels_use_custom_width_measurement(): + def display_width(text): + return progressbar.utils.len_color(text.replace('界', 'xx')) + + fd = TTY() + bar = fast_module.FastProgressBar( + max_value=10, + fd=fd, + term_width=80, + prefix='界 ', + suffix=' 界', + custom_len=display_width, + ) + assert list(bar(range(10))) == list(range(10)) + for frame in fd.repaints(): + assert display_width(frame.rstrip('\r\n')) == 80 + + +def test_shortcut_desc_shares_terminal_width(): + fd = TTY() + assert list( + progressbar.progressbar(range(10), fd=fd, desc='load ', term_width=80) + ) == list(range(10)) + for frame in fd.repaints(): + assert progressbar.utils.len_color(frame.rstrip('\r\n')) == 80