From 1df438aeab9ae5668dc5e6cc3d4f960545f1ef3f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 28 Nov 2025 16:35:50 -0600 Subject: [PATCH 01/19] ci(tests): remove tmux < 3.2 from test matrix why: Minimum supported tmux version is now 3.2a, older versions no longer need CI coverage. what: - Remove tmux 2.6, 2.7, 2.8, 3.0a, 3.1b from version matrix - Keep 3.2a through master for testing --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0421f8e4..d2a41f603 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: python-version: ['3.14'] - tmux-version: ['2.6', '2.7', '2.8', '3.0a', '3.1b', '3.2a', '3.3a', '3.4', '3.5', '3.6', 'master'] + tmux-version: ['3.2a', '3.3a', '3.4', '3.5', '3.6', 'master'] steps: - uses: actions/checkout@v5 From d35e59f25036e19a796ba46b41b1acbb05e91811 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:39:06 -0600 Subject: [PATCH 02/19] common.py(refactor): Bump TMUX_MIN_VERSION to 3.2a why: tmux versions below 3.2a are no longer supported as of libtmux 0.49.0 what: - Change TMUX_MIN_VERSION from "1.8" to "3.2a" - Remove TMUX_SOFT_MIN_VERSION constant (no longer needed) - Remove _version_deprecation_checked flag - Remove _check_deprecated_version() function - Remove deprecation warning call from get_version() - Update has_minimum_version() docstring to reflect 3.2a requirement - Update error message to mention v0.48.x backport --- src/libtmux/common.py | 46 +++------------ tests/test_common.py | 131 ------------------------------------------ 2 files changed, 7 insertions(+), 170 deletions(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 6923f0e0f..dd1fad9a6 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -24,49 +24,16 @@ #: Minimum version of tmux required to run libtmux -TMUX_MIN_VERSION = "1.8" +TMUX_MIN_VERSION = "3.2a" #: Most recent version of tmux supported TMUX_MAX_VERSION = "3.6" -#: Minimum version before deprecation warning is shown -TMUX_SOFT_MIN_VERSION = "3.2a" - SessionDict = dict[str, t.Any] WindowDict = dict[str, t.Any] WindowOptionDict = dict[str, t.Any] PaneDict = dict[str, t.Any] -#: Flag to ensure deprecation warning is only shown once per process -_version_deprecation_checked: bool = False - - -def _check_deprecated_version(version: LooseVersion) -> None: - """Check if tmux version is deprecated and warn once. - - This is called from get_version() on first invocation. - """ - global _version_deprecation_checked - if _version_deprecation_checked: - return - _version_deprecation_checked = True - - import os - import warnings - - if os.environ.get("LIBTMUX_SUPPRESS_VERSION_WARNING"): - return - - if version < LooseVersion(TMUX_SOFT_MIN_VERSION): - warnings.warn( - f"tmux {version} is deprecated and will be unsupported in a future " - f"libtmux release. Please upgrade to tmux {TMUX_SOFT_MIN_VERSION} " - "or newer. Set LIBTMUX_SUPPRESS_VERSION_WARNING=1 to suppress this " - "warning.", - FutureWarning, - stacklevel=4, - ) - class EnvironmentMixin: """Mixin for manager session and server level environment variables in tmux.""" @@ -336,9 +303,7 @@ def get_version() -> LooseVersion: version = re.sub(r"[a-z-]", "", version) - version_obj = LooseVersion(version) - _check_deprecated_version(version_obj) - return version_obj + return LooseVersion(version) def has_version(version: str) -> bool: @@ -422,7 +387,7 @@ def has_lt_version(max_version: str) -> bool: def has_minimum_version(raises: bool = True) -> bool: - """Return True if tmux meets version requirement. Version >1.8 or above. + """Return True if tmux meets version requirement. Version >= 3.2a. Parameters ---------- @@ -441,6 +406,9 @@ def has_minimum_version(raises: bool = True) -> bool: Notes ----- + .. versionchanged:: 0.49.0 + Minimum version bumped to 3.2a. For older tmux, use libtmux v0.48.x. + .. versionchanged:: 0.7.0 No longer returns version, returns True or False @@ -454,7 +422,7 @@ def has_minimum_version(raises: bool = True) -> bool: msg = ( f"libtmux only supports tmux {TMUX_MIN_VERSION} and greater. This " f"system has {get_version()} installed. Upgrade your tmux to use " - "libtmux." + "libtmux, or use libtmux v0.48.x for older tmux versions." ) raise exc.VersionTooLow(msg) return False diff --git a/tests/test_common.py b/tests/test_common.py index 57a9e4745..3aa045bc4 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -508,134 +508,3 @@ def mock_get_version() -> LooseVersion: elif check_type == "type_check": assert mock_version is not None # For type checker assert isinstance(has_version(mock_version), bool) - - -class VersionDeprecationFixture(t.NamedTuple): - """Test fixture for version deprecation warning.""" - - test_id: str - version: str - suppress_env: bool - expected_warning: bool - - -VERSION_DEPRECATION_FIXTURES: list[VersionDeprecationFixture] = [ - VersionDeprecationFixture( - test_id="deprecated_version_warns", - version="3.1", - suppress_env=False, - expected_warning=True, - ), - VersionDeprecationFixture( - test_id="old_deprecated_version_warns", - version="2.9", - suppress_env=False, - expected_warning=True, - ), - VersionDeprecationFixture( - test_id="current_version_no_warning", - version="3.2a", - suppress_env=False, - expected_warning=False, - ), - VersionDeprecationFixture( - test_id="newer_version_no_warning", - version="3.5", - suppress_env=False, - expected_warning=False, - ), - VersionDeprecationFixture( - test_id="env_var_suppresses_warning", - version="3.0", - suppress_env=True, - expected_warning=False, - ), -] - - -@pytest.mark.parametrize( - list(VersionDeprecationFixture._fields), - VERSION_DEPRECATION_FIXTURES, - ids=[test.test_id for test in VERSION_DEPRECATION_FIXTURES], -) -def test_version_deprecation_warning( - test_id: str, - version: str, - suppress_env: bool, - expected_warning: bool, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test version deprecation warning behavior.""" - import warnings - - import libtmux.common - - # Reset the warning flag for each test - monkeypatch.setattr(libtmux.common, "_version_deprecation_checked", False) - - # Set or clear the suppress env var - if suppress_env: - monkeypatch.setenv("LIBTMUX_SUPPRESS_VERSION_WARNING", "1") - else: - monkeypatch.delenv("LIBTMUX_SUPPRESS_VERSION_WARNING", raising=False) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - libtmux.common._check_deprecated_version(LooseVersion(version)) - - if expected_warning: - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) - assert version in str(w[0].message) - assert "3.2a" in str(w[0].message) - else: - assert len(w) == 0 - - -def test_version_deprecation_warns_once(monkeypatch: pytest.MonkeyPatch) -> None: - """Test that deprecation warning only fires once per process.""" - import warnings - - import libtmux.common - - monkeypatch.setattr(libtmux.common, "_version_deprecation_checked", False) - monkeypatch.delenv("LIBTMUX_SUPPRESS_VERSION_WARNING", raising=False) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - libtmux.common._check_deprecated_version(LooseVersion("3.1")) - libtmux.common._check_deprecated_version(LooseVersion("3.1")) - - assert len(w) == 1 - - -def test_version_deprecation_via_get_version(monkeypatch: pytest.MonkeyPatch) -> None: - """Test deprecation warning fires through get_version() call. - - This integration test verifies the warning is emitted when calling - get_version() with an old tmux version, testing the full call chain. - """ - import warnings - - import libtmux.common - - class MockTmuxOutput: - stdout: t.ClassVar = ["tmux 3.1"] - stderr: t.ClassVar[list[str]] = [] - - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> MockTmuxOutput: - return MockTmuxOutput() - - monkeypatch.setattr(libtmux.common, "_version_deprecation_checked", False) - monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) - monkeypatch.delenv("LIBTMUX_SUPPRESS_VERSION_WARNING", raising=False) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - version = libtmux.common.get_version() - - assert str(version) == "3.1" - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) - assert "3.1" in str(w[0].message) - assert "3.2a" in str(w[0].message) From 1351964b8d1bcb8fa2ff6290201c472294951bff Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:40:15 -0600 Subject: [PATCH 03/19] server.py(refactor): Remove pre-3.2 version guards why: tmux >= 3.2a is now required, these version checks are unnecessary what: - Remove has_gte_version("2.1") check in has_session() - always prepend = for exact match - Remove has_gte_version("3.2") check in new_session() - always pass -e environment flags - Remove has_gte_version import (no longer used) --- src/libtmux/server.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libtmux/server.py b/src/libtmux/server.py index 17b290c34..7dfe99740 100644 --- a/src/libtmux/server.py +++ b/src/libtmux/server.py @@ -28,7 +28,6 @@ PaneDict, SessionDict, WindowDict, - has_gte_version, session_check_name, ) @@ -337,7 +336,7 @@ def has_session(self, target_session: str, exact: bool = True) -> bool: """ session_check_name(target_session) - if exact and has_gte_version("2.1"): + if exact: target_session = f"={target_session}" proc = self.cmd("has-session", target=target_session) @@ -555,13 +554,8 @@ def new_session( tmux_args += ("-y", y) if environment: - if has_gte_version("3.2"): - for k, v in environment.items(): - tmux_args += (f"-e{k}={v}",) - else: - logger.warning( - "Environment flag ignored, tmux 3.2 or newer required.", - ) + for k, v in environment.items(): + tmux_args += (f"-e{k}={v}",) if window_command: tmux_args += (window_command,) From 1350cb9ec14cdf5940ccd3594c201711386450ea Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:41:42 -0600 Subject: [PATCH 04/19] session.py(refactor): Remove pre-3.2 version guards why: tmux >= 3.2a is now required, these version checks are unnecessary what: - Remove has_version("2.7") BSD error workaround in rename_session() - Remove has_gte_version("3.0") check in new_window() - always pass -e environment flags - Remove has_gte_version("3.2") check for direction flags - always pass direction - Remove has_gte_version("3.2") check for target_window - always use target - Remove version skip comment from doctest examples - Remove unused has_gte_version and has_version imports --- src/libtmux/session.py | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/src/libtmux/session.py b/src/libtmux/session.py index 26b55426d..217a472bf 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -25,8 +25,6 @@ EnvironmentMixin, WindowDict, handle_option_error, - has_gte_version, - has_version, session_check_name, ) @@ -569,16 +567,7 @@ def rename_session(self, new_name: str) -> Session: proc = self.cmd("rename-session", new_name) if proc.stderr: - if has_version("2.7") and "no current client" in proc.stderr: - """tmux 2.7 raises "no current client" warning on BSD systems. - - Should be fixed next release: - - - https://www.mail-archive.com/tech@openbsd.org/msg45186.html - - https://marc.info/?l=openbsd-cvs&m=152183263526828&w=2 - """ - else: - raise exc.LibTmuxException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) self.refresh() @@ -636,11 +625,6 @@ def new_window( Examples -------- - .. :: - >>> import pytest - >>> from libtmux.common import has_lt_version - >>> if has_lt_version('3.2'): - ... pytest.skip('direction doctests require tmux 3.2 or newer') >>> window_initial = session.new_window(window_name='Example') >>> window_initial Window(@... 2:Example, Session($1 libtmux_...)) @@ -689,33 +673,18 @@ def new_window( window_args += ("-n", window_name) if environment: - if has_gte_version("3.0"): - for k, v in environment.items(): - window_args += (f"-e{k}={v}",) - else: - logger.warning( - "Environment flag ignored, requires tmux 3.0 or newer.", - ) + for k, v in environment.items(): + window_args += (f"-e{k}={v}",) if direction is not None: - if has_gte_version("3.2"): - window_args += (WINDOW_DIRECTION_FLAG_MAP[direction],) - else: - logger.warning( - "Direction flag ignored, requires tmux 3.1 or newer.", - ) + window_args += (WINDOW_DIRECTION_FLAG_MAP[direction],) target: str | None = None if window_index is not None: # empty string for window_index will use the first one available target = f"{self.session_id}:{window_index}" if target_window: - if has_gte_version("3.2"): - target = target_window - else: - logger.warning( - "Window target ignored, requires tmux 3.1 or newer.", - ) + target = target_window elif window_index is not None: # empty string for window_index will use the first one available window_args += (f"-t{self.session_id}:{window_index}",) From fb48dceb531fe3bab5829812f0ad750d6136cc28 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:43:57 -0600 Subject: [PATCH 05/19] window.py(refactor): Remove pre-3.2 version guards why: tmux >= 3.2a is now required, version guards for older tmux are unnecessary. what: - Remove has_gte_version("2.9") check in resize() - resize is always available - Remove doctest version skip block - Remove unused has_gte_version import --- src/libtmux/window.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index e20eb26f3..b863442a8 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -14,7 +14,7 @@ import warnings from libtmux._internal.query_list import QueryList -from libtmux.common import has_gte_version, tmux_cmd +from libtmux.common import tmux_cmd from libtmux.constants import ( RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, PaneDirection, @@ -357,10 +357,6 @@ def resize( 2. Manual resizing: ``height`` and / or ``width``. 3. Expand or shrink: ``expand`` or ``shrink``. """ - if not has_gte_version("2.9"): - warnings.warn("resize() requires tmux 2.9 or newer", stacklevel=2) - return self - tmux_args: tuple[str, ...] = () # Adjustments @@ -694,11 +690,6 @@ def new_window( Examples -------- - .. :: - >>> import pytest - >>> from libtmux.common import has_lt_version - >>> if has_lt_version('3.2'): - ... pytest.skip('This doctest requires tmux 3.2 or newer') >>> window_initial = session.new_window(window_name='Example') >>> window_initial Window(@... 2:Example, Session($1 libtmux_...)) From b4e8d203805dc47d17df15cb85a7e9170448e6f9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:45:07 -0600 Subject: [PATCH 06/19] pane.py(refactor): Remove pre-3.2 version guards why: tmux >= 3.2a is now required, version guards for older tmux are unnecessary. what: - Remove has_gte_version("3.1") percentage checks in resize() - percentages always allowed - Remove has_lt_version("3.1") size flag branching in split() - always use -l flag - Remove has_gte_version("3.0") environment check in split() - -e flag always available - Simplify nested if statements after removing version checks - Remove unused has_gte_version, has_lt_version imports --- src/libtmux/pane.py | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 7f126f452..f8d95e5af 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -14,7 +14,7 @@ import warnings from libtmux import exc -from libtmux.common import has_gte_version, has_lt_version, tmux_cmd +from libtmux.common import tmux_cmd from libtmux.constants import ( PANE_DIRECTION_FLAG_MAP, RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, @@ -275,20 +275,21 @@ def resize( elif height or width: # Manual resizing if height: - if isinstance(height, str): - if height.endswith("%") and not has_gte_version("3.1"): - raise exc.VersionTooLow - if not height.isdigit() and not height.endswith("%"): - raise exc.RequiresDigitOrPercentage + if ( + isinstance(height, str) + and not height.isdigit() + and not height.endswith("%") + ): + raise exc.RequiresDigitOrPercentage tmux_args += (f"-y{height}",) if width: - if isinstance(width, str): - if width.endswith("%") and not has_gte_version("3.1"): - raise exc.VersionTooLow - if not width.isdigit() and not width.endswith("%"): - raise exc.RequiresDigitOrPercentage - + if ( + isinstance(width, str) + and not width.isdigit() + and not width.endswith("%") + ): + raise exc.RequiresDigitOrPercentage tmux_args += (f"-x{width}",) elif zoom: # Zoom / Unzoom @@ -650,16 +651,7 @@ def split( tmux_args += tuple(PANE_DIRECTION_FLAG_MAP[PaneDirection.Below]) if size is not None: - if has_lt_version("3.1"): - if isinstance(size, str) and size.endswith("%"): - tmux_args += (f"-p{str(size).rstrip('%')}",) - else: - warnings.warn( - 'Ignored size. Use percent in tmux < 3.1, e.g. "size=50%"', - stacklevel=2, - ) - else: - tmux_args += (f"-l{size}",) + tmux_args += (f"-l{size}",) if full_window_split: tmux_args += ("-f",) @@ -678,13 +670,8 @@ def split( tmux_args += ("-d",) if environment: - if has_gte_version("3.0"): - for k, v in environment.items(): - tmux_args += (f"-e{k}={v}",) - else: - logger.warning( - "Environment flag ignored, tmux 3.0 or newer required.", - ) + for k, v in environment.items(): + tmux_args += (f"-e{k}={v}",) if shell: tmux_args += (shell,) From cf66ebf4b552e4f4157d5952b49df95d457c070c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:46:09 -0600 Subject: [PATCH 07/19] tests/test_server.py(refactor): Remove pre-3.2 version conditionals why: tmux >= 3.2a is now required, version conditionals are unnecessary. what: - Remove has_gte_version("3.2") conditionals for pane_start_command format - Mark test_new_session_width_height as skip (always skipped on 3.2+, needs rework) - Remove has_gte_version("3.2") check from test_new_session_environmental_variables - Remove unused has_gte_version, has_version imports --- tests/test_server.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 4ae614e0d..9b85d279c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,7 +11,6 @@ import pytest -from libtmux.common import has_gte_version, has_version from libtmux.server import Server if t.TYPE_CHECKING: @@ -134,10 +133,7 @@ def test_new_session_shell(server: Server) -> None: pane_start_command = pane.pane_start_command assert pane_start_command is not None - if has_gte_version("3.2"): - assert pane_start_command.replace('"', "") == cmd - else: - assert pane_start_command == cmd + assert pane_start_command.replace('"', "") == cmd def test_new_session_shell_env(server: Server) -> None: @@ -158,13 +154,10 @@ def test_new_session_shell_env(server: Server) -> None: pane_start_command = pane.pane_start_command assert pane_start_command is not None - if has_gte_version("3.2"): - assert pane_start_command.replace('"', "") == cmd - else: - assert pane_start_command == cmd + assert pane_start_command.replace('"', "") == cmd -@pytest.mark.skipif(has_version("3.2"), reason="Wrong width returned with 3.2") +@pytest.mark.skipif(True, reason="tmux 3.2 returns wrong width - test needs rework") def test_new_session_width_height(server: Server) -> None: """Verify ``Server.new_session`` creates valid session running w/ dimensions.""" cmd = "/usr/bin/env PS1='$ ' sh" @@ -182,17 +175,11 @@ def test_new_session_width_height(server: Server) -> None: def test_new_session_environmental_variables( server: Server, - caplog: pytest.LogCaptureFixture, ) -> None: """Server.new_session creates and returns valid session.""" my_session = server.new_session("test_new_session", environment={"FOO": "HI"}) - if has_gte_version("3.2"): - assert my_session.show_environment()["FOO"] == "HI" - else: - assert any( - "Environment flag ignored" in record.msg for record in caplog.records - ), "Warning missing" + assert my_session.show_environment()["FOO"] == "HI" def test_no_server_sessions() -> None: From 701915bb255600774afb6b19296618296ad6a89d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:47:38 -0600 Subject: [PATCH 08/19] tests/test_session.py(refactor): Remove pre-3.2 version conditionals why: tmux >= 3.2a is now required, version conditionals are unnecessary. what: - Remove has_gte_version("2.1") from test_has_session - exact match always works - Simplify test_show_option_unknown - always expect InvalidOption - Simplify test_set_option_invalid - always expect InvalidOption - Remove skip from test_new_window_with_environment - -e flag always available - Delete test_new_window_with_environment_logs_warning_for_old_tmux (dead code) - Remove skip from test_session_new_window_with_direction - direction flags always work - Delete test_session_new_window_with_direction_logs_warning_for_old_tmux (dead code) - Remove unused has_gte_version, has_lt_version imports --- tests/test_session.py | 74 ++++--------------------------------------- 1 file changed, 7 insertions(+), 67 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index e3a6a8929..67c85deb9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -10,7 +10,6 @@ import pytest from libtmux import exc -from libtmux.common import has_gte_version, has_lt_version from libtmux.constants import WindowDirection from libtmux.pane import Pane from libtmux.session import Session @@ -30,9 +29,8 @@ def test_has_session(server: Server, session: Session) -> None: TEST_SESSION_NAME = session.session_name assert TEST_SESSION_NAME is not None assert server.has_session(TEST_SESSION_NAME) - if has_gte_version("2.1"): - assert not server.has_session(TEST_SESSION_NAME[:-2]) - assert server.has_session(TEST_SESSION_NAME[:-2], exact=False) + assert not server.has_session(TEST_SESSION_NAME[:-2]) + assert server.has_session(TEST_SESSION_NAME[:-2], exact=False) assert not server.has_session("asdf2314324321") @@ -151,11 +149,8 @@ def test_empty_session_option_returns_None(session: Session) -> None: def test_show_option_unknown(session: Session) -> None: - """Session.show_option raises UnknownOption for invalid option.""" - cmd_exception: type[exc.OptionError] = exc.UnknownOption - if has_gte_version("3.0"): - cmd_exception = exc.InvalidOption - with pytest.raises(cmd_exception): + """Session.show_option raises InvalidOption for invalid option.""" + with pytest.raises(exc.InvalidOption): session.show_option("moooz") @@ -172,13 +167,9 @@ def test_set_option_ambiguous(session: Session) -> None: def test_set_option_invalid(session: Session) -> None: - """Session.set_option raises UnknownOption for invalid option.""" - if has_gte_version("2.4"): - with pytest.raises(exc.InvalidOption): - session.set_option("afewewfew", 43) - else: - with pytest.raises(exc.UnknownOption): - session.set_option("afewewfew", 43) + """Session.set_option raises InvalidOption for invalid option.""" + with pytest.raises(exc.InvalidOption): + session.set_option("afewewfew", 43) def test_show_environment(session: Session) -> None: @@ -315,10 +306,6 @@ class SessionWindowEnvironmentFixture(t.NamedTuple): ] -@pytest.mark.skipif( - has_lt_version("3.0"), - reason="needs -e flag for new-window which was introduced in 3.0", -) @pytest.mark.parametrize( list(SessionWindowEnvironmentFixture._fields), SESSION_WINDOW_ENV_FIXTURES, @@ -346,34 +333,6 @@ def test_new_window_with_environment( assert pane.capture_pane()[-2] == v -@pytest.mark.skipif( - has_gte_version("3.0"), - reason="3.0 has the -e flag on new-window", -) -def test_new_window_with_environment_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify new window with environment vars create a warning if tmux is too old.""" - env = shutil.which("env") - assert env is not None, "Cannot find usable `env` in PATH." - - session.new_window( - attach=True, - window_name="window_with_environment", - window_shell=f"{env} PS1='$ ' sh", - environment={"ENV_VAR": "window"}, - ) - - assert any("Environment flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) - - -@pytest.mark.skipif( - has_lt_version("3.2"), - reason="Only 3.2+ has the -a and -b flag on new-window", -) def test_session_new_window_with_direction( session: Session, ) -> None: @@ -403,25 +362,6 @@ def test_session_new_window_with_direction( assert window_before.window_index == "1" -@pytest.mark.skipif( - has_gte_version("3.1"), - reason="Only 3.1 has the -a and -b flag on new-window", -) -def test_session_new_window_with_direction_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify new window with direction create a warning if tmux is too old.""" - session.new_window( - window_name="session_window_with_direction", - direction=WindowDirection.After, - ) - - assert any("Direction flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) - - def test_session_context_manager(server: Server) -> None: """Test Session context manager functionality.""" with server.new_session() as session: From 0139bca43435b71efceca2d419ad2421c663986c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:49:35 -0600 Subject: [PATCH 09/19] tests/test_window.py(refactor): Remove pre-3.2 version conditionals why: tmux >= 3.2a is now required, version conditionals are unnecessary. what: - Simplify test_split_shell pane_start_command check - always quote format - Simplify test_split_size - always use modern -l flag - Simplify test_set_show_window_options - pane-border-format always available - Simplify test_show_window_option_unknown - always expect InvalidOption - Simplify test_set_window_option_invalid - always expect InvalidOption - Remove skip from test_empty_window_name - filter flag always available - Remove skip from test_split_with_environment - -e flag always available - Remove skip from test_split_window_zoom - -Z flag always available - Delete test_split_with_environment_logs_warning_for_old_tmux (dead code) - Remove skip from test_resize - resize-window always available - Remove skip from test_new_window_with_direction - direction flags always work - Delete test_new_window_with_direction_logs_warning_for_old_tmux (dead code) - Remove unused has_gte_version, has_lt_version, has_lte_version imports --- tests/test_window.py | 129 ++++++------------------------------------- 1 file changed, 17 insertions(+), 112 deletions(-) diff --git a/tests/test_window.py b/tests/test_window.py index 74d0f703d..70fffa36e 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -12,7 +12,6 @@ from libtmux import exc from libtmux._internal.query_list import ObjectDoesNotExist -from libtmux.common import has_gte_version, has_lt_version, has_lte_version from libtmux.constants import ( PaneDirection, ResizeAdjustmentDirection, @@ -158,12 +157,8 @@ def test_split_shell(session: Session) -> None: assert window.window_width is not None assert float(first_pane.pane_height) <= ((float(window.window_width) + 1) / 2) - if has_gte_version("3.2"): - pane_start_command = pane.pane_start_command or "" - assert pane_start_command.replace('"', "") == cmd - - else: - assert pane.pane_start_command == cmd + pane_start_command = pane.pane_start_command or "" + assert pane_start_command.replace('"', "") == cmd def test_split_horizontal(session: Session) -> None: @@ -187,30 +182,17 @@ def test_split_size(session: Session) -> None: window = session.new_window(window_name="split window size") window.resize(height=100, width=100) - if has_gte_version("3.1"): - pane = window.split(size=10) - assert pane.pane_height == "10" - - pane = window.split(direction=PaneDirection.Right, size=10) - assert pane.pane_width == "10" + pane = window.split(size=10) + assert pane.pane_height == "10" - pane = window.split(size="10%") - assert pane.pane_height == "8" + pane = window.split(direction=PaneDirection.Right, size=10) + assert pane.pane_width == "10" - pane = window.split(direction=PaneDirection.Right, size="10%") - assert pane.pane_width == "8" - else: - window_height_before = ( - int(window.window_height) if isinstance(window.window_height, str) else 0 - ) - window_width_before = ( - int(window.window_width) if isinstance(window.window_width, str) else 0 - ) - pane = window.split(size="10%") - assert pane.pane_height == str(int(window_height_before * 0.1)) + pane = window.split(size="10%") + assert pane.pane_height == "8" - pane = window.split(direction=PaneDirection.Right, size="10%") - assert pane.pane_width == str(int(window_width_before * 0.1)) + pane = window.split(direction=PaneDirection.Right, size="10%") + assert pane.pane_width == "8" class WindowRenameFixture(t.NamedTuple): @@ -297,9 +279,8 @@ def test_set_show_window_options(session: Session) -> None: assert window.show_window_option("main-pane-height") == 40 assert window.show_window_options()["main-pane-height"] == 40 - if has_gte_version("2.3"): - window.set_window_option("pane-border-format", " #P ") - assert window.show_window_option("pane-border-format") == " #P " + window.set_window_option("pane-border-format", " #P ") + assert window.show_window_option("pane-border-format") == " #P " def test_empty_window_option_returns_None(session: Session) -> None: @@ -321,13 +302,10 @@ def test_show_window_option(session: Session) -> None: def test_show_window_option_unknown(session: Session) -> None: - """Window.show_window_option raises UnknownOption for bad option key.""" + """Window.show_window_option raises InvalidOption for bad option key.""" window = session.new_window(window_name="test_window") - cmd_exception: type[exc.OptionError] = exc.UnknownOption - if has_gte_version("3.0"): - cmd_exception = exc.InvalidOption - with pytest.raises(cmd_exception): + with pytest.raises(exc.InvalidOption): window.show_window_option("moooz") @@ -348,15 +326,11 @@ def test_set_window_option_ambiguous(session: Session) -> None: def test_set_window_option_invalid(session: Session) -> None: - """Window.set_window_option raises ValueError for invalid option key.""" + """Window.set_window_option raises InvalidOption for invalid option key.""" window = session.new_window(window_name="test_window") - if has_gte_version("2.4"): - with pytest.raises(exc.InvalidOption): - window.set_window_option("afewewfew", 43) - else: - with pytest.raises(exc.UnknownOption): - window.set_window_option("afewewfew", 43) + with pytest.raises(exc.InvalidOption): + window.set_window_option("afewewfew", 43) def test_move_window(session: Session) -> None: @@ -384,10 +358,6 @@ def test_select_layout_accepts_no_arg(server: Server, session: Session) -> None: window.select_layout() -@pytest.mark.skipif( - has_lt_version("3.2"), - reason="needs filter introduced in tmux >= 3.2", -) def test_empty_window_name(session: Session) -> None: """New windows can be created with empty string for window name.""" session.set_option("automatic-rename", "off") @@ -426,10 +396,6 @@ class WindowSplitEnvironmentFixture(t.NamedTuple): ] -@pytest.mark.skipif( - has_lt_version("3.0"), - reason="needs -e flag for split-window which was introduced in 3.0", -) @pytest.mark.parametrize( list(WindowSplitEnvironmentFixture._fields), WINDOW_SPLIT_ENV_FIXTURES, @@ -457,10 +423,6 @@ def test_split_with_environment( assert pane.capture_pane()[-2] == v -@pytest.mark.skipif( - has_lte_version("3.1"), - reason="3.2 has the -Z flag on split-window", -) def test_split_window_zoom( session: Session, ) -> None: @@ -483,33 +445,6 @@ def test_split_window_zoom( assert pane_with_zoom.height == pane_with_zoom.window_height -@pytest.mark.skipif( - has_gte_version("3.0"), - reason="3.0 has the -e flag on split-window", -) -def test_split_with_environment_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify splitting window with environment variables warns if tmux too old.""" - env = shutil.which("env") - assert env is not None, "Cannot find usable `env` in Path." - - window = session.new_window(window_name="split_with_environment") - window.split( - shell=f"{env} PS1='$ ' sh", - environment={"ENV_VAR": "pane"}, - ) - - assert any("Environment flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) - - -@pytest.mark.skipif( - has_lt_version("2.9"), - reason="resize-window only exists in tmux 2.9+", -) def test_resize( session: Session, ) -> None: @@ -586,10 +521,6 @@ def test_resize( assert window_height_before < window_height_expanded -@pytest.mark.skipif( - has_lt_version("3.2"), - reason="Only 3.2+ has the -a and -b flag on new-window", -) def test_new_window_with_direction( session: Session, ) -> None: @@ -619,32 +550,6 @@ def test_new_window_with_direction( assert window_before.window_index == "2" -@pytest.mark.skipif( - has_gte_version("3.2"), - reason="Only 3.2+ has the -a and -b flag on new-window", -) -def test_new_window_with_direction_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify new window with direction create a warning if tmux is too old.""" - window = session.active_window - window.refresh() - - window.new_window( - window_name="window_with_direction", - direction=WindowDirection.After, - ) - - assert any("Window target ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) - - assert any("Direction flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) - - def test_window_context_manager(session: Session) -> None: """Test Window context manager functionality.""" with session.new_window() as window: From 34460431c81ed4036bd6aabb19d3e276076715d1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:52:21 -0600 Subject: [PATCH 10/19] tests/test_pane.py(refactor): Remove pre-3.2 version conditionals why: tmux >= 3.2a is now required, version conditionals are unnecessary. what: - Remove skip from test_pane_split_window_zoom - -Z flag always available - Remove skip from test_resize_pane - resize-pane always available - Remove has_gte_version("3.1") percentage branching in test_resize_pane - Simplify test_split_pane_size - always use modern path - Remove unused has_gte_version, has_lt_version, has_lte_version imports --- tests/test_pane.py | 99 ++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 61 deletions(-) diff --git a/tests/test_pane.py b/tests/test_pane.py index 4f68eec17..015a7218c 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -9,7 +9,6 @@ import pytest -from libtmux.common import has_gte_version, has_lt_version, has_lte_version from libtmux.constants import PaneDirection, ResizeAdjustmentDirection from libtmux.test.retry import retry_until @@ -158,10 +157,6 @@ def test_capture_pane_end(session: Session) -> None: assert pane_contents == '$ printf "%s"\n$' -@pytest.mark.skipif( - has_lte_version("3.1"), - reason="3.2 has the -Z flag on split-window", -) def test_pane_split_window_zoom( session: Session, ) -> None: @@ -188,10 +183,6 @@ def test_pane_split_window_zoom( assert pane_with_zoom.height == pane_with_zoom.window_height -@pytest.mark.skipif( - has_lt_version("2.9"), - reason="resize-window only exists in tmux 2.9+", -) def test_resize_pane( session: Session, ) -> None: @@ -229,21 +220,20 @@ def test_resize_pane( ) assert int(pane.pane_width) == 75 - if has_gte_version("3.1"): - # Manual: Height percentage - window.select_layout("main-vertical") - pane_height_before = int(pane.pane_height) - pane.resize_pane( - height="15%", - ) - assert int(pane.pane_height) == 75 - - # Manual: Width percentage - window.select_layout("main-horizontal") - pane.resize_pane( - width="15%", - ) - assert int(pane.pane_width) == 75 + # Manual: Height percentage + window.select_layout("main-vertical") + pane_height_before = int(pane.pane_height) + pane.resize_pane( + height="15%", + ) + assert int(pane.pane_height) == 75 + + # Manual: Width percentage + window.select_layout("main-horizontal") + pane.resize_pane( + width="15%", + ) + assert int(pane.pane_width) == 75 # # Adjustments @@ -287,43 +277,30 @@ def test_split_pane_size(session: Session) -> None: pane = window.active_pane assert pane is not None - if has_gte_version("3.1"): - short_pane = pane.split(size=10) - assert short_pane.pane_height == "10" - - assert short_pane.at_left - assert short_pane.at_right - assert not short_pane.at_top - assert short_pane.at_bottom - - narrow_pane = pane.split(direction=PaneDirection.Right, size=10) - assert narrow_pane.pane_width == "10" - - assert not narrow_pane.at_left - assert narrow_pane.at_right - assert narrow_pane.at_top - assert not narrow_pane.at_bottom - - new_pane = pane.split(size="10%") - assert new_pane.pane_height == "8" - - new_pane = short_pane.split(direction=PaneDirection.Right, size="10%") - assert new_pane.pane_width == "10" - - assert not new_pane.at_left - assert new_pane.at_right - else: - window_height_before = ( - int(window.window_height) if isinstance(window.window_height, str) else 0 - ) - window_width_before = ( - int(window.window_width) if isinstance(window.window_width, str) else 0 - ) - new_pane = pane.split(size="10%") - assert new_pane.pane_height == str(int(window_height_before * 0.1)) - - new_pane = new_pane.split(direction=PaneDirection.Right, size="10%") - assert new_pane.pane_width == str(int(window_width_before * 0.1)) + short_pane = pane.split(size=10) + assert short_pane.pane_height == "10" + + assert short_pane.at_left + assert short_pane.at_right + assert not short_pane.at_top + assert short_pane.at_bottom + + narrow_pane = pane.split(direction=PaneDirection.Right, size=10) + assert narrow_pane.pane_width == "10" + + assert not narrow_pane.at_left + assert narrow_pane.at_right + assert narrow_pane.at_top + assert not narrow_pane.at_bottom + + new_pane = pane.split(size="10%") + assert new_pane.pane_height == "8" + + new_pane = short_pane.split(direction=PaneDirection.Right, size="10%") + assert new_pane.pane_width == "10" + + assert not new_pane.at_left + assert new_pane.at_right def test_pane_context_manager(session: Session) -> None: From acd91cf90463c179fb3174213a00769fd74cad40 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:57:27 -0600 Subject: [PATCH 11/19] tests/legacy_api(refactor): Remove pre-3.2 version conditionals why: Drop support for tmux versions older than 3.2 what: - test_session.py: Remove has_gte_version/has_lt_version imports and guards - test_session.py: Simplify option error tests to always expect InvalidOption - test_session.py: Remove environment warning test for old tmux - test_window.py: Remove has_gte_version/has_lt_version imports - test_window.py: Simplify pane_start_command and size tests - test_window.py: Simplify option tests to always expect InvalidOption - test_window.py: Remove skipif decorators for 3.0/3.2 features - test_window.py: Remove environment warning test for old tmux --- tests/legacy_api/test_session.py | 51 +++--------------- tests/legacy_api/test_window.py | 89 ++++++-------------------------- 2 files changed, 24 insertions(+), 116 deletions(-) diff --git a/tests/legacy_api/test_session.py b/tests/legacy_api/test_session.py index c756999ea..e565983e6 100644 --- a/tests/legacy_api/test_session.py +++ b/tests/legacy_api/test_session.py @@ -9,7 +9,6 @@ import pytest from libtmux import exc -from libtmux.common import has_gte_version, has_lt_version from libtmux.pane import Pane from libtmux.session import Session from libtmux.test.constants import TEST_SESSION_PREFIX @@ -27,9 +26,8 @@ def test_has_session(server: Server, session: Session) -> None: TEST_SESSION_NAME = session.get("session_name") assert TEST_SESSION_NAME is not None assert server.has_session(TEST_SESSION_NAME) - if has_gte_version("2.1"): - assert not server.has_session(TEST_SESSION_NAME[:-2]) - assert server.has_session(TEST_SESSION_NAME[:-2], exact=False) + assert not server.has_session(TEST_SESSION_NAME[:-2]) + assert server.has_session(TEST_SESSION_NAME[:-2], exact=False) assert not server.has_session("asdf2314324321") @@ -148,11 +146,8 @@ def test_empty_session_option_returns_None(session: Session) -> None: def test_show_option_unknown(session: Session) -> None: - """Session.show_option raises UnknownOption for invalid option.""" - cmd_exception: type[exc.OptionError] = exc.UnknownOption - if has_gte_version("3.0"): - cmd_exception = exc.InvalidOption - with pytest.raises(cmd_exception): + """Session.show_option raises InvalidOption for invalid option.""" + with pytest.raises(exc.InvalidOption): session.show_option("moooz") @@ -169,13 +164,9 @@ def test_set_option_ambiguous(session: Session) -> None: def test_set_option_invalid(session: Session) -> None: - """Session.set_option raises UnknownOption for invalid option.""" - if has_gte_version("2.4"): - with pytest.raises(exc.InvalidOption): - session.set_option("afewewfew", 43) - else: - with pytest.raises(exc.UnknownOption): - session.set_option("afewewfew", 43) + """Session.set_option raises InvalidOption for invalid option.""" + with pytest.raises(exc.InvalidOption): + session.set_option("afewewfew", 43) def test_show_environment(session: Session) -> None: @@ -264,10 +255,6 @@ def test_cmd_inserts_session_id(session: Session) -> None: assert cmd.cmd[-1] == last_arg -@pytest.mark.skipif( - has_lt_version("3.0"), - reason="needs -e flag for new-window which was introduced in 3.0", -) @pytest.mark.parametrize( "environment", [ @@ -294,27 +281,3 @@ def test_new_window_with_environment( for k, v in environment.items(): pane.send_keys(f"echo ${k}") assert pane.capture_pane()[-2] == v - - -@pytest.mark.skipif( - has_gte_version("3.0"), - reason="3.0 has the -e flag on new-window", -) -def test_new_window_with_environment_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify new window with environment vars create a warning if tmux is too old.""" - env = shutil.which("env") - assert env is not None, "Cannot find usable `env` in PATH." - - session.new_window( - attach=True, - window_name="window_with_environment", - window_shell=f"{env} PS1='$ ' sh", - environment={"ENV_VAR": "window"}, - ) - - assert any("Environment flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) diff --git a/tests/legacy_api/test_window.py b/tests/legacy_api/test_window.py index 09e6e3ef3..7b2b2d130 100644 --- a/tests/legacy_api/test_window.py +++ b/tests/legacy_api/test_window.py @@ -10,7 +10,7 @@ import pytest from libtmux import exc -from libtmux.common import has_gte_version, has_lt_version, has_version +from libtmux.common import has_version from libtmux.pane import Pane from libtmux.server import Server from libtmux.window import Window @@ -144,10 +144,7 @@ def test_split_window_shell(session: Session) -> None: assert window.width is not None assert window.panes[0].height is not None assert float(window.panes[0].height) <= ((float(window.width) + 1) / 2) - if has_gte_version("3.2"): - assert pane.get("pane_start_command", "").replace('"', "") == cmd - else: - assert pane.get("pane_start_command") == cmd + assert pane.get("pane_start_command", "").replace('"', "") == cmd def test_split_window_horizontal(session: Session) -> None: @@ -188,30 +185,17 @@ def test_split_window_size(session: Session) -> None: window = session.new_window(window_name="split_window window size") window.resize(height=100, width=100) - if has_gte_version("3.1"): - pane = window.split_window(size=10) - assert pane.pane_height == "10" + pane = window.split_window(size=10) + assert pane.pane_height == "10" - pane = window.split_window(vertical=False, size=10) - assert pane.pane_width == "10" + pane = window.split_window(vertical=False, size=10) + assert pane.pane_width == "10" - pane = window.split_window(size="10%") - assert pane.pane_height == "8" + pane = window.split_window(size="10%") + assert pane.pane_height == "8" - pane = window.split_window(vertical=False, size="10%") - assert pane.pane_width == "8" - else: - window_height_before = ( - int(window.window_height) if isinstance(window.window_height, str) else 0 - ) - window_width_before = ( - int(window.window_width) if isinstance(window.window_width, str) else 0 - ) - pane = window.split_window(size="10%") - assert pane.pane_height == str(int(window_height_before * 0.1)) - - pane = window.split_window(vertical=False, size="10%") - assert pane.pane_width == str(int(window_width_before * 0.1)) + pane = window.split_window(vertical=False, size="10%") + assert pane.pane_width == "8" @pytest.mark.parametrize( @@ -279,9 +263,8 @@ def test_set_show_window_options(session: Session) -> None: assert window.show_window_option("main-pane-height") == 40 assert window.show_window_options()["main-pane-height"] == 40 - if has_gte_version("2.3"): - window.set_window_option("pane-border-format", " #P ") - assert window.show_window_option("pane-border-format") == " #P " + window.set_window_option("pane-border-format", " #P ") + assert window.show_window_option("pane-border-format") == " #P " def test_empty_window_option_returns_None(session: Session) -> None: @@ -303,13 +286,10 @@ def test_show_window_option(session: Session) -> None: def test_show_window_option_unknown(session: Session) -> None: - """Window.show_window_option raises UnknownOption for bad option key.""" + """Window.show_window_option raises InvalidOption for bad option key.""" window = session.new_window(window_name="test_window") - cmd_exception: type[exc.OptionError] = exc.UnknownOption - if has_gte_version("3.0"): - cmd_exception = exc.InvalidOption - with pytest.raises(cmd_exception): + with pytest.raises(exc.InvalidOption): window.show_window_option("moooz") @@ -330,15 +310,11 @@ def test_set_window_option_ambiguous(session: Session) -> None: def test_set_window_option_invalid(session: Session) -> None: - """Window.set_window_option raises ValueError for invalid option key.""" + """Window.set_window_option raises InvalidOption for invalid option key.""" window = session.new_window(window_name="test_window") - if has_gte_version("2.4"): - with pytest.raises(exc.InvalidOption): - window.set_window_option("afewewfew", 43) - else: - with pytest.raises(exc.UnknownOption): - window.set_window_option("afewewfew", 43) + with pytest.raises(exc.InvalidOption): + window.set_window_option("afewewfew", 43) def test_move_window(session: Session) -> None: @@ -366,10 +342,6 @@ def test_select_layout_accepts_no_arg(server: Server, session: Session) -> None: window.select_layout() -@pytest.mark.skipif( - has_lt_version("3.2"), - reason="needs filter introduced in tmux >= 3.2", -) def test_empty_window_name(session: Session) -> None: """New windows can be created with empty string for window name.""" session.set_option("automatic-rename", "off") @@ -389,10 +361,6 @@ def test_empty_window_name(session: Session) -> None: assert "''" in cmd.stdout -@pytest.mark.skipif( - has_lt_version("3.0"), - reason="needs -e flag for split-window which was introduced in 3.0", -) @pytest.mark.parametrize( "environment", [ @@ -419,26 +387,3 @@ def test_split_window_with_environment( for k, v in environment.items(): pane.send_keys(f"echo ${k}") assert pane.capture_pane()[-2] == v - - -@pytest.mark.skipif( - has_gte_version("3.0"), - reason="3.0 has the -e flag on split-window", -) -def test_split_window_with_environment_logs_warning_for_old_tmux( - session: Session, - caplog: pytest.LogCaptureFixture, -) -> None: - """Verify splitting window with environment variables warns if tmux too old.""" - env = shutil.which("env") - assert env is not None, "Cannot find usable `env` in Path." - - window = session.new_window(window_name="split_window_with_environment") - window.split_window( - shell=f"{env} PS1='$ ' sh", - environment={"ENV_VAR": "pane"}, - ) - - assert any("Environment flag ignored" in record.msg for record in caplog.records), ( - "Warning missing" - ) From 186332974fccecd44687541a59f8a53aef6f3487 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 13:58:35 -0600 Subject: [PATCH 12/19] docs/quickstart.md(refactor): Update tmux requirements why: Drop support for tmux versions older than 3.2 what: - Remove "(recommended)" qualifier since 3.2a is now required - Remove deprecation notice for tmux 1.8-3.1 --- docs/quickstart.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 9e74f6274..3d2790133 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -12,8 +12,7 @@ from inside a live tmux session. ## Requirements -- [tmux] 3.2a or newer (recommended) - - tmux 1.8 - 3.1 are deprecated and will be unsupported in a future release +- [tmux] 3.2a or newer - [pip] - for this handbook's examples [tmux]: https://tmux.github.io/ From 1ba834a207ae732db83275c7ca8b95685c54befe Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 14:00:11 -0600 Subject: [PATCH 13/19] README.md(refactor): Update backports documentation why: Clarify backport branches for legacy support what: - Rename "Python support" section to "Backports" - Make tmux version range explicit (1.8 to 3.1c) - Reorder items chronologically (Python 2.x, then tmux) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0257fa6c6..efd324b67 100644 --- a/README.md +++ b/README.md @@ -246,14 +246,14 @@ Window(@1 1:..., Session($1 ...)) Session($1 ...) ``` -# Python support +# Backports Unsupported / no security releases or bug fixes: -- tmux < 3.2: The backports branch is - [`v0.48.x`](https://github.com/tmux-python/libtmux/tree/v0.48.x). - Python 2.x: The backports branch is [`v0.8.x`](https://github.com/tmux-python/libtmux/tree/v0.8.x). +- tmux 1.8 to 3.1c: The backports branch is + [`v0.48.x`](https://github.com/tmux-python/libtmux/tree/v0.48.x). # Donations From 39f7815027b80d151b1cbf0c19e9d5b7d0a6007a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 15:18:32 -0600 Subject: [PATCH 14/19] docs(refactor): Remove outdated tmux version references why: Clean up docstrings/comments referencing old tmux versions now that 3.2a is minimum what: - common.py: Update error message, simplify handle_option_error docstring - server.py: Remove "tmux 2.1 and up only" from exact param, remove 1.9-dev comment - session.py: Remove "(tmux 3.2+)" from direction param, remove 1.9-dev comment - window.py: Remove "tmux 3.0+ only" from environment param, update deprecation msg - pane.py: Remove "tmux 3.0+ only" from environment params, remove 1.9-dev comment - neo.py: Remove QUIRK_TMUX_3_1_X_0001 documentation (no longer applicable) - Update test assertions for new error message --- src/libtmux/common.py | 9 ++------- src/libtmux/neo.py | 13 ------------- src/libtmux/pane.py | 5 ++--- src/libtmux/server.py | 2 -- src/libtmux/session.py | 3 +-- src/libtmux/window.py | 4 ++-- tests/legacy_api/test_common.py | 2 +- tests/test_common.py | 2 +- 8 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index dd1fad9a6..8985c9d53 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -288,7 +288,7 @@ def get_version() -> LooseVersion: return LooseVersion(f"{TMUX_MAX_VERSION}-openbsd") msg = ( f"libtmux supports tmux {TMUX_MIN_VERSION} and greater. This system" - " is running tmux 1.3 or earlier." + " does not meet the minimum tmux version requirement." ) raise exc.LibTmuxException( msg, @@ -456,17 +456,12 @@ def session_check_name(session_name: str | None) -> None: def handle_option_error(error: str) -> type[exc.OptionError]: """Raise exception if error in option command found. - In tmux 3.0, show-option and show-window-option return invalid option instead of - unknown option. See https://github.com/tmux/tmux/blob/3.0/cmd-show-options.c. - - In tmux >2.4, there are 3 different types of option errors: + There are 3 different types of option errors: - unknown option - invalid option - ambiguous option - In tmux <2.4, unknown option was the only option. - All errors raised will have the base error of :exc:`exc.OptionError`. So to catch any option error, use ``except exc.OptionError``. diff --git a/src/libtmux/neo.py b/src/libtmux/neo.py index e9bbd0c5a..932f969e1 100644 --- a/src/libtmux/neo.py +++ b/src/libtmux/neo.py @@ -24,16 +24,6 @@ OutputsRaw = list[OutputRaw] -""" -Quirks: - -QUIRK_TMUX_3_1_X_0001: - -- tmux 3.1 and 3.1a: -- server crash with list-panes w/ buffer_created, client_activity, client_created -""" - - @dataclasses.dataclass() class Obj: """Dataclass of generic tmux object.""" @@ -43,14 +33,11 @@ class Obj: active_window_index: str | None = None alternate_saved_x: str | None = None alternate_saved_y: str | None = None - # See QUIRK_TMUX_3_1_X_0001 buffer_name: str | None = None buffer_sample: str | None = None buffer_size: str | None = None - # See QUIRK_TMUX_3_1_X_0001 client_cell_height: str | None = None client_cell_width: str | None = None - # See QUIRK_TMUX_3_1_X_0001 client_discarded: str | None = None client_flags: str | None = None client_height: str | None = None diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index f8d95e5af..0edd156fb 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -586,7 +586,7 @@ def split( size: int, optional Cell/row or percentage to occupy with respect to current window. environment: dict, optional - Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + Environmental variables for new pane. Passthrough to ``-e``. Examples -------- @@ -662,7 +662,6 @@ def split( tmux_args += ("-P", "-F{}".format("".join(tmux_formats))) # output if start_directory: - # as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c. start_path = pathlib.Path(start_directory).expanduser() tmux_args += (f"-c{start_path}",) @@ -880,7 +879,7 @@ def split_window( percent: int, optional percentage to occupy with respect to current pane environment: dict, optional - Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + Environmental variables for new pane. Passthrough to ``-e``. Notes ----- diff --git a/src/libtmux/server.py b/src/libtmux/server.py index 7dfe99740..9041fe2cf 100644 --- a/src/libtmux/server.py +++ b/src/libtmux/server.py @@ -324,7 +324,6 @@ def has_session(self, target_session: str, exact: bool = True) -> bool: exact : bool match the session name exactly. tmux uses fnmatch by default. Internally prepends ``=`` to the session in ``$ tmux has-session``. - tmux 2.1 and up only. Raises ------ @@ -540,7 +539,6 @@ def new_session( tmux_args += ("-d",) if start_directory: - # as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-session -c. start_directory = pathlib.Path(start_directory).expanduser() tmux_args += ("-c", str(start_directory)) diff --git a/src/libtmux/session.py b/src/libtmux/session.py index 217a472bf..20696538c 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -610,7 +610,7 @@ def new_window( window upon completion is desired. direction : WindowDirection, optional - Insert window before or after target window (tmux 3.2+). + Insert window before or after target window. target_window : str, optional Used by :meth:`Window.new_window` to specify the target window. @@ -664,7 +664,6 @@ def new_window( # Catch empty string and default (`None`) if start_directory: - # as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c. start_directory = pathlib.Path(start_directory).expanduser() window_args += (f"-c{start_directory}",) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index b863442a8..df63e19b6 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -294,7 +294,7 @@ def split( size: int, optional Cell/row or percentage to occupy with respect to current window. environment: dict, optional - Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + Environmental variables for new pane. Passthrough to ``-e``. """ active_pane = self.active_pane or self.panes[0] return active_pane.split( @@ -891,7 +891,7 @@ def split_window( # Deprecated in 3.1 in favor of -l warnings.warn( f'Deprecated in favor of size="{str(percent).rstrip("%")}%" ' - ' ("-l" flag) in tmux 3.1+.', + '(using the "-l" flag).', category=DeprecationWarning, stacklevel=2, ) diff --git a/tests/legacy_api/test_common.py b/tests/legacy_api/test_common.py index 44c1742fb..c61f457bd 100644 --- a/tests/legacy_api/test_common.py +++ b/tests/legacy_api/test_common.py @@ -102,7 +102,7 @@ def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) with pytest.raises(LibTmuxException) as exc_info: get_version() - exc_info.match("is running tmux 1.3 or earlier") + exc_info.match("does not meet the minimum tmux version requirement") def test_ignores_letter_versions(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/test_common.py b/tests/test_common.py index 3aa045bc4..24b4ecf6d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -321,7 +321,7 @@ class VersionParsingFixture(t.NamedTuple): mock_platform=None, expected_version=None, raises=True, - exc_msg_regex="is running tmux 1.3 or earlier", + exc_msg_regex="does not meet the minimum tmux version requirement", ), ] From 07ba7973b160b4956a6f5b3cf2fa480ae2b0f4da Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 14:01:23 -0600 Subject: [PATCH 15/19] CHANGES(docs): Document tmux < 3.2 support removal why: Document breaking change for v0.49.x release what: - Add Breaking Changes section for 0.49.x - Document removal of tmux 1.8 to 3.1c support - Note removal of TMUX_SOFT_MIN_VERSION and deprecation system - Point users to v0.48.x backport branch --- CHANGES | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 947954ab9..d8e065ed1 100644 --- a/CHANGES +++ b/CHANGES @@ -32,7 +32,17 @@ $ uvx --from 'libtmux' --prerelease allow python -_Future release notes will be placed here_ +### Breaking Changes + +#### tmux 1.8 to 3.1c support removed (#608) + +Support for tmux versions below 3.2a has been removed. This completes the +deprecation announced in v0.48.0. + +- Minimum tmux version is now 3.2a (`TMUX_MIN_VERSION`) +- Removed `TMUX_SOFT_MIN_VERSION` constant and deprecation warning system +- Removed version guards throughout the codebase +- For users on older tmux, use libtmux v0.48.x ## libtmux 0.48.0 (2025-11-28) From 858329d9ec336ac89d1abdfff32e7937edf3c8fa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 15:55:15 -0600 Subject: [PATCH 16/19] tests/legacy_api/test_server.py(refactor): Remove pre-3.2 version conditional why: tmux >= 3.2a is now required, making the conditional always true what: - Remove has_gte_version("3.2") check in test_new_session_shell - Remove unused has_gte_version import --- tests/legacy_api/test_server.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/legacy_api/test_server.py b/tests/legacy_api/test_server.py index 8ab21d864..3aa6ab473 100644 --- a/tests/legacy_api/test_server.py +++ b/tests/legacy_api/test_server.py @@ -8,7 +8,6 @@ import pytest -from libtmux.common import has_gte_version from libtmux.server import Server if t.TYPE_CHECKING: @@ -130,10 +129,7 @@ def test_new_session_shell(server: Server) -> None: pane_start_command = pane.get("pane_start_command") assert pane_start_command is not None - if has_gte_version("3.2"): - assert pane_start_command.replace('"', "") == cmd - else: - assert pane_start_command == cmd + assert pane_start_command.replace('"', "") == cmd def test_no_server_sessions() -> None: From 59e6edef3e6b0feea13ac88fac4a684b4527ea4b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 15:55:22 -0600 Subject: [PATCH 17/19] common.py(docs): Update version examples in docstrings why: Examples should reflect current minimum version (3.2a) what: - Update has_version, has_gt_version, has_gte_version docstring examples - Update has_lte_version, has_lt_version docstring examples - Change '1.8' to '3.2a' in all version comparison function docs --- src/libtmux/common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 8985c9d53..056c888fc 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -312,7 +312,7 @@ def has_version(version: str) -> bool: Parameters ---------- version : str - version number, e.g. '1.8' + version number, e.g. '3.2a' Returns ------- @@ -328,7 +328,7 @@ def has_gt_version(min_version: str) -> bool: Parameters ---------- min_version : str - tmux version, e.g. '1.8' + tmux version, e.g. '3.2a' Returns ------- @@ -344,7 +344,7 @@ def has_gte_version(min_version: str) -> bool: Parameters ---------- min_version : str - tmux version, e.g. '1.8' + tmux version, e.g. '3.2a' Returns ------- @@ -360,7 +360,7 @@ def has_lte_version(max_version: str) -> bool: Parameters ---------- max_version : str - tmux version, e.g. '1.8' + tmux version, e.g. '3.2a' Returns ------- @@ -376,7 +376,7 @@ def has_lt_version(max_version: str) -> bool: Parameters ---------- max_version : str - tmux version, e.g. '1.8' + tmux version, e.g. '3.2a' Returns ------- From 174341b5f933a06c83e0b5434929eba8fad72ed8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 15:55:25 -0600 Subject: [PATCH 18/19] pane.py(docs): Remove outdated tmux version comment why: Comment about tmux < 1.7 irrelevant with 3.2a minimum what: - Remove "tmux < 1.7. This is added in 1.7." comment from split() --- src/libtmux/pane.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 0edd156fb..17752df37 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -677,7 +677,6 @@ def split( pane_cmd = self.cmd("split-window", *tmux_args, target=target) - # tmux < 1.7. This is added in 1.7. if pane_cmd.stderr: if "pane too small" in pane_cmd.stderr: raise exc.LibTmuxException(pane_cmd.stderr) From a145e7fcd171836c7e7f02d66cfc6b245c4e8a47 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Nov 2025 15:55:31 -0600 Subject: [PATCH 19/19] exc.py(docs): Remove version reference from InvalidOption docstring why: Version introduction info irrelevant with 3.2a minimum what: - Remove "introduced in tmux v2.4" from InvalidOption docstring --- src/libtmux/exc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libtmux/exc.py b/src/libtmux/exc.py index 7777403f3..c5363b5ef 100644 --- a/src/libtmux/exc.py +++ b/src/libtmux/exc.py @@ -81,7 +81,7 @@ def __init__(self, *args: object) -> None: class InvalidOption(OptionError): - """Option invalid to tmux, introduced in tmux v2.4.""" + """Option invalid to tmux.""" class AmbiguousOption(OptionError):