diff --git a/AsyncProgress.py b/AsyncProgress.py index fc4f0eb..cd50b30 100644 --- a/AsyncProgress.py +++ b/AsyncProgress.py @@ -5,7 +5,7 @@ import tkinter as tk import tkinter.ttk as ttk from contextlib import contextmanager from dataclasses import dataclass -from typing import Optional +from typing import Optional, Callable class ClearableQueue(queue.Queue): @@ -25,29 +25,41 @@ def acquire_timeout(lock, timeout): lock.release() +def format_time(seconds): + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f'{str(int(h)) + "h " if h > 0 else ""}{"{:2d}min ".format(int(m)) if m > 0 else ""}{int(s):2d}s' + + class AsyncProgress(ttk.Frame): - @dataclass(repr=False) + @dataclass(init=False, repr=False) class Stats: + start_time: float total: int current: int = 0 - start_time: float = time.time() last_elapsed_time: float = 0 total_elapsed_time: float = 0 + def __init__(self, total: int): + self.total = total + self.start_time = time.time() + def __repr__(self): ips = 1 / self.last_elapsed_time if self.last_elapsed_time != 0 else 0 return (f"{'{v:{p}d}'.format(v=self.current, p=len(str(self.total)))}/{self.total} " - f"| {ips:.2f}it/s " - f"| elapsed: {time.time() - self.start_time:.2f} " - f"| remaining: {(self.total - self.current) * self.last_elapsed_time:.2f} ") + f"| {self.last_elapsed_time}s/it" if ips < 1 else f"{ips:.2f}it/s " + f"| elapsed: {format_time(time.time() - self.start_time)} " + f"| remaining: {format_time((self.total - self.current) * self.last_elapsed_time):} ") class Range: - def __init__(self, parent, iterable, update_interval): + def __init__(self, parent, iterable, iter_len, update_interval, stop_event): self.iterable = iterable + self.iter_len = iter_len self.parent = parent self.update_interval = update_interval + self.stop_event = stop_event - self.parent.pbar['maximum'] = len(iterable) + self.parent.pbar['maximum'] = iter_len def __iter__(self): last_time = time.time() @@ -57,6 +69,8 @@ class AsyncProgress(ttk.Frame): try: for obj in self.iterable: + if self.stop_event.is_set(): + return yield obj stats.current += 1 @@ -67,17 +81,17 @@ class AsyncProgress(ttk.Frame): self.parent.step(1, stats) last_update_time = time.time() + finally: stats.total_elapsed_time = time.time() - stats.start_time self.parent._finish(stats) - finally: self.close() def close(self): self.iterable = range(0) def __init__(self, parent, *, - width=450, + width=800, height=30, update_interval=20, range_update_interval=10, @@ -87,6 +101,10 @@ class AsyncProgress(ttk.Frame): self.__event_step_queue = ClearableQueue() self.__lock = threading.Lock() + self.__cancel_event = threading.Event() + + self.__stop_condition = None + self.__tk_pbar_value = tk.IntVar() self.__tk_stats_str = tk.StringVar() self.__tk_stats_str.set("Not running") @@ -117,16 +135,33 @@ class AsyncProgress(ttk.Frame): self.__tk_pbar_value.set(0) self.__tk_stats_str.set("Not running") + def stop(self): + self.__cancel_event.set() + + def stop_if(self, condition: Callable): + if (condition()): + self.stop() + else: + self.__stop_condition = condition + def range(self, start_stop, stop=None, step=1): + _start = start_stop + _stop = stop + if _stop is None: + _stop = start_stop + _start = 0 + return self.iter(range(_start, _stop, step)) + + def iter(self, iterable, length=None): + if length is None: + length = len(iterable) with self.__lock: if self.running: raise RuntimeError('Progressbar is already running') - if stop is None: - stop = start_stop - start_stop = 0 - return AsyncProgress.Range(self, range(start_stop, stop, step), self.range_update_interval) + return AsyncProgress.Range(self, iterable, length, self.range_update_interval, self.__cancel_event) def _start(self): + self.__cancel_event.clear() with self.__lock: self.running = True @@ -141,6 +176,9 @@ class AsyncProgress(ttk.Frame): def __update_self(self): with acquire_timeout(self.__lock, 0.1): + if self.running and self.__stop_condition is not None and self.__stop_condition(): + self.stop() + while not self.__event_step_queue.empty(): (amount, stat) = self.__event_step_queue.get() @@ -163,7 +201,7 @@ if __name__ == '__main__': def worker(pbar, t): print('Starting worker') while True: - for i in pbar.range(10): + for i in pbar.range(1000): print(i) time.sleep(t) @@ -174,6 +212,7 @@ if __name__ == '__main__': root = tk.Tk() + root.minsize(width=800, height=600) bar1 = AsyncProgress(root, label="Progressbar 1", update_interval=20, range_update_interval=10) bar2 = AsyncProgress(root, label="Progressbar 2", update_interval=20, range_update_interval=10) bar3 = AsyncProgress(root, label=None, update_interval=20, range_update_interval=10)