Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ $ uvx --from 'libtmux' --prerelease allow python

<!-- To maintainers and contributors: Please add notes for the forthcoming version below -->

- _Future release notes will be placed here_
_Future release notes will be placed here_

### Deprecations

- tmux versions below 3.2a are now deprecated (#606). A `FutureWarning` will
be emitted on first use. Support for these versions will be removed in a future
release. Set `LIBTMUX_SUPPRESS_VERSION_WARNING=1` to suppress the warning.

### Internal

- Added `TMUX_SOFT_MIN_VERSION` constant (3.2a) for deprecation threshold (#606)

### What's new

Expand Down
3 changes: 2 additions & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ from inside a live tmux session.

## Requirements

- [tmux]
- [tmux] 3.2a or newer (recommended)
- tmux 1.8 - 3.1 are deprecated and will be unsupported in a future release
- [pip] - for this handbook's examples

[tmux]: https://tmux.github.io/
Expand Down
37 changes: 36 additions & 1 deletion src/libtmux/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,44 @@
#: 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."""
Expand Down Expand Up @@ -303,7 +336,9 @@ def get_version() -> LooseVersion:

version = re.sub(r"[a-z-]", "", version)

return LooseVersion(version)
version_obj = LooseVersion(version)
_check_deprecated_version(version_obj)
return version_obj


def has_version(version: str) -> bool:
Expand Down
131 changes: 131 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,134 @@ 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)