From 3ca0bf91aa38f271cb2131063debdf5002957a83 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:39:13 -0600 Subject: [PATCH 01/12] pyproject(ruff[lint.flake8-builtins]) Skip `random` src/libtmux/test/random.py:1:1: A005 Module `random` shadows a Python standard-library module See also: https://docs.astral.sh/ruff/settings/#lint_flake8-builtins_builtins-allowed-modules --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a6a014676..a95f12034 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,6 +197,7 @@ required-imports = [ [tool.ruff.lint.flake8-builtins] builtins-allowed-modules = [ "dataclasses", + "random", "types", ] From c3021fd08d3cbc47a957762711c701e5698d8a27 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:27:04 -0600 Subject: [PATCH 02/12] refactor(test) test.py -> test/__init__.py --- src/libtmux/{test.py => test/__init__.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/libtmux/{test.py => test/__init__.py} (99%) diff --git a/src/libtmux/test.py b/src/libtmux/test/__init__.py similarity index 99% rename from src/libtmux/test.py rename to src/libtmux/test/__init__.py index 2bbaa2e53..937c16250 100644 --- a/src/libtmux/test.py +++ b/src/libtmux/test/__init__.py @@ -10,7 +10,7 @@ import time import typing as t -from .exc import WaitTimeout +from libtmux.exc import WaitTimeout logger = logging.getLogger(__name__) From 044431a3ee3027d2de1ba4fe175157dbafaf6f83 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:29:07 -0600 Subject: [PATCH 03/12] refactor! EnvironmentVarGuard to `tests/environment.py` --- src/libtmux/test/__init__.py | 54 ------------------------- src/libtmux/test/environment.py | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 src/libtmux/test/environment.py diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 937c16250..1797ebefe 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -300,57 +300,3 @@ def temp_window( if len(session.windows.filter(window_id=window_id)) > 0: window.kill() return - - -class EnvironmentVarGuard: - """Mock environmental variables safely. - - Helps rotect the environment variable properly. Can be used as context - manager. - - Notes - ----- - Vendorized to fix issue with Anaconda Python 2 not including test module, - see #121 [1]_ - - References - ---------- - .. [1] Just installed, "ImportError: cannot import name test_support". - GitHub issue for tmuxp. https://github.com/tmux-python/tmuxp/issues/121. - Created October 12th, 2015. Accessed April 7th, 2018. - """ - - def __init__(self) -> None: - self._environ = os.environ - self._unset: set[str] = set() - self._reset: dict[str, str] = {} - - def set(self, envvar: str, value: str) -> None: - """Set environment variable.""" - if envvar not in self._environ: - self._unset.add(envvar) - else: - self._reset[envvar] = self._environ[envvar] - self._environ[envvar] = value - - def unset(self, envvar: str) -> None: - """Unset environment variable.""" - if envvar in self._environ: - self._reset[envvar] = self._environ[envvar] - del self._environ[envvar] - - def __enter__(self) -> Self: - """Return context for for context manager.""" - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - """Cleanup to run after context manager finishes.""" - for envvar, value in self._reset.items(): - self._environ[envvar] = value - for unset in self._unset: - del self._environ[unset] diff --git a/src/libtmux/test/environment.py b/src/libtmux/test/environment.py new file mode 100644 index 000000000..9023c1f83 --- /dev/null +++ b/src/libtmux/test/environment.py @@ -0,0 +1,72 @@ +"""Helper methods for libtmux and downstream libtmux libraries.""" + +from __future__ import annotations + +import logging +import os +import typing as t + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + import sys + import types + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + + +class EnvironmentVarGuard: + """Mock environmental variables safely. + + Helps rotect the environment variable properly. Can be used as context + manager. + + Notes + ----- + Vendorized to fix issue with Anaconda Python 2 not including test module, + see #121 [1]_ + + References + ---------- + .. [1] Just installed, "ImportError: cannot import name test_support". + GitHub issue for tmuxp. https://github.com/tmux-python/tmuxp/issues/121. + Created October 12th, 2015. Accessed April 7th, 2018. + """ + + def __init__(self) -> None: + self._environ = os.environ + self._unset: set[str] = set() + self._reset: dict[str, str] = {} + + def set(self, envvar: str, value: str) -> None: + """Set environment variable.""" + if envvar not in self._environ: + self._unset.add(envvar) + else: + self._reset[envvar] = self._environ[envvar] + self._environ[envvar] = value + + def unset(self, envvar: str) -> None: + """Unset environment variable.""" + if envvar in self._environ: + self._reset[envvar] = self._environ[envvar] + del self._environ[envvar] + + def __enter__(self) -> Self: + """Return context for for context manager.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + """Cleanup to run after context manager finishes.""" + for envvar, value in self._reset.items(): + self._environ[envvar] = value + for unset in self._unset: + del self._environ[unset] From d3ea0f96ca05cacb6a92aff26f7884eef1910511 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:35:50 -0600 Subject: [PATCH 04/12] refactor!(test[constants]) Move to test.constants --- src/libtmux/pytest_plugin.py | 3 ++- src/libtmux/test/__init__.py | 10 +++++----- src/libtmux/test/constants.py | 9 +++++++++ tests/legacy_api/test_session.py | 3 ++- tests/legacy_api/test_tmuxobject.py | 3 ++- tests/test_session.py | 3 ++- tests/test_tmuxobject.py | 3 ++- 7 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 src/libtmux/test/constants.py diff --git a/src/libtmux/pytest_plugin.py b/src/libtmux/pytest_plugin.py index 320d31ca2..dcef4d9c2 100644 --- a/src/libtmux/pytest_plugin.py +++ b/src/libtmux/pytest_plugin.py @@ -13,7 +13,8 @@ from libtmux import exc from libtmux.server import Server -from libtmux.test import TEST_SESSION_PREFIX, get_test_session_name, namer +from libtmux.test import get_test_session_name, namer +from libtmux.test.constants import TEST_SESSION_PREFIX if t.TYPE_CHECKING: import pathlib diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 1797ebefe..fd40e9e0c 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -11,6 +11,11 @@ import typing as t from libtmux.exc import WaitTimeout +from libtmux.test.constants import ( + RETRY_INTERVAL_SECONDS, + RETRY_TIMEOUT_SECONDS, + TEST_SESSION_PREFIX, +) logger = logging.getLogger(__name__) @@ -29,11 +34,6 @@ from typing_extensions import Self -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)) - - class RandomStrSequence: """Factory to generate random string.""" diff --git a/src/libtmux/test/constants.py b/src/libtmux/test/constants.py new file mode 100644 index 000000000..63d644da3 --- /dev/null +++ b/src/libtmux/test/constants.py @@ -0,0 +1,9 @@ +"""Constants for libtmux test helpers.""" + +from __future__ import annotations + +import os + +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)) diff --git a/tests/legacy_api/test_session.py b/tests/legacy_api/test_session.py index ffe115ae2..770c77255 100644 --- a/tests/legacy_api/test_session.py +++ b/tests/legacy_api/test_session.py @@ -12,7 +12,8 @@ from libtmux.common import has_gte_version, has_lt_version from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import TEST_SESSION_PREFIX, namer +from libtmux.test import namer +from libtmux.test.constants import TEST_SESSION_PREFIX from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/legacy_api/test_tmuxobject.py b/tests/legacy_api/test_tmuxobject.py index 790b8cebb..c1cb2fdaa 100644 --- a/tests/legacy_api/test_tmuxobject.py +++ b/tests/legacy_api/test_tmuxobject.py @@ -7,7 +7,8 @@ from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import TEST_SESSION_PREFIX, namer +from libtmux.test import namer +from libtmux.test.constants import TEST_SESSION_PREFIX from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/test_session.py b/tests/test_session.py index 33b82dd72..c88686f5e 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -13,7 +13,8 @@ from libtmux.constants import WindowDirection from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import TEST_SESSION_PREFIX, namer +from libtmux.test import namer +from libtmux.test.constants import TEST_SESSION_PREFIX from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/test_tmuxobject.py b/tests/test_tmuxobject.py index 5c77e29e4..a72b97fb9 100644 --- a/tests/test_tmuxobject.py +++ b/tests/test_tmuxobject.py @@ -7,7 +7,8 @@ from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import TEST_SESSION_PREFIX, namer +from libtmux.test import namer +from libtmux.test.constants import TEST_SESSION_PREFIX from libtmux.window import Window if t.TYPE_CHECKING: From e07ebf99baa734cf133b9d2ab70b9e0f3c7d68cd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:38:13 -0600 Subject: [PATCH 05/12] refactor!(test[random]) Move helpers to test.random --- src/libtmux/pytest_plugin.py | 3 +- src/libtmux/test/__init__.py | 31 ++----------------- src/libtmux/test/random.py | 46 +++++++++++++++++++++++++++++ tests/legacy_api/test_session.py | 2 +- tests/legacy_api/test_tmuxobject.py | 2 +- tests/test_session.py | 2 +- tests/test_tmuxobject.py | 2 +- 7 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 src/libtmux/test/random.py diff --git a/src/libtmux/pytest_plugin.py b/src/libtmux/pytest_plugin.py index dcef4d9c2..2d5c11704 100644 --- a/src/libtmux/pytest_plugin.py +++ b/src/libtmux/pytest_plugin.py @@ -13,8 +13,9 @@ from libtmux import exc from libtmux.server import Server -from libtmux.test import get_test_session_name, namer +from libtmux.test import get_test_session_name from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.random import namer if t.TYPE_CHECKING: import pathlib diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index fd40e9e0c..52070760a 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -17,6 +17,8 @@ TEST_SESSION_PREFIX, ) +from .random import namer + logger = logging.getLogger(__name__) if t.TYPE_CHECKING: @@ -34,35 +36,6 @@ from typing_extensions import Self -class RandomStrSequence: - """Factory to generate random string.""" - - def __init__( - self, - characters: str = "abcdefghijklmnopqrstuvwxyz0123456789_", - ) -> None: - """Create a random letter / number generator. 8 chars in length. - - >>> rng = RandomStrSequence() - >>> next(rng) - '...' - >>> len(next(rng)) - 8 - >>> type(next(rng)) - - """ - self.characters: str = characters - - def __iter__(self) -> RandomStrSequence: - """Return self.""" - return self - - def __next__(self) -> str: - """Return next random string.""" - return "".join(random.sample(self.characters, k=8)) - - -namer = RandomStrSequence() current_dir = pathlib.Path(__file__) example_dir = current_dir.parent / "examples" fixtures_dir = current_dir / "fixtures" diff --git a/src/libtmux/test/random.py b/src/libtmux/test/random.py new file mode 100644 index 000000000..e874bd3c7 --- /dev/null +++ b/src/libtmux/test/random.py @@ -0,0 +1,46 @@ +"""Random helpers for libtmux and downstream libtmux libraries.""" + +from __future__ import annotations + +import logging +import random +import typing as t + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 11): + pass + + +class RandomStrSequence: + """Factory to generate random string.""" + + def __init__( + self, + characters: str = "abcdefghijklmnopqrstuvwxyz0123456789_", + ) -> None: + """Create a random letter / number generator. 8 chars in length. + + >>> rng = RandomStrSequence() + >>> next(rng) + '...' + >>> len(next(rng)) + 8 + >>> type(next(rng)) + + """ + self.characters: str = characters + + def __iter__(self) -> RandomStrSequence: + """Return self.""" + return self + + def __next__(self) -> str: + """Return next random string.""" + return "".join(random.sample(self.characters, k=8)) + + +namer = RandomStrSequence() diff --git a/tests/legacy_api/test_session.py b/tests/legacy_api/test_session.py index 770c77255..c756999ea 100644 --- a/tests/legacy_api/test_session.py +++ b/tests/legacy_api/test_session.py @@ -12,8 +12,8 @@ from libtmux.common import has_gte_version, has_lt_version from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import namer from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.random import namer from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/legacy_api/test_tmuxobject.py b/tests/legacy_api/test_tmuxobject.py index c1cb2fdaa..dc023d24b 100644 --- a/tests/legacy_api/test_tmuxobject.py +++ b/tests/legacy_api/test_tmuxobject.py @@ -7,8 +7,8 @@ from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import namer from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.random import namer from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/test_session.py b/tests/test_session.py index c88686f5e..88d5f79e6 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -13,8 +13,8 @@ from libtmux.constants import WindowDirection from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import namer from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.random import namer from libtmux.window import Window if t.TYPE_CHECKING: diff --git a/tests/test_tmuxobject.py b/tests/test_tmuxobject.py index a72b97fb9..e8c8be9ec 100644 --- a/tests/test_tmuxobject.py +++ b/tests/test_tmuxobject.py @@ -7,8 +7,8 @@ from libtmux.pane import Pane from libtmux.session import Session -from libtmux.test import namer from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.random import namer from libtmux.window import Window if t.TYPE_CHECKING: From 5e87bc094f5d378eb7d2741be9f563c4305f7dc4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:43:48 -0600 Subject: [PATCH 06/12] refactor!(test[random]) Move more helpers to test.random --- src/libtmux/pytest_plugin.py | 3 +- src/libtmux/test/__init__.py | 72 ----------------------------- src/libtmux/test/random.py | 88 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 74 deletions(-) diff --git a/src/libtmux/pytest_plugin.py b/src/libtmux/pytest_plugin.py index 2d5c11704..92da4676d 100644 --- a/src/libtmux/pytest_plugin.py +++ b/src/libtmux/pytest_plugin.py @@ -13,9 +13,8 @@ from libtmux import exc from libtmux.server import Server -from libtmux.test import get_test_session_name from libtmux.test.constants import TEST_SESSION_PREFIX -from libtmux.test.random import namer +from libtmux.test.random import get_test_session_name, namer if t.TYPE_CHECKING: import pathlib diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 52070760a..52f72cbbd 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -90,78 +90,6 @@ def retry_until( return True -def get_test_session_name(server: Server, prefix: str = TEST_SESSION_PREFIX) -> str: - """ - Faker to create a session name that doesn't exist. - - Parameters - ---------- - server : :class:`libtmux.Server` - libtmux server - prefix : str - prefix for sessions (e.g. ``libtmux_``). Defaults to - ``TEST_SESSION_PREFIX``. - - Returns - ------- - str - Random session name guaranteed to not collide with current ones. - - Examples - -------- - >>> get_test_session_name(server=server) - 'libtmux_...' - - Never the same twice: - >>> get_test_session_name(server=server) != get_test_session_name(server=server) - True - """ - while True: - session_name = prefix + next(namer) - if not server.has_session(session_name): - break - return session_name - - -def get_test_window_name( - session: Session, - prefix: str | None = TEST_SESSION_PREFIX, -) -> str: - """ - Faker to create a window name that doesn't exist. - - Parameters - ---------- - session : :class:`libtmux.Session` - libtmux session - prefix : str - prefix for windows (e.g. ``libtmux_``). Defaults to - ``TEST_SESSION_PREFIX``. - - ATM we reuse the test session prefix here. - - Returns - ------- - str - Random window name guaranteed to not collide with current ones. - - Examples - -------- - >>> get_test_window_name(session=session) - 'libtmux_...' - - Never the same twice: - >>> get_test_window_name(session=session) != get_test_window_name(session=session) - True - """ - assert prefix is not None - while True: - window_name = prefix + next(namer) - if len(session.windows.filter(window_name=window_name)) == 0: - break - return window_name - - @contextlib.contextmanager def temp_session( server: Server, diff --git a/src/libtmux/test/random.py b/src/libtmux/test/random.py index e874bd3c7..abcb95bce 100644 --- a/src/libtmux/test/random.py +++ b/src/libtmux/test/random.py @@ -6,6 +6,22 @@ import random import typing as t +from libtmux.test.constants import ( + TEST_SESSION_PREFIX, +) + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + import sys + + from libtmux.server import Server + from libtmux.session import Session + + if sys.version_info >= (3, 11): + pass + + logger = logging.getLogger(__name__) if t.TYPE_CHECKING: @@ -44,3 +60,75 @@ def __next__(self) -> str: namer = RandomStrSequence() + + +def get_test_session_name(server: Server, prefix: str = TEST_SESSION_PREFIX) -> str: + """ + Faker to create a session name that doesn't exist. + + Parameters + ---------- + server : :class:`libtmux.Server` + libtmux server + prefix : str + prefix for sessions (e.g. ``libtmux_``). Defaults to + ``TEST_SESSION_PREFIX``. + + Returns + ------- + str + Random session name guaranteed to not collide with current ones. + + Examples + -------- + >>> get_test_session_name(server=server) + 'libtmux_...' + + Never the same twice: + >>> get_test_session_name(server=server) != get_test_session_name(server=server) + True + """ + while True: + session_name = prefix + next(namer) + if not server.has_session(session_name): + break + return session_name + + +def get_test_window_name( + session: Session, + prefix: str | None = TEST_SESSION_PREFIX, +) -> str: + """ + Faker to create a window name that doesn't exist. + + Parameters + ---------- + session : :class:`libtmux.Session` + libtmux session + prefix : str + prefix for windows (e.g. ``libtmux_``). Defaults to + ``TEST_SESSION_PREFIX``. + + ATM we reuse the test session prefix here. + + Returns + ------- + str + Random window name guaranteed to not collide with current ones. + + Examples + -------- + >>> get_test_window_name(session=session) + 'libtmux_...' + + Never the same twice: + >>> get_test_window_name(session=session) != get_test_window_name(session=session) + True + """ + assert prefix is not None + while True: + window_name = prefix + next(namer) + if len(session.windows.filter(window_name=window_name)) == 0: + break + return window_name From 8751eb283df321c78c20446751fe99a8a07ee3ea Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:46:15 -0600 Subject: [PATCH 07/12] refactor!(test[temporary]) Move from `test` to `test.temporary` --- src/libtmux/test/__init__.py | 113 ---------------------------- src/libtmux/test/temporary.py | 135 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 113 deletions(-) create mode 100644 src/libtmux/test/temporary.py diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 52f72cbbd..006b2f579 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -88,116 +88,3 @@ def retry_until( return False time.sleep(interval) return True - - -@contextlib.contextmanager -def temp_session( - server: Server, - *args: t.Any, - **kwargs: t.Any, -) -> Generator[Session, t.Any, t.Any]: - """ - Return a context manager with a temporary session. - - If no ``session_name`` is entered, :func:`get_test_session_name` will make - an unused session name. - - The session will destroy itself upon closing with :meth:`Session.session()`. - - Parameters - ---------- - server : :class:`libtmux.Server` - - Other Parameters - ---------------- - args : list - Arguments passed into :meth:`Server.new_session` - kwargs : dict - Keyword arguments passed into :meth:`Server.new_session` - - Yields - ------ - :class:`libtmux.Session` - Temporary session - - Examples - -------- - >>> with temp_session(server) as session: - ... session.new_window(window_name='my window') - Window(@3 2:my window, Session($... ...)) - """ - if "session_name" in kwargs: - session_name = kwargs.pop("session_name") - else: - session_name = get_test_session_name(server) - - session = server.new_session(session_name, *args, **kwargs) - - try: - yield session - finally: - if server.has_session(session_name): - session.kill() - return - - -@contextlib.contextmanager -def temp_window( - session: Session, - *args: t.Any, - **kwargs: t.Any, -) -> Generator[Window, t.Any, t.Any]: - """ - Return a context manager with a temporary window. - - The window will destroy itself upon closing with :meth:`window. - kill()`. - - If no ``window_name`` is entered, :func:`get_test_window_name` will make - an unused window name. - - Parameters - ---------- - session : :class:`libtmux.Session` - - Other Parameters - ---------------- - args : list - Arguments passed into :meth:`Session.new_window` - kwargs : dict - Keyword arguments passed into :meth:`Session.new_window` - - Yields - ------ - :class:`libtmux.Window` - temporary window - - Examples - -------- - >>> with temp_window(session) as window: - ... window - Window(@2 2:... Session($1 libtmux_...)) - - - >>> with temp_window(session) as window: - ... window.split() - Pane(%4 Window(@3 2:libtmux_..., Session($1 libtmux_...))) - """ - if "window_name" not in kwargs: - window_name = get_test_window_name(session) - else: - window_name = kwargs.pop("window_name") - - window = session.new_window(window_name, *args, **kwargs) - - # Get ``window_id`` before returning it, it may be killed within context. - window_id = window.window_id - assert window_id is not None - assert isinstance(window_id, str) - - try: - yield window - finally: - if len(session.windows.filter(window_id=window_id)) > 0: - window.kill() - return diff --git a/src/libtmux/test/temporary.py b/src/libtmux/test/temporary.py new file mode 100644 index 000000000..cc2106edd --- /dev/null +++ b/src/libtmux/test/temporary.py @@ -0,0 +1,135 @@ +"""Temporary object helpers for libtmux and downstream libtmux libraries.""" + +from __future__ import annotations + +import contextlib +import logging +import typing as t + +from libtmux.test.random import get_test_session_name, get_test_window_name + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + import sys + from collections.abc import Generator + + from libtmux.server import Server + from libtmux.session import Session + from libtmux.window import Window + + if sys.version_info >= (3, 11): + pass + + +@contextlib.contextmanager +def temp_session( + server: Server, + *args: t.Any, + **kwargs: t.Any, +) -> Generator[Session, t.Any, t.Any]: + """ + Return a context manager with a temporary session. + + If no ``session_name`` is entered, :func:`get_test_session_name` will make + an unused session name. + + The session will destroy itself upon closing with :meth:`Session.session()`. + + Parameters + ---------- + server : :class:`libtmux.Server` + + Other Parameters + ---------------- + args : list + Arguments passed into :meth:`Server.new_session` + kwargs : dict + Keyword arguments passed into :meth:`Server.new_session` + + Yields + ------ + :class:`libtmux.Session` + Temporary session + + Examples + -------- + >>> with temp_session(server) as session: + ... session.new_window(window_name='my window') + Window(@3 2:my window, Session($... ...)) + """ + if "session_name" in kwargs: + session_name = kwargs.pop("session_name") + else: + session_name = get_test_session_name(server) + + session = server.new_session(session_name, *args, **kwargs) + + try: + yield session + finally: + if server.has_session(session_name): + session.kill() + return + + +@contextlib.contextmanager +def temp_window( + session: Session, + *args: t.Any, + **kwargs: t.Any, +) -> Generator[Window, t.Any, t.Any]: + """ + Return a context manager with a temporary window. + + The window will destroy itself upon closing with :meth:`window. + kill()`. + + If no ``window_name`` is entered, :func:`get_test_window_name` will make + an unused window name. + + Parameters + ---------- + session : :class:`libtmux.Session` + + Other Parameters + ---------------- + args : list + Arguments passed into :meth:`Session.new_window` + kwargs : dict + Keyword arguments passed into :meth:`Session.new_window` + + Yields + ------ + :class:`libtmux.Window` + temporary window + + Examples + -------- + >>> with temp_window(session) as window: + ... window + Window(@2 2:... Session($1 libtmux_...)) + + + >>> with temp_window(session) as window: + ... window.split() + Pane(%4 Window(@3 2:libtmux_..., Session($1 libtmux_...))) + """ + if "window_name" not in kwargs: + window_name = get_test_window_name(session) + else: + window_name = kwargs.pop("window_name") + + window = session.new_window(window_name, *args, **kwargs) + + # Get ``window_id`` before returning it, it may be killed within context. + window_id = window.window_id + assert window_id is not None + assert isinstance(window_id, str) + + try: + yield window + finally: + if len(session.windows.filter(window_id=window_id)) > 0: + window.kill() + return From ef55d418e500a1e5c06024eeffed8ff3120539f2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:50:11 -0600 Subject: [PATCH 08/12] refactor!(test[retry]) Move from `test` to `test.retry` --- src/libtmux/test/__init__.py | 49 ------------------------- src/libtmux/test/retry.py | 71 ++++++++++++++++++++++++++++++++++++ tests/test_pane.py | 2 +- tests/test_test.py | 2 +- 4 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 src/libtmux/test/retry.py diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 006b2f579..4bc7627df 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -39,52 +39,3 @@ current_dir = pathlib.Path(__file__) example_dir = current_dir.parent / "examples" fixtures_dir = current_dir / "fixtures" - - -def retry_until( - fun: Callable[[], bool], - seconds: float = RETRY_TIMEOUT_SECONDS, - *, - interval: float = RETRY_INTERVAL_SECONDS, - raises: bool | None = True, -) -> bool: - """ - 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 : float - 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 - Whether or not to raise an exception on timeout. Defaults to ``True``. - - Examples - -------- - >>> def fn(): - ... p = session.active_window.active_pane - ... return p.pane_current_path is not None - - >>> retry_until(fn) - True - - In pytest: - - >>> assert retry_until(fn, raises=False) - """ - ini = time.time() - - while not fun(): - end = time.time() - if end - ini >= seconds: - if raises: - raise WaitTimeout - return False - time.sleep(interval) - return True diff --git a/src/libtmux/test/retry.py b/src/libtmux/test/retry.py new file mode 100644 index 000000000..1f989e73a --- /dev/null +++ b/src/libtmux/test/retry.py @@ -0,0 +1,71 @@ +"""Retry helpers for libtmux and downstream libtmux libraries.""" + +from __future__ import annotations + +import logging +import time +import typing as t + +from libtmux.exc import WaitTimeout +from libtmux.test.constants import ( + RETRY_INTERVAL_SECONDS, + RETRY_TIMEOUT_SECONDS, +) + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + import sys + from collections.abc import Callable + + if sys.version_info >= (3, 11): + pass + + +def retry_until( + fun: Callable[[], bool], + seconds: float = RETRY_TIMEOUT_SECONDS, + *, + interval: float = RETRY_INTERVAL_SECONDS, + raises: bool | None = True, +) -> bool: + """ + 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 : float + 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 + Whether or not to raise an exception on timeout. Defaults to ``True``. + + Examples + -------- + >>> def fn(): + ... p = session.active_window.active_pane + ... return p.pane_current_path is not None + + >>> retry_until(fn) + True + + In pytest: + + >>> assert retry_until(fn, raises=False) + """ + ini = time.time() + + while not fun(): + end = time.time() + if end - ini >= seconds: + if raises: + raise WaitTimeout + return False + time.sleep(interval) + return True diff --git a/tests/test_pane.py b/tests/test_pane.py index 21d0cbb87..746467851 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -10,7 +10,7 @@ from libtmux.common import has_gte_version, has_lt_version, has_lte_version from libtmux.constants import PaneDirection, ResizeAdjustmentDirection -from libtmux.test import retry_until +from libtmux.test.retry import retry_until if t.TYPE_CHECKING: from libtmux.session import Session diff --git a/tests/test_test.py b/tests/test_test.py index a9471363c..36e35930d 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -7,7 +7,7 @@ import pytest from libtmux.exc import WaitTimeout -from libtmux.test import retry_until +from libtmux.test.retry import retry_until def test_retry_three_times() -> None: From 75502951fcb135fe43bb93a26d19bc77a658cd90 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:54:35 -0600 Subject: [PATCH 09/12] docs(CHANGES) Note move of test helpers --- CHANGES | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGES b/CHANGES index eb74dc809..b1c4851d0 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,41 @@ $ pip install --user --upgrade --pre libtmux - _Future release notes will be placed here_ +### Breaking Changes + +#### Test helpers: Refactor + +Test helper functionality has been split into focused modules (#XXX): + +- `libtmux.test` module split into: + - `libtmux.test.constants`: Test-related constants (`TEST_SESSION_PREFIX`, etc.) + - `libtmux.test.environment`: Environment variable mocking + - `libtmux.test.random`: Random string generation utilities + - `libtmux.test.temporary`: Temporary session/window management + +**Breaking**: Import paths have changed. Update imports: + +```python +# Old (0.45.x and earlier) +from libtmux.test import ( + TEST_SESSION_PREFIX, + get_test_session_name, + get_test_window_name, + namer, + temp_session, + temp_window, + EnvironmentVarGuard, +) +``` + +```python +# New (0.46.0+) +from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.environment import EnvironmentVarGuard +from libtmux.test.random import get_test_session_name, get_test_window_name, namer +from libtmux.test.temporary import temp_session, temp_window +``` + ### Development - CI: Check for runtime dependencies (#574) From 172de1f6fce52e19545ce4e417b48f3b5c83ed61 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 12:54:42 -0600 Subject: [PATCH 10/12] docs(MIGRATION) Note move of test helpers --- MIGRATION | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/MIGRATION b/MIGRATION index fcba337e9..ee58dbce6 100644 --- a/MIGRATION +++ b/MIGRATION @@ -25,6 +25,41 @@ _Detailed migration steps for the next version will be posted here._ +## libtmux 0.45.x (Yet to be released) + +### Test helpers: Module moves + +Test helper functionality has been split into focused modules (#XXX): + +- `libtmux.test` module split into: + - `libtmux.test.constants`: Test-related constants (`TEST_SESSION_PREFIX`, etc.) + - `libtmux.test.environment`: Environment variable mocking + - `libtmux.test.random`: Random string generation utilities + - `libtmux.test.temporary`: Temporary session/window management + +**Breaking**: Import paths have changed. Update imports: + +```python +# Old (0.45.x and earlier) +from libtmux.test import ( + TEST_SESSION_PREFIX, + get_test_session_name, + get_test_window_name, + namer, + temp_session, + temp_window, + EnvironmentVarGuard, +) +``` + +```python +# New (0.46.0+) +from libtmux.test.constants import TEST_SESSION_PREFIX +from libtmux.test.environment import EnvironmentVarGuard +from libtmux.test.random import get_test_session_name, get_test_window_name, namer +from libtmux.test.temporary import temp_session, temp_window +``` + ## 0.35.0: Commands require explicit targets (2024-03-17) ### Commands require explicit targets (#535) From 4d637c5c53b073a6023583b491d629cb43d68e91 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 13:12:53 -0600 Subject: [PATCH 11/12] tests: test -> test/test_retry.py --- tests/{test_test.py => test/test_retry.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_test.py => test/test_retry.py} (100%) diff --git a/tests/test_test.py b/tests/test/test_retry.py similarity index 100% rename from tests/test_test.py rename to tests/test/test_retry.py From 6cbfe31958ac7a835f8f5067070f0e5ac13278d3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 13:17:54 -0600 Subject: [PATCH 12/12] test: Remove `example_dir`, `fixtures_dir` --- src/libtmux/test/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libtmux/test/__init__.py b/src/libtmux/test/__init__.py index 4bc7627df..8fba9fa82 100644 --- a/src/libtmux/test/__init__.py +++ b/src/libtmux/test/__init__.py @@ -34,8 +34,3 @@ from typing import Self else: from typing_extensions import Self - - -current_dir = pathlib.Path(__file__) -example_dir = current_dir.parent / "examples" -fixtures_dir = current_dir / "fixtures"