Skip to content

Commit 21eeda9

Browse files
Ken Odegardjonathanslenders
authored andcommitted
Introducing ProgressBarCounter.stopped
Include a distinction for counters that have stopped (time_elapsed no longer increments and Bar formatter stops running in the case of unknown totals). This allows bars to better handle cases where a counter doesn't complete properly and terminates in some error state the developer cares about. Added breaking task to a-lot-of-parallel-tasks.py example.
1 parent 5ee9df2 commit 21eeda9

File tree

3 files changed

+94
-25
lines changed

3 files changed

+94
-25
lines changed

examples/progress-bar/a-lot-of-parallel-tasks.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,24 @@ def main():
1717
) as pb:
1818

1919
def run_task(label, total, sleep_time):
20+
"""Complete a normal run."""
2021
for i in pb(range(total), label=label):
2122
time.sleep(sleep_time)
2223

24+
def stop_task(label, total, sleep_time):
25+
"""Stop at some random index.
26+
27+
Breaking out of iteration at some stop index mimics how progress
28+
bars behave in cases where errors are raised.
29+
"""
30+
stop_i = random.randrange(total)
31+
bar = pb(range(total), label=label)
32+
for i in bar:
33+
if stop_i == i:
34+
bar.label = f"{label} BREAK"
35+
break
36+
time.sleep(sleep_time)
37+
2338
threads = []
2439

2540
for i in range(160):
@@ -28,7 +43,10 @@ def run_task(label, total, sleep_time):
2843
sleep_time = random.randrange(5, 20) / 100.0
2944

3045
threads.append(
31-
threading.Thread(target=run_task, args=(label, total, sleep_time))
46+
threading.Thread(
47+
target=random.choice((run_task, stop_task)),
48+
args=(label, total, sleep_time),
49+
)
3250
)
3351

3452
for t in threads:

prompt_toolkit/shortcuts/progress_bar/base.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@
5656
try:
5757
import contextvars
5858
except ImportError:
59-
from prompt_toolkit.eventloop import dummy_contextvars as contextvars # type: ignore
59+
from prompt_toolkit.eventloop import ( # type: ignore
60+
dummy_contextvars as contextvars,
61+
)
6062

6163

62-
__all__ = [
63-
"ProgressBar",
64-
]
64+
__all__ = ["ProgressBar"]
6565

6666
E = KeyPressEvent
6767

@@ -342,13 +342,20 @@ def __init__(
342342
self.total = total
343343

344344
def __iter__(self) -> Iterable[_CounterItem]:
345-
try:
346-
if self.data is not None:
345+
if self.data is not None:
346+
try:
347347
for item in self.data:
348348
yield item
349349
self.item_completed()
350-
finally:
351-
self.done = True
350+
351+
# Only done if we iterate to the very end.
352+
self.done = True
353+
finally:
354+
# Ensure counter has stopped even if we did not iterate to the
355+
# end (e.g. break or exceptions).
356+
self.stopped = True
357+
else:
358+
raise NotImplementedError("No data defined to iterate over.")
352359

353360
def item_completed(self) -> None:
354361
"""
@@ -361,18 +368,52 @@ def item_completed(self) -> None:
361368

362369
@property
363370
def done(self) -> bool:
371+
"""Whether a counter has been completed.
372+
373+
Done counter have been stopped (see stopped) and removed depending on
374+
remove_when_done value.
375+
376+
Contrast this with stopped. A stopped counter may be terminated before
377+
100% completion. A done counter has reached its 100% completion.
378+
"""
364379
return self._done
365380

366381
@done.setter
367382
def done(self, value: bool) -> None:
368383
self._done = value
369-
370-
# If done then store the stop_time, otherwise clear.
371-
self.stop_time = datetime.datetime.now() if value else None
384+
self.stopped = value
372385

373386
if value and self.remove_when_done:
374387
self.progress_bar.counters.remove(self)
375388

389+
@property
390+
def stopped(self) -> bool:
391+
"""Whether a counter has been stopped.
392+
393+
Stopped counters no longer have increasing time_elapsed. This distinction is
394+
also used to prevent the Bar formatter with unknown totals from continuing to run.
395+
396+
A stopped counter (but not done) can be used to signal that a given counter has
397+
encountered an error but allows other counters to continue
398+
(e.g. download X of Y failed). Given how only done counters are removed
399+
(see remove_when_done) this can help aggregate failures from a large number of
400+
successes.
401+
402+
Contrast this with done. A done counter has reached its 100% completion.
403+
A stopped counter may be terminated before 100% completion.
404+
"""
405+
return self.stop_time is not None
406+
407+
@stopped.setter
408+
def stopped(self, value: bool):
409+
if value:
410+
# This counter has not already been stopped.
411+
if not self.stop_time:
412+
self.stop_time = datetime.datetime.now()
413+
else:
414+
# Clearing any previously set stop_time.
415+
self.stop_time = None
416+
376417
@property
377418
def percentage(self) -> float:
378419
if self.total is None:

prompt_toolkit/shortcuts/progress_bar/formatters.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,31 @@ def format(
177177
progress: "ProgressBarCounter[object]",
178178
width: int,
179179
) -> AnyFormattedText:
180+
if progress.done or progress.total or progress.stopped:
181+
sym_a, sym_b, sym_c = self.sym_a, self.sym_b, self.sym_c
182+
183+
# Compute pb_a based on done, total, or stopped states.
184+
if progress.done:
185+
# 100% completed irrelevant of how much was actually marked as completed.
186+
percent = 1.0
187+
else:
188+
# Show percentage completed.
189+
percent = progress.percentage / 100
190+
else:
191+
# Total is unknown and bar is still running.
192+
sym_a, sym_b, sym_c = self.sym_c, self.unknown, self.sym_c
180193

181-
# Subtract left, bar_b and right.
182-
width -= get_cwidth(self.start + self.sym_b + self.end)
194+
# Compute percent based on the time.
195+
percent = time.time() * 20 % 100 / 100
183196

184-
if progress.total:
185-
pb_a = int(progress.percentage * width / 100)
186-
bar_a = self.sym_a * pb_a
187-
bar_b = self.sym_b
188-
bar_c = self.sym_c * (width - pb_a)
189-
else:
190-
# Total is unknown.
191-
pb_a = int(time.time() * 20 % 100 * width / 100)
192-
bar_a = self.sym_c * pb_a
193-
bar_b = self.unknown
194-
bar_c = self.sym_c * (width - pb_a)
197+
# Subtract left, sym_b, and right.
198+
width -= get_cwidth(self.start + sym_b + self.end)
199+
200+
# Scale percent by width
201+
pb_a = int(percent * width)
202+
bar_a = sym_a * pb_a
203+
bar_b = sym_b
204+
bar_c = sym_c * (width - pb_a)
195205

196206
return HTML(self.template).format(
197207
start=self.start, end=self.end, bar_a=bar_a, bar_b=bar_b, bar_c=bar_c

0 commit comments

Comments
 (0)