Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/howto/tqdm-style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 6 additions & 15 deletions progressbar/bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down
34 changes: 16 additions & 18 deletions tests/test_format_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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}'
)
Expand Down
Loading