Skip to content

Commit e40a4dc

Browse files
committed
refactors task runner
1 parent 75e0ab6 commit e40a4dc

File tree

4 files changed

+118
-41
lines changed

4 files changed

+118
-41
lines changed

ngwidgets/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.29.1"
1+
__version__ = "0.29.2"

ngwidgets/task_runner.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
import asyncio
88
import inspect
9+
import time
910
from typing import Callable, Optional
1011

1112
from basemkit.persistent_log import Log
13+
from ngwidgets.progress import Progressbar
1214
from nicegui import background_tasks
15+
from nicegui import run
1316

1417
# optional generic Progressbar tqdm or nicegui.ui
15-
from ngwidgets.progress import Progressbar
16-
17-
1818
class TaskRunner:
1919
"""
2020
A robust background task runner that supports:
@@ -29,12 +29,50 @@ def __init__(self, timeout: float = 20.0, progress: Optional[Progressbar] = None
2929
self.timeout = timeout
3030
self.progress = progress
3131
self.log = Log()
32+
self.start_time = None
33+
self.stop_time = None
34+
self.task_name = "?"
35+
36+
def set_name(self, func: Callable):
37+
"""Set task name from function"""
38+
self.task_name = getattr(func, '__name__', '?')
39+
40+
def get_status(self) -> str:
41+
"""Get formatted status string with timing information"""
42+
if not self.start_time:
43+
status = 'Ready'
44+
else:
45+
elapsed = self.get_elapsed_time()
46+
start_str = time.strftime('%H:%M:%S', time.localtime(self.start_time))
47+
48+
if self.is_running():
49+
status = f'Running "{self.task_name}" for {elapsed:.1f}s since {start_str}'
50+
elif self.stop_time:
51+
stop_str = time.strftime('%H:%M:%S', time.localtime(self.stop_time))
52+
status = f'Completed "{self.task_name}" in {elapsed:.1f}s ({start_str}-{stop_str})'
53+
else:
54+
status = f'Interrupted "{self.task_name}" after {elapsed:.1f}s from {start_str}'
55+
56+
return status
57+
58+
def get_elapsed_time(self) -> float:
59+
"""Get elapsed time in seconds since task started"""
60+
elapsed = 0.0
61+
if self.start_time:
62+
if self.is_running():
63+
elapsed = time.time() - self.start_time
64+
elif self.stop_time:
65+
elapsed = self.stop_time - self.start_time
66+
return elapsed
3267

3368
def cancel_running(self):
3469
if self.task and not self.task.done():
3570
self.task.cancel()
36-
if self.progress:
37-
self.progress.reset()
71+
if self.progress:
72+
self.progress.reset()
73+
# Reset timing when cancelled
74+
self.start_time = None
75+
self.stop_time = None
3876

3977
def is_running(self) -> bool:
4078
running = self.task and not self.task.done()
@@ -65,9 +103,12 @@ def run_blocking(self, blocking_func: Callable, *args, **kwargs):
65103
blocking_func
66104
):
67105
raise TypeError("run_blocking expects a sync function, not async.")
68-
106+
self.set_name(blocking_func)
69107
async def wrapper():
70-
await asyncio.to_thread(blocking_func, *args, **kwargs)
108+
# this would be the native asyncio call
109+
# await asyncio.to_thread(blocking_func, *args, **kwargs)
110+
# we use nicegui managed threads
111+
await run.io_bound(blocking_func, *args, **kwargs)
71112

72113
self._start(wrapper)
73114

@@ -82,6 +123,7 @@ def run_async_wrapping_blocking(self, coro_func: Callable[[], asyncio.Future]):
82123
raise TypeError(
83124
"run_async_wrapping_blocking expects an async def function."
84125
)
126+
self.set_name(coro_func)
85127
self._start(coro_func)
86128

87129
def run_async(self, coro_func: Callable[..., asyncio.Future], *args, **kwargs):
@@ -94,10 +136,12 @@ def run_async(self, coro_func: Callable[..., asyncio.Future], *args, **kwargs):
94136
"""
95137
if not inspect.iscoroutinefunction(coro_func):
96138
raise TypeError("run_async expects an async def function (not called yet).")
139+
self.set_name(coro_func)
97140
self._start(coro_func, *args, **kwargs)
98141

99142
def _start(self, coro_func: Callable[..., asyncio.Future], *args, **kwargs):
100143
self.cancel_running()
144+
self.start_time = time.time()
101145

102146
async def wrapped():
103147
try:
@@ -116,5 +160,6 @@ async def wrapped():
116160
finally:
117161
if self.progress:
118162
self.progress.update_value(self.progress.total)
163+
self.stop_time = time.time()
119164

120165
self.task = background_tasks.create(wrapped())

ngwidgets/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Version:
1818
name = "ngwidgets"
1919
version = ngwidgets.__version__
2020
date = "2023-09-10"
21-
updated = "2025-07-22"
21+
updated = "2025-07-23"
2222
description = "NiceGUI widgets"
2323

2424
authors = "Wolfgang Fahl"

ngwidgets/widgets_demo.py

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import asyncio
88
import logging
99
import random
10+
import time
1011
from dataclasses import dataclass
1112
from datetime import datetime
1213

@@ -587,52 +588,83 @@ async def show_taskrunner_demo(self):
587588
"""
588589
Demonstrate the TaskRunner with timeout, progress, and cancellation
589590
"""
590-
591591
def show():
592-
task_runner = TaskRunner(timeout=10.0)
592+
# Create progress bar for TaskRunner
593+
progress_bar = NiceguiProgressbar(total=100, desc="Task", unit="step")
594+
task_runner = TaskRunner(timeout=8.0, progress=progress_bar)
593595

594-
async def load_data():
595-
await asyncio.sleep(3) # Simulate data loading
596-
content.clear()
596+
async def hint(title:str,done:bool=False,marker:str="✅"):
597597
with content:
598-
ui.markdown("✅ Data loaded successfully!")
599-
600-
async def slow_load():
601-
await asyncio.sleep(15) # Will timeout
602-
content.clear()
598+
content.clear()
603599
with content:
604-
ui.markdown("This should timeout!")
600+
if not done:
601+
ui.spinner()
602+
marker=""
603+
ui.markdown(title+marker)
604+
605+
async def load_data_5secs():
606+
title='Loading data ... for 5 secs'
607+
await hint(title)
608+
609+
# Simulate work with progress updates
610+
for _i in range(50):
611+
await asyncio.sleep(0.1) # 5 seconds total
612+
progress_bar.update(2) # 2% per step
613+
614+
await hint(title,True)
615+
616+
async def slow_load_10secs():
617+
title='Loading 10 secs (this will timeout after 8 secs)...'
618+
await hint(title)
619+
620+
# This will timeout at 8 seconds
621+
for _i in range(100):
622+
await asyncio.sleep(0.1) # 10 seconds total - will timeout
623+
progress_bar.update(1)
624+
625+
await hint('This should timeout!',True,marker="❌")
626+
627+
def blocking_task(secs:float=5):
628+
time.sleep(secs)
629+
return f"Blocking task {secs} secs completed"
605630

606-
def blocking_task():
607-
import time
631+
async def blocking_task_5secs():
632+
title = 'Running blocking task for 5 secs'
633+
await hint(title)
634+
result = blocking_task(5)
635+
await hint(result, True)
608636

609-
time.sleep(2) # Blocking operation
610-
return "Blocking task completed"
637+
async def quick_task_1sec():
638+
title='Quick task 1sec'
639+
await hint(title)
611640

612-
async def handle_blocking():
613-
result = await run.io_bound(blocking_task)
614-
content.clear()
641+
await asyncio.sleep(1) # 1 second for quick demo
642+
await hint(title,True)
643+
644+
def cancel_task():
645+
task_runner.cancel_running()
615646
with content:
616-
ui.markdown(f"✅ {result}")
647+
content.clear()
648+
ui.markdown('⚠️ Task cancelled')
617649

618650
with ui.card() as content:
619-
ui.spinner()
620-
ui.markdown("Ready to run tasks...")
651+
ui.markdown('Ready to run tasks...')
652+
653+
# Show progress bar
654+
progress_bar.progress.visible = True
621655

622656
with ui.row():
623-
ui.button("Load Data (3s)", on_click=lambda: task_runner.run(load_data))
624-
ui.button(
625-
"Timeout Test (15s)", on_click=lambda: task_runner.run(slow_load)
626-
)
627-
ui.button(
628-
"Blocking Task", on_click=lambda: task_runner.run(handle_blocking)
629-
)
630-
ui.button("Cancel", on_click=task_runner.cancel_running)
657+
ui.button('Load Data (5s)', on_click=lambda: task_runner.run(load_data_5secs))
658+
ui.button('Quick Task (1s)', on_click=lambda: task_runner.run(quick_task_1sec))
659+
ui.button('Blocking Task (5s)', on_click=lambda: task_runner.run(blocking_task_5secs))
660+
ui.button('Timeout Test (8s+)', on_click=lambda: task_runner.run(slow_load_10secs))
661+
ui.button('Cancel', on_click=cancel_task)
631662

632663
with ui.row():
633-
ui.label().bind_text_from(
634-
task_runner, "is_running", lambda running: f"Running: {running}"
635-
)
664+
status_label = ui.label('')
665+
666+
# Update status every 100ms using timer
667+
ui.timer(0.1, lambda: status_label.set_text(task_runner.get_status()))
636668

637669
await self.setup_content_div(show)
638670

0 commit comments

Comments
 (0)