Repository: Textualize/textual
Pull request: Get rid of `_Clock` and move utility time-related functions to `_time.py`.
This will close #1961.

Problem statement:
Remove clock concept
clock.py was indented to be a virtual clock to driver Textual apps. The ideas was that in testing we could create a mocked clock that would make the event system predictable.

That never happened, and I suspect the abstraction in clock.py is superfluous. Suggest we ditch it in favour of simple module to expose time related functions.

May require an investigation to see if I am correct about this.

Hints from issue discussion:

Commit message: Remove async version of _time.py::get_time.

We started by removing '_time.py::get_time' because that was the async one and then I renamed 'get_time_no_wait' to 'get_time'.

Patch to review:
diff --git a/src/textual/_animator.py b/src/textual/_animator.py
index 564f87da87..060da0c984 100644
--- a/src/textual/_animator.py
+++ b/src/textual/_animator.py
@@ -8,7 +8,7 @@
 
 from typing_extensions import Protocol, runtime_checkable
 
-from . import _clock
+from . import _time
 from ._callback import invoke
 from ._easing import DEFAULT_EASING, EASING
 from ._types import CallbackType
@@ -385,7 +385,7 @@ def _get_time(self) -> float:
         """Get the current wall clock time, via the internal Timer."""
         # N.B. We could remove this method and always call `self._timer.get_time()` internally,
         # but it's handy to have in mocking situations
-        return _clock.get_time_no_wait()
+        return _time.get_time()
 
     async def wait_for_idle(self) -> None:
         """Wait for any animations to complete."""
diff --git a/src/textual/_clock.py b/src/textual/_clock.py
deleted file mode 100644
index 408b64f873..0000000000
--- a/src/textual/_clock.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import asyncio
-
-from ._time import time
-
-"""
-A module that serves as the single source of truth for everything time-related in a Textual app.
-Having this logic centralised makes it easier to simulate time in integration tests,
-by mocking the few functions exposed by this module.
-"""
-
-
-# N.B. This class and its singleton instance have to be hidden APIs because we want to be able to mock time,
-# even for Python modules that imported functions such as `get_time` *before* we mocked this internal _Clock.
-# (so mocking public APIs such as `get_time` wouldn't affect direct references to then that were done during imports)
-class _Clock:
-    async def get_time(self) -> float:
-        return time()
-
-    def get_time_no_wait(self) -> float:
-        return time()
-
-    async def sleep(self, seconds: float) -> None:
-        await asyncio.sleep(seconds)
-
-
-_clock = _Clock()
-
-
-def get_time_no_wait() -> float:
-    """
-    Get the current wall clock time.
-
-    Returns:
-        The value (in fractional seconds) of a monotonic clock, i.e. a clock that cannot go backwards.
-    """
-    return _clock.get_time_no_wait()
-
-
-async def get_time() -> float:
-    """
-    Asynchronous version of `get_time`. Useful in situations where we want asyncio to be
-    able to "do things" elsewhere right before we fetch the time.
-
-    Returns:
-        The value (in fractional seconds) of a monotonic clock, i.e. a clock that cannot go backwards.
-    """
-    return await _clock.get_time()
-
-
-async def sleep(seconds: float) -> None:
-    """
-    Coroutine that completes after a given time (in seconds).
-
-    Args:
-        seconds: The duration we should wait for before unblocking the awaiter
-    """
-    return await _clock.sleep(seconds)
diff --git a/src/textual/_time.py b/src/textual/_time.py
index 8e71d5ea83..52306a3b0e 100644
--- a/src/textual/_time.py
+++ b/src/textual/_time.py
@@ -42,3 +42,13 @@ async def sleep(secs: float) -> None:
         sleep_for = secs - 0.0005
         if sleep_for > 0:
             await asyncio_sleep(sleep_for)
+
+
+def get_time() -> float:
+    """Get the current wall clock (monotonic) time.
+
+    Returns:
+        The value (in fractional seconds) of a monotonic clock,
+        i.e. a clock that cannot go backwards.
+    """
+    return time()
diff --git a/src/textual/driver.py b/src/textual/driver.py
index 3aee951efe..652b79f663 100644
--- a/src/textual/driver.py
+++ b/src/textual/driver.py
@@ -4,7 +4,7 @@
 from abc import ABC, abstractmethod
 from typing import TYPE_CHECKING
 
-from . import _clock, events
+from . import _time, events
 from ._types import MessageTarget
 from .events import MouseUp
 
@@ -26,7 +26,7 @@ def __init__(
         self._debug = debug
         self._size = size
         self._loop = asyncio.get_running_loop()
-        self._mouse_down_time = _clock.get_time_no_wait()
+        self._mouse_down_time = _time.get_time()
         self._down_buttons: list[int] = []
         self._last_move_event: events.MouseMove | None = None
 
diff --git a/src/textual/message.py b/src/textual/message.py
index 7b8b3befe7..5d2ddb793b 100644
--- a/src/textual/message.py
+++ b/src/textual/message.py
@@ -4,7 +4,7 @@
 
 import rich.repr
 
-from . import _clock
+from . import _time
 from ._context import active_message_pump
 from ._types import MessageTarget as MessageTarget
 from .case import camel_to_snake
@@ -34,7 +34,7 @@ class Message:
 
     def __init__(self) -> None:
         self._sender: MessageTarget | None = active_message_pump.get(None)
-        self.time: float = _clock.get_time_no_wait()
+        self.time: float = _time.get_time()
         self._forwarded = False
         self._no_default_action = False
         self._stop_propagation = False
diff --git a/src/textual/timer.py b/src/textual/timer.py
index f2e8ba25ee..85867c5d11 100644
--- a/src/textual/timer.py
+++ b/src/textual/timer.py
@@ -13,7 +13,7 @@
 
 from rich.repr import Result, rich_repr
 
-from . import _clock, events
+from . import _time, events
 from ._asyncio import create_task
 from ._callback import invoke
 from ._context import active_app
@@ -126,15 +126,15 @@ async def _run(self) -> None:
         _repeat = self._repeat
         _interval = self._interval
         await self._active.wait()
-        start = _clock.get_time_no_wait()
+        start = _time.get_time()
 
         while _repeat is None or count <= _repeat:
             next_timer = start + ((count + 1) * _interval)
-            now = await _clock.get_time()
+            now = _time.get_time()
             if self._skip and next_timer < now:
                 count += 1
                 continue
-            now = await _clock.get_time()
+            now = _time.get_time()
             wait_time = max(0, next_timer - now)
             if wait_time > 1 / 1000:
                 await sleep(wait_time)
@@ -142,7 +142,7 @@ async def _run(self) -> None:
             count += 1
             await self._active.wait()
             if self._reset:
-                start = _clock.get_time_no_wait()
+                start = _time.get_time()
                 count = 0
                 self._reset = False
                 continue


Code review comments:
diff_hunk: @@ -42,3 +42,13 @@ async def sleep(secs: float) -> None:
         sleep_for = secs - 0.0005
         if sleep_for > 0:
             await asyncio_sleep(sleep_for)
+
+
+def get_time() -> float:
path: src/textual/_time.py
text: @user1:
Could we replace this with:

```python
get_time = time
```