AI-Project/AsyncProgress.py

236 lines
7.5 KiB
Python
Raw Normal View History

2024-07-03 00:42:21 +02:00
import queue
import threading
import time
import tkinter as tk
import tkinter.ttk as ttk
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Optional, Callable
2024-07-03 00:42:21 +02:00
class ClearableQueue(queue.Queue):
def clear(self):
with self.mutex:
while self._qsize() != 0:
_ = self._get()
@contextmanager
def acquire_timeout(lock, timeout):
result = lock.acquire(timeout=timeout)
try:
yield result
finally:
if result:
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'
2024-07-03 00:42:21 +02:00
class AsyncProgress(ttk.Frame):
@dataclass(init=False, repr=False)
2024-07-03 00:42:21 +02:00
class Stats:
start_time: float
2024-07-03 00:42:21 +02:00
total: int
current: int = 0
last_elapsed_time: float = 0
total_elapsed_time: float = 0
def __init__(self, total: int):
self.total = total
self.start_time = time.time()
2024-07-03 00:42:21 +02:00
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} "
2024-07-06 22:27:25 +02:00
f"| {f'{self.last_elapsed_time:.2f}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):} ")
2024-07-03 00:42:21 +02:00
class Range:
def __init__(self, parent, iterable, iter_len, update_interval, stop_event):
2024-07-03 00:42:21 +02:00
self.iterable = iterable
self.iter_len = iter_len
2024-07-03 00:42:21 +02:00
self.parent = parent
self.update_interval = update_interval
self.stop_event = stop_event
2024-07-03 00:42:21 +02:00
self.parent.pbar['maximum'] = iter_len
2024-07-03 00:42:21 +02:00
def __iter__(self):
last_time = time.time()
last_update_time = 0
stats = AsyncProgress.Stats(len(self.iterable))
self.parent._start()
try:
for obj in self.iterable:
if self.stop_event.is_set():
return
2024-07-03 00:42:21 +02:00
yield obj
stats.current += 1
stats.last_elapsed_time = time.time() - last_time
last_time = time.time()
if time.time() - last_update_time > self.update_interval / 1000:
self.parent.step(1, stats)
last_update_time = time.time()
finally:
2024-07-03 00:42:21 +02:00
stats.total_elapsed_time = time.time() - stats.start_time
self.parent._finish(stats)
self.close()
def close(self):
self.iterable = range(0)
def __init__(self, parent, *,
width=800,
2024-07-03 00:42:21 +02:00
height=30,
update_interval=20,
range_update_interval=10,
label=None):
super().__init__(parent, width=width, height=height)
self.grid_propagate(False)
self.__event_step_queue = ClearableQueue()
self.__lock = threading.Lock()
self.__cancel_event = threading.Event()
self.__stop_condition = None
2024-07-03 00:42:21 +02:00
self.__tk_pbar_value = tk.IntVar()
self.__tk_stats_str = tk.StringVar()
self.__tk_stats_str.set("Not running")
self.running = False
self.update_interval = update_interval
self.range_update_interval = range_update_interval
self.label = ttk.Label(self, text=label if label is not None else "")
self.pbar = ttk.Progressbar(self, variable=self.__tk_pbar_value)
self.stats = ttk.Label(self, textvariable=self.__tk_stats_str)
self.label.configure(font='TkFixedFont')
self.stats.configure(font='TkFixedFont')
self.label.grid(row=0, column=0, sticky=tk.NW)
self.pbar.grid(row=0, column=1, sticky=tk.NW)
self.stats.grid(row=0, column=2, sticky=tk.NW, padx=5)
self.__schedule_update()
def step(self, amount: int = 1, stat: Optional[Stats] = None):
self.__event_step_queue.put((amount, stat))
def reset(self):
with self.__lock:
self.running = False
self.__event_step_queue.clear()
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
2024-07-03 00:42:21 +02:00
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)
2024-07-03 00:42:21 +02:00
with self.__lock:
if self.running:
raise RuntimeError('Progressbar is already running')
return AsyncProgress.Range(self, iterable, length, self.range_update_interval, self.__cancel_event)
2024-07-03 00:42:21 +02:00
def _start(self):
self.__cancel_event.clear()
2024-07-03 00:42:21 +02:00
with self.__lock:
self.running = True
def _finish(self, stat):
with self.__lock:
self.running = False
self.__event_step_queue.clear()
self.__event_step_queue.put((0, stat))
def __schedule_update(self):
self.master.after(self.update_interval, self.__update_self)
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()
2024-07-03 00:42:21 +02:00
while not self.__event_step_queue.empty():
(amount, stat) = self.__event_step_queue.get()
if stat is not None:
self.__tk_pbar_value.set(stat.current)
if self.running:
self.__tk_stats_str.set(stat)
else:
self.__tk_stats_str.set(
f'{stat.current}/{stat.total} | total: {stat.total_elapsed_time:.2f} seconds')
else:
self.__tk_pbar_value.set(self.__tk_pbar_value.get() + amount)
self.__schedule_update()
if __name__ == '__main__':
def worker(pbar, t):
print('Starting worker')
while True:
for i in pbar.range(1000):
2024-07-03 00:42:21 +02:00
print(i)
time.sleep(t)
time.sleep(1)
pbar.reset()
time.sleep(1)
print("Finished worker" + str(t))
root = tk.Tk()
root.minsize(width=800, height=600)
2024-07-03 00:42:21 +02:00
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)
bar1.grid(row=0, column=0)
bar2.grid(row=1, column=0)
bar3.grid(row=2, column=0)
# worker(bar)
def start_worker():
threading.Thread(target=worker, args=(bar1, 0.10), daemon=True).start()
threading.Thread(target=worker, args=(bar2, 10.15), daemon=True).start()
threading.Thread(target=worker, args=(bar3, 0.05), daemon=True).start()
print("Worker threads started")
root.after(0, start_worker)
root.focus_set()
root.mainloop()