From fddccb981758013a98e7bb95d77883e750dedf51 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 5 Nov 2026 11:01:44 +0100 Subject: [PATCH] Render desc as a template again, through the safe formatter #330 made desc literal text, so a brace in a file name no longer raised. That changed what every 4.6.0 user with desc='{value}' saw, and desc was documented as the prefix alias all along. The alias is a plain prefix again: a field renders the bar's data, a dunder walk is refused like any other template, and a literal brace is doubled. The fast formatter never rendered templates and still does not. --- docs/howto/tqdm-style.rst | 4 ++-- progressbar/bar.py | 21 ++++++--------------- tests/test_format_security.py | 34 ++++++++++++++++------------------ 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/docs/howto/tqdm-style.rst b/docs/howto/tqdm-style.rst index 292bc0e..2e80ee9 100644 --- a/docs/howto/tqdm-style.rst +++ b/docs/howto/tqdm-style.rst @@ -9,8 +9,8 @@ here. .. demo:: howto/tqdm-style ``desc`` and ``total`` work on any ``ProgressBar``: ``desc`` becomes the -prefix (rendered as ``f'{desc}: '``, as literal text, so braces in a file -name stay braces), and ``total`` becomes ``max_value`` when ``max_value`` +prefix (rendered as ``f'{desc}: '``, a format template like ``prefix``, +so double a literal brace), and ``total`` becomes ``max_value`` when ``max_value`` isn't given explicitly. ``unit=``/``unit_scale=`` don't render anything by themselves -- they only show up through a widget that reads them, such as ``UnitProgress()`` -- so pass a widget diff --git a/progressbar/bar.py b/progressbar/bar.py index 4004ae4..7905582 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -915,10 +915,10 @@ class ProgressBar( line_offset: The number of lines to offset the progressbar from your current line. This is useful if you have other output or multiple progressbars. - desc: tqdm-style description, rendered literally as - `f'{desc}: '` in front of the bar. Unlike `prefix` it is - never a format template: braces in a file name stay braces. - Ignored if `prefix` is also given. + desc: tqdm-style alias for `prefix`, rendered as `f'{desc}: '` + in front of the bar. Like `prefix` it is a format template, + so `desc='{value}'` shows the value and a literal brace + must be doubled. Ignored if `prefix` is also given. total: tqdm-style alias for `max_value`. Ignored if `max_value` is also given. unit: The unit label used by unit-aware widgets. Defaults to @@ -1002,14 +1002,9 @@ def __init__( if max_value is None and total is not None: # tqdm-style alias for `max_value` max_value = total - self._literal_prefix: str | None = None if prefix is None and desc is not None: - # tqdm-style alias for `prefix`: `desc` is literal text, as in - # tqdm and as the fast path renders it. Remember the exact - # string, so `_init_prefix` escapes its braces and a prefix - # assigned later is a template again. + # tqdm-style alias for `prefix`, a template like `prefix` prefix = f'{desc}: ' - self._literal_prefix = prefix if max_value and min_value > typing.cast(NumberT, max_value): raise ValueError( @@ -1823,12 +1818,8 @@ def _init_prefix(self) -> None: if self.prefix: widgets = _load_widgets() - template: str = self.prefix - if template is getattr(self, '_literal_prefix', None): - # A desc is literal text: escape its braces for the label - template = template.replace('{', '{{').replace('}', '}}') self.widgets.insert( - 0, widgets.FormatLabel(template, new_style=True) + 0, widgets.FormatLabel(self.prefix, new_style=True) ) self.prefix = None diff --git a/tests/test_format_security.py b/tests/test_format_security.py index 5c5f767..638fb8a 100644 --- a/tests/test_format_security.py +++ b/tests/test_format_security.py @@ -24,16 +24,20 @@ def _render(**kwargs: object) -> str: return fd.getvalue() -def test_desc_attribute_walk_renders_literally() -> None: +def test_desc_attribute_walk_is_refused() -> None: desc: str = ( '{variables.__class__.__getattr__.__globals__[os].environ[HOME]}' ) - output: str = _render(desc=desc) - assert desc + ': ' in output + with pytest.raises(ValueError, match='__class__'): + _render(desc=desc) -def test_desc_with_braces_renders_literally() -> None: - output: str = _render(desc='report{2024}.txt') +def test_desc_is_a_template_like_prefix() -> None: + # 4.6.0 rendered desc through the prefix label, so a field in it + # shows the bar's data and a literal brace has to be doubled. + output: str = _render(desc='{value} done') + assert '10 done: ' in output # the finishing redraw + output = _render(desc='report{{2024}}.txt') assert 'report{2024}.txt: ' in output @@ -239,30 +243,24 @@ def warn() -> None: assert len(messages) == 18 -def test_desc_prefix_is_a_format_label_that_renders_literally() -> None: +def test_desc_prefix_is_a_format_label_with_variables() -> None: fd: io.StringIO = io.StringIO() bar: progressbar.ProgressBar = progressbar.ProgressBar( fd=fd, max_value=10, - desc='{files}', + desc='{variables.files}', + variables={'files': 'spam'}, widgets=[progressbar.SimpleProgress()], ) - assert bar.prefix == '{files}: ' # read back unescaped, as in 4.6.0 + assert bar.prefix == '{variables.files}: ' # as in 4.6.0 bar.start() assert isinstance(bar.widgets[0], progressbar.FormatLabel) # as in 4.6.0 bar.update(4, force=True) - assert '{files}: 4 of 10' in fd.getvalue() + assert 'spam: 4 of 10' in fd.getvalue() -def test_desc_with_braces_renders_literally_on_the_fast_path() -> None: - fd: io.StringIO = io.StringIO() - bar: progressbar.ProgressBar = progressbar.ProgressBar( - fd=fd, max_value=10, desc='{files}' - ) - bar.start() - bar.update(4, force=True) - assert '{files}: ' in fd.getvalue() - assert '{{files}}' not in fd.getvalue() +def test_desc_renders_literally_on_the_fast_path() -> None: + # The fast formatter never rendered templates, in 4.6.0 either. fast: progressbar.FastProgressBar = progressbar.FastProgressBar( fd=io.StringIO(), max_value=10, desc='{files}' )