From 8fd0ceedba14c3f892f7963df3b4bccb509eed8e Mon Sep 17 00:00:00 2001 From: Abraham Toriz Date: Wed, 18 May 2022 21:17:58 +0800 Subject: [PATCH 1/9] Add exception to libtmux.exc --- libtmux/exc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libtmux/exc.py b/libtmux/exc.py index 541bcbca2..e76c41263 100644 --- a/libtmux/exc.py +++ b/libtmux/exc.py @@ -49,3 +49,8 @@ class InvalidOption(OptionError): class AmbiguousOption(OptionError): """Option that could potentially match more than one.""" + + +class WaitTimeout(LibTmuxException): + + """Function timed out without meeting condition""" From 036642ebb4aa83c560fb6ca6acc2bc870d165b53 Mon Sep 17 00:00:00 2001 From: Abraham Toriz Date: Tue, 17 May 2022 10:35:14 +0800 Subject: [PATCH 2/9] Add retry_until() to take a callable and enforce the timeout --- libtmux/test.py | 49 ++++++++++++++++++++++++++++++++++++++++++ tests/test_test.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/test_test.py diff --git a/libtmux/test.py b/libtmux/test.py index 73f10eb83..ab67accc2 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -5,10 +5,13 @@ import tempfile import time +from .exc import WaitTimeout + logger = logging.getLogger(__name__) TEST_SESSION_PREFIX = "libtmux_" RETRY_TIMEOUT_SECONDS = int(os.getenv("RETRY_TIMEOUT_SECONDS", 8)) +RETRY_INTERVAL_SECONDS = float(os.getenv("RETRY_INTERVAL_SECONDS", 0.05)) namer = tempfile._RandomNameSequence() current_dir = os.path.abspath(os.path.dirname(__file__)) @@ -43,6 +46,52 @@ def retry(seconds=RETRY_TIMEOUT_SECONDS): return (lambda: time.time() < time.time() + seconds)() +def retry_until( + fun, + seconds=RETRY_TIMEOUT_SECONDS, + *, + interval=RETRY_INTERVAL_SECONDS, + raises=True, +): + """ + Retry a function until a condition meets or the specified time passes. + + Parameters + ---------- + fun : callable + A function that will be called repeatedly until it returns ``True`` or + the specified time passes. + seconds : int + Seconds to retry. Defaults to ``8``, which is configurable via + ``RETRY_TIMEOUT_SECONDS`` environment variables. + interval : float + Time in seconds to wait between calls. Defaults to ``0.05`` and is + configurable via ``RETRY_INTERVAL_SECONDS`` environment variable. + raises : bool + Wether or not to raise an exception on timeout. Defaults to ``True``. + + Examples + -------- + + >>> def f(): + ... p = w.attached_pane + ... p.server._update_panes() + ... return p.current_path == pane_path + ... + ... retry(f) + """ + ini = time.time() + + while not fun(): + end = time.time() + if end - ini >= seconds: + if raises: + raise WaitTimeout() + else: + break + time.sleep(interval) + + def get_test_session_name(server, prefix=TEST_SESSION_PREFIX): """ Faker to create a session name that doesn't exist. diff --git a/tests/test_test.py b/tests/test_test.py new file mode 100644 index 000000000..559e2e76a --- /dev/null +++ b/tests/test_test.py @@ -0,0 +1,53 @@ +from time import time + +import pytest + +from libtmux.test import WaitTimeout, retry_until + + +def test_retry_three_times(): + ini = time() + value = 0 + + def call_me_three_times(): + nonlocal value + + if value == 2: + return True + + value += 1 + + return False + + retry_until(call_me_three_times, 1) + + end = time() + + assert abs((end - ini) - 0.1) < 0.01 + + +def test_function_times_out(): + ini = time() + + def never_true(): + return False + + with pytest.raises(WaitTimeout): + retry_until(never_true, 1) + + end = time() + + assert abs((end - ini) - 1.0) < 0.01 + + +def test_function_times_out_no_rise(): + ini = time() + + def never_true(): + return False + + retry_until(never_true, 1, raises=False) + + end = time() + + assert abs((end - ini) - 1.0) < 0.01 From a8c8ca37a4500d0213dfffdd762ab40fc33e9d78 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 04:40:31 -0500 Subject: [PATCH 3/9] test(retry_until): Return truthy when True, False with timeout raise=False --- libtmux/test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libtmux/test.py b/libtmux/test.py index ab67accc2..81328728f 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -79,6 +79,10 @@ def retry_until( ... return p.current_path == pane_path ... ... retry(f) + + In pytest: + + >>> assert retry(f, raises=False) """ ini = time.time() @@ -88,8 +92,9 @@ def retry_until( if raises: raise WaitTimeout() else: - break + return False time.sleep(interval) + return True def get_test_session_name(server, prefix=TEST_SESSION_PREFIX): From eb9742f2e81e48d08e4d6c81436f600463fa2702 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:02:55 -0500 Subject: [PATCH 4/9] tests: Test retry_until with assert statements raises=False --- tests/test_test.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_test.py b/tests/test_test.py index 559e2e76a..e98bd8771 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -51,3 +51,37 @@ def never_true(): end = time() assert abs((end - ini) - 1.0) < 0.01 + + +def test_function_times_out_no_raise_assert(): + ini = time() + + def never_true(): + return False + + assert not retry_until(never_true, 1, raises=False) + + end = time() + + assert abs((end - ini) - 1.0) < 0.01 + + +def test_retry_three_times_no_raise_assert(): + ini = time() + value = 0 + + def call_me_three_times(): + nonlocal value + + if value == 2: + return True + + value += 1 + + return False + + assert retry_until(call_me_three_times, 1, raises=False) + + end = time() + + assert abs((end - ini) - 0.1) < 0.01 From 1a37c31720cd78ba06a1602c2e7eb274402c9df9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:03:31 -0500 Subject: [PATCH 5/9] chore(retry): Type annotation --- libtmux/test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libtmux/test.py b/libtmux/test.py index 81328728f..b83102bf6 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -4,6 +4,7 @@ import os import tempfile import time +from typing import Optional from .exc import WaitTimeout @@ -19,13 +20,13 @@ fixtures_dir = os.path.realpath(os.path.join(current_dir, "fixtures")) -def retry(seconds=RETRY_TIMEOUT_SECONDS): +def retry(seconds: Optional[float] = RETRY_TIMEOUT_SECONDS) -> bool: """ Retry a block of code until a time limit or ``break``. Parameters ---------- - seconds : int + seconds : float Seconds to retry, defaults to ``RETRY_TIMEOUT_SECONDS``, which is configurable via environmental variables. From e3194ba5d5eb2863c9036f1fc1c5871244f78d88 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:06:14 -0500 Subject: [PATCH 6/9] chore: Add DeprecationWarning for retry() --- libtmux/test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libtmux/test.py b/libtmux/test.py index b83102bf6..9a809e6ff 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -4,6 +4,7 @@ import os import tempfile import time +import warnings from typing import Optional from .exc import WaitTimeout @@ -24,6 +25,10 @@ def retry(seconds: Optional[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 @@ -44,6 +49,10 @@ def retry(seconds: Optional[float] = RETRY_TIMEOUT_SECONDS) -> bool: ... 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)() From be119e0a26421823673f7ac0f2fcc44beacef391 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:07:11 -0500 Subject: [PATCH 7/9] chore(retry_until): Type annotation --- libtmux/test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libtmux/test.py b/libtmux/test.py index 9a809e6ff..ad5494e2b 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -5,7 +5,7 @@ import tempfile import time import warnings -from typing import Optional +from typing import Callable, Optional from .exc import WaitTimeout @@ -57,12 +57,12 @@ def retry(seconds: Optional[float] = RETRY_TIMEOUT_SECONDS) -> bool: def retry_until( - fun, - seconds=RETRY_TIMEOUT_SECONDS, + fun: Callable, + seconds: float = RETRY_TIMEOUT_SECONDS, *, - interval=RETRY_INTERVAL_SECONDS, - raises=True, -): + interval: Optional[float] = RETRY_INTERVAL_SECONDS, + raises: Optional[bool] = True, +) -> bool: """ Retry a function until a condition meets or the specified time passes. @@ -71,7 +71,7 @@ def retry_until( fun : callable A function that will be called repeatedly until it returns ``True`` or the specified time passes. - seconds : int + seconds : float Seconds to retry. Defaults to ``8``, which is configurable via ``RETRY_TIMEOUT_SECONDS`` environment variables. interval : float From 5c98cd27bfecb16cfa3aa8378beeb8ea61c70567 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:51:52 -0500 Subject: [PATCH 8/9] docs(API): Add retry_until --- docs/api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api.md b/docs/api.md index 11ab33040..8fcdc2499 100644 --- a/docs/api.md +++ b/docs/api.md @@ -170,6 +170,10 @@ versions. .. automethod:: libtmux.test.retry ``` +```{eval-rst} +.. automethod:: libtmux.test.retry_until +``` + ```{eval-rst} .. automethod:: libtmux.test.get_test_session_name ``` From 8f555e5c9837395f561fb4eaee6d8a1a6e94cd11 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 20 May 2022 05:51:37 -0500 Subject: [PATCH 9/9] docs(CHANGES): Note changes to retry --- CHANGES | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index da08e4fe8..21ef7fc33 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,7 @@ To install the unreleased libtmux version, see [developmental releases](https:// $ pip install --user --upgrade --pre libtmux ``` -## libtmux current (unreleased) +## libtmux 0.12.x (unreleased) - _Insert changes/features/fixes for next release here_ @@ -22,6 +22,11 @@ $ pip install --user --upgrade --pre libtmux - Try out sphinx-autoapi for its table of contents generation ({issue}`367`) +### Testing + +- `retry()`: Add deprecation warning. This will be removed in 0.13.x ({issue}`368`, {issue}`372`) +- New function `retry_until()`: Polls a callback function for a set period of time until it returns `True` or times out. By default it will raise {exc}`libtmux.exc.WaitTimeout`, with `raises=False` it will return `False`. Thank you @categulario! ({issue}`368`, {issue}`372`) + ## libtmux 0.11.0 (2022-03-10) ### Compatibility