Repository: tmux-python/libtmux (language: python)

Issue:
`retry(seconds=...)` broke
Instead

```
import time
t = time.process_time()

while (time.process_time() - t) * 1000 < 2:
    # stuff happens
    time.sleep(0.1)
```

Find cross platform way to do timeout with signals

See also https://github.com/tmux-python/tmuxp/issues/620


Discussion / hints:
It turns out that this is the cause of https://github.com/tmux-python/tmuxp/issues/704#issuecomment-1128288348 . The current implementation of the `retry()` function is equivalent to `return True`.

Why is this function necessary? Maybe it can be replaced by a `sleep(some_millis)` in most cases. Seems strange needing to retry a block of code thousands of times until a condition meets. That makes me think (and taking a look at the places where it is used) that it can actually be accurately replaced by a `while` loop with the condition that breaks (which in fact would be equivalent since current implementation is a `return True`).
Ok, I just found a case where a piece of code was tried 89 times before it succeeded, which was about 300 ms.
I came up with this function:

```python
def retry(fun, secs=1):
    ini = time.time()

    while not fun():
        end = time.time()
        if end - ini >= secs:
            raise Exception('retry timed out')
```

which does the retrying thing accurately with the timeout, it is more or less clean to use (little change to the code) and reports when the retrying timed out:

```python
    def f():
        session.server._update_windows()
        return w.name == "sh"

    retry(f)

    assert w.name == "sh"
```

what do you think? Of course it can read the timeout from the environment just like the code does right now
@categulario I'm still working and won't be able to focus on this in earnest until the weekend, but here's what i have to offer (to make it easier, maybe):

```python
class WaitTimeout(Exception):
    pass


class WaitConditionCallbackProtocol(typing.Protocol):
    def __call__(self: Generic) -> bool:
        ...


def wait_for_condition(
    self,
    callback: WaitConditionCallbackProtocol,
    wait_time=0.5,
    interval=0.25,
    raises=False,
):
    if not callable(callback):
        raise TypeError("callback should be callable")
    time_spent = 0
    condition = False
    while condition:
        self.server.update_panes()
        if time_spent >= wait_time:
            if raises:
                raise WaitTimeout(f"Condition timed out after {time_spent}")
            break
        condition = callback(self)
        if condition is True:
            return True
        time.sleep(interval)
        time_spent += interval
    return False


class WaitableMixin:
    wait_for_condition = wait_for_condition


# In window.py (example):
class Window(..., WaitableMixin):
    ...

# In pane.py (example):
class Pane(..., WaitableMixin):
    ...
```

Usage:

```
>>> window.wait_for_condition(lambda w: w.window_name == "test")
raises WaitTimeout

>>> window.wait_for_condition(lambda w: w.window_name == "test", raises=False)
False

>>> window.wait_for_condition(lambda w: w.window_name == "test", timeout=5, interval=1)
After 5 seconds of checking every 1 second
```
For an instant I thought this issue was about the `libtmux.test` function only, which is used exactly 10 times in testing code.

I understand that its usage can be extended to `tmuxp` code for cases like https://github.com/tmux-python/tmuxp/issues/96 and that probably motivates this code. However I see a few shortcomings:

* If it is available only as a mixin then using it requires using it from the class that implements the mixin. It might be that implementing it in Window and Pane is enough, it might be not. To me seems restrictive for no good reason and if it ever needs to be used when no window or pane is available then it leads its users to doing some weird tricks to have the function. Function that can very easily be just a function attached to nothing. 
* I think the calculation of time spent should be done with the `time` module, just because operations (like calling the callback) take time, so for the sake of unnecessary precision I would just call `time()` as in my proposal.
* I think the function should rise by default, and be explicitly silenced. A condition that didn't meet after the given time is probably an error anyways. Knowing the reason of the error by default seems reasonable.
* the while loop never enters, but that's probably a typo.

Things that I like:

* the `WaitTimeout` error
* the check for callable argument
* sleeping a small interval between calls, although it should probably be around `.1` second or less to have a more "real-time" effect. Waiting for 250 ms and failing to meet the condition implies the user waiting for half a second, which starts to feel like lag.
@categulario Would you like to make a new PR with a wait function, using the `time` module?

if not i can get around to it in this weekend or next weekend

i may not be around in the weekdays as much due to normal work deadlines
> @categulario Would you like to make a new PR with a wait function w/o the time function, using the time module?

Sorry, I didn't understand that. Got confused between "with a wait function" and "without the time function"
@categulario I edited it. I'm just asking if you want to make PR with an example of what you meant [here](https://github.com/tmux-python/libtmux/issues/368#issuecomment-1129427034)
Just updated #372 with that

Fix patch:
diff --git a/CHANGES b/CHANGES
index d71feac48..5554ff68e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,14 @@ $ pip install --user --upgrade --pre libtmux
 
 - _Insert changes/features/fixes for next release here_
 
+## What's new
+
+- **Improved typings**
+
+  Now [`mypy --strict`] compliant ({issue}`383`)
+
+  [`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict
+
 ### Breaking changes
 
 - Deprecated individual item lookups ({issue}`390`)
@@ -61,13 +69,7 @@ $ pip install --user --upgrade --pre libtmux
     window.show_window_option('DISPLAY')
     ```
 
-## What's new
-
-- **Improved typings**
-
-  Now [`mypy --strict`] compliant ({issue}`383`)
-
-  [`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict
+- Remove `libtmux.test.retry()`, deprecated since 0.12.x ({issue}`393`)
 
 ### Development
 


Test patch:
diff --git a/libtmux/test.py b/libtmux/test.py
index bdad6e29d..4af61b11e 100644
--- a/libtmux/test.py
+++ b/libtmux/test.py
@@ -6,7 +6,6 @@
 import time
 import types
 import typing as t
-import warnings
 from typing import Callable, Optional
 
 from libtmux.server import Server
@@ -42,41 +41,6 @@ def __next__(self) -> str:
 fixtures_dir = os.path.realpath(os.path.join(current_dir, "fixtures"))
 
 
-def retry(seconds: float = RETRY_TIMEOUT_SECONDS) -> bool:
-    """
-    Retry a block of code until a time limit or ``break``.
-
-    .. deprecated:: 0.12.0
-          `retry` doesn't work, it will be removed in libtmux 0.13.0, it is replaced by
-          `retry_until`, more info: https://github.com/tmux-python/libtmux/issues/368.
-
-    Parameters
-    ----------
-    seconds : float
-        Seconds to retry, defaults to ``RETRY_TIMEOUT_SECONDS``, which is
-        configurable via environmental variables.
-
-    Returns
-    -------
-    bool
-        True if time passed since retry() invoked less than seconds param.
-
-    Examples
-    --------
-
-    >>> while retry():
-    ...     p = w.attached_pane
-    ...     p.server._update_panes()
-    ...     if p.current_path == pane_path:
-    ...         break
-    """
-    warnings.warn(
-        "retry() is being deprecated and will soon be replaced by retry_until()",
-        DeprecationWarning,
-    )
-    return (lambda: time.time() < time.time() + seconds)()
-
-
 def retry_until(
     fun: Callable[[], bool],
     seconds: float = RETRY_TIMEOUT_SECONDS,