204 lines
6.1 KiB
Python
204 lines
6.1 KiB
Python
"""
|
|
|
|
Contains the `Timer` class.
|
|
Timer objects are created by [set_interval][textual.message_pump.MessagePump.set_interval] or
|
|
[set_timer][textual.message_pump.MessagePump.set_timer].
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import weakref
|
|
from asyncio import CancelledError, Event, Task, create_task, gather
|
|
from typing import Any, Awaitable, Callable, Iterable, Union
|
|
|
|
from rich.repr import Result, rich_repr
|
|
|
|
from textual import _time, events
|
|
from textual._callback import invoke
|
|
from textual._compat import cached_property
|
|
from textual._context import active_app
|
|
from textual._time import sleep
|
|
from textual._types import MessageTarget
|
|
|
|
TimerCallback = Union[Callable[[], Awaitable[Any]], Callable[[], Any]]
|
|
"""Type of valid callbacks to be used with timers."""
|
|
|
|
|
|
class EventTargetGone(Exception):
|
|
"""Raised if the timer event target has been deleted prior to the timer event being sent."""
|
|
|
|
|
|
@rich_repr
|
|
class Timer:
|
|
"""A class to send timer-based events.
|
|
|
|
Args:
|
|
event_target: The object which will receive the timer events.
|
|
interval: The time between timer events, in seconds.
|
|
name: A name to assign the event (for debugging).
|
|
callback: An optional callback to invoke when the event is handled.
|
|
repeat: The number of times to repeat the timer, or None to repeat forever.
|
|
skip: Enable skipping of scheduled events that couldn't be sent in time.
|
|
pause: Start the timer paused.
|
|
"""
|
|
|
|
_timer_count: int = 1
|
|
|
|
def __init__(
|
|
self,
|
|
event_target: MessageTarget,
|
|
interval: float,
|
|
*,
|
|
name: str | None = None,
|
|
callback: TimerCallback | None = None,
|
|
repeat: int | None = None,
|
|
skip: bool = True,
|
|
pause: bool = False,
|
|
) -> None:
|
|
self._target_repr = repr(event_target)
|
|
self._target = weakref.ref(event_target)
|
|
self._interval = interval
|
|
self.name = f"Timer#{self._timer_count}" if name is None else name
|
|
self._timer_count += 1
|
|
self._callback = callback
|
|
self._repeat = repeat
|
|
self._skip = skip
|
|
self._task: Task | None = None
|
|
self._reset: bool = False
|
|
self._original_pause = pause
|
|
|
|
@cached_property
|
|
def _active(self) -> Event:
|
|
event = Event()
|
|
if not self._original_pause:
|
|
event.set()
|
|
return event
|
|
|
|
def __rich_repr__(self) -> Result:
|
|
yield self._interval
|
|
yield "name", self.name
|
|
yield "repeat", self._repeat, None
|
|
|
|
@property
|
|
def target(self) -> MessageTarget:
|
|
target = self._target()
|
|
if target is None:
|
|
raise EventTargetGone()
|
|
return target
|
|
|
|
def _start(self) -> None:
|
|
"""Start the timer."""
|
|
self._task = create_task(self._run_timer(), name=self.name)
|
|
|
|
def stop(self) -> None:
|
|
"""Stop the timer."""
|
|
if self._task is None:
|
|
return
|
|
|
|
self._active.set()
|
|
self._task.cancel()
|
|
self._task = None
|
|
|
|
@classmethod
|
|
async def _stop_all(cls, timers: Iterable[Timer]) -> None:
|
|
"""Stop a number of timers, and await their completion.
|
|
|
|
Args:
|
|
timers: A number of timers.
|
|
"""
|
|
|
|
async def stop_timer(timer: Timer) -> None:
|
|
"""Stop a timer and wait for it to finish.
|
|
|
|
Args:
|
|
timer: A Timer instance.
|
|
"""
|
|
if timer._task is not None:
|
|
timer._active.set()
|
|
timer._task.cancel()
|
|
try:
|
|
await timer._task
|
|
except CancelledError:
|
|
pass
|
|
timer._task = None
|
|
|
|
await gather(*[stop_timer(timer) for timer in list(timers)])
|
|
|
|
def pause(self) -> None:
|
|
"""Pause the timer.
|
|
|
|
A paused timer will not send events until it is resumed.
|
|
"""
|
|
self._active.clear()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the timer, so it starts from the beginning."""
|
|
self._active.set()
|
|
self._reset = True
|
|
|
|
def resume(self) -> None:
|
|
"""Resume a paused timer."""
|
|
self._active.set()
|
|
|
|
async def _run_timer(self) -> None:
|
|
"""Run the timer task."""
|
|
try:
|
|
await self._run()
|
|
except CancelledError:
|
|
pass
|
|
|
|
async def _run(self) -> None:
|
|
"""Run the timer."""
|
|
count = 0
|
|
_repeat = self._repeat
|
|
_interval = self._interval
|
|
self._active # Force instantiation in same thread
|
|
await self._active.wait()
|
|
start = _time.get_time()
|
|
|
|
while _repeat is None or count <= _repeat:
|
|
next_timer = start + ((count + 1) * _interval)
|
|
now = _time.get_time()
|
|
if self._skip and next_timer < now:
|
|
count = int((now - start) / _interval + 1)
|
|
continue
|
|
now = _time.get_time()
|
|
wait_time = max(0, next_timer - now)
|
|
await sleep(wait_time)
|
|
count += 1
|
|
await self._active.wait()
|
|
if self._reset:
|
|
start = _time.get_time()
|
|
count = 0
|
|
self._reset = False
|
|
continue
|
|
try:
|
|
await self._tick(next_timer=next_timer, count=count)
|
|
except EventTargetGone:
|
|
break
|
|
|
|
async def _tick(self, *, next_timer: float, count: int) -> None:
|
|
"""Triggers the Timer's action: either call its callback, or sends an event to its target"""
|
|
|
|
app = active_app.get()
|
|
if app._exit:
|
|
return
|
|
|
|
if self._callback is not None:
|
|
try:
|
|
await invoke(self._callback)
|
|
except CancelledError:
|
|
# https://github.com/Textualize/textual/pull/2895
|
|
# Re-raise CancelledErrors that would be caught by the following exception block in Python 3.7
|
|
raise
|
|
except Exception as error:
|
|
app._handle_exception(error)
|
|
else:
|
|
event = events.Timer(
|
|
timer=self,
|
|
time=next_timer,
|
|
count=count,
|
|
callback=self._callback,
|
|
)
|
|
self.target.post_message(event)
|