From dd67944ef37f0db51434208bf7fdc58c3a59850f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:11:41 -0600 Subject: [PATCH 01/19] Hooks(refactor[typing]): Narrow hook dict values why: Improve hook typing specificity for show_hooks output. what: - Replace HookDict values from Any to int | str to match parsing logic --- src/libtmux/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libtmux/hooks.py b/src/libtmux/hooks.py index c0c28fd09..a40278601 100644 --- a/src/libtmux/hooks.py +++ b/src/libtmux/hooks.py @@ -50,7 +50,7 @@ if t.TYPE_CHECKING: from typing_extensions import Self -HookDict = dict[str, t.Any] +HookDict = dict[str, int | str] HookValues = dict[int, str] | SparseArray[str] | list[str] logger = logging.getLogger(__name__) From 01270d9762d1333cdd85bf05a636839c754d82c0 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:15:24 -0600 Subject: [PATCH 02/19] Temporary(refactor[typing]): Tighten contextmanager generator types why: Narrow send/return types for temp_session and temp_window. what: - Use Generator[..., None, None] for contextmanager yields --- src/libtmux/test/temporary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libtmux/test/temporary.py b/src/libtmux/test/temporary.py index cc2106edd..2049070dd 100644 --- a/src/libtmux/test/temporary.py +++ b/src/libtmux/test/temporary.py @@ -27,7 +27,7 @@ def temp_session( server: Server, *args: t.Any, **kwargs: t.Any, -) -> Generator[Session, t.Any, t.Any]: +) -> Generator[Session, None, None]: """ Return a context manager with a temporary session. @@ -78,7 +78,7 @@ def temp_window( session: Session, *args: t.Any, **kwargs: t.Any, -) -> Generator[Window, t.Any, t.Any]: +) -> Generator[Window, None, None]: """ Return a context manager with a temporary window. From 3742c9a9b768d0b2caa2f663657b4aec544c3361 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:19:35 -0600 Subject: [PATCH 03/19] Tests(refactor[typing]): Tighten version compare fixtures why: Use specific LooseVersion operands in version comparison fixtures. what: - Narrow VersionCompareOp and fixture fields to str inputs --- tests/legacy_api/test_version.py | 6 +++--- tests/test_version.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/legacy_api/test_version.py b/tests/legacy_api/test_version.py index e3a1de855..a33c943b6 100644 --- a/tests/legacy_api/test_version.py +++ b/tests/legacy_api/test_version.py @@ -22,7 +22,7 @@ RaisesExc: TypeAlias = RaisesContext[Exception] # type: ignore[no-redef] VersionCompareOp: TypeAlias = Callable[ - [t.Any, t.Any], + [LooseVersion, LooseVersion], bool, ] @@ -48,9 +48,9 @@ def test_version(version: str) -> None: class VersionCompareFixture(t.NamedTuple): """Test fixture for version comparison.""" - a: object + a: str op: VersionCompareOp - b: object + b: str raises: type[Exception] | bool diff --git a/tests/test_version.py b/tests/test_version.py index 1f3e3bede..967e729b1 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -22,7 +22,7 @@ RaisesExc: TypeAlias = RaisesContext[Exception] # type: ignore[no-redef] VersionCompareOp: TypeAlias = Callable[ - [t.Any, t.Any], + [LooseVersion, LooseVersion], bool, ] @@ -60,9 +60,9 @@ class VersionCompareFixture(t.NamedTuple): """Test fixture for version comparison.""" test_id: str - a: object + a: str op: VersionCompareOp - b: object + b: str raises: type[Exception] | bool From 91d027fc6e48fe5b1268cb688eeaa1003aab765a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:25:46 -0600 Subject: [PATCH 04/19] Tests(refactor[typing]): Use options dict alias for sparse array cases why: Align sparse-array test inputs with ExplodedComplexUntypedOptionsDict. what: - Replace Any with ExplodedComplexUntypedOptionsDict - Instantiate SparseArray with matching str | int parameter --- tests/test/test_sparse_array.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test/test_sparse_array.py b/tests/test/test_sparse_array.py index 194079d9d..dc80626e3 100644 --- a/tests/test/test_sparse_array.py +++ b/tests/test/test_sparse_array.py @@ -7,13 +7,14 @@ import pytest from libtmux._internal.sparse_array import SparseArray, is_sparse_array_list +from libtmux.options import ExplodedComplexUntypedOptionsDict class IsSparseArrayListTestCase(t.NamedTuple): """Test case for is_sparse_array_list TypeGuard function.""" test_id: str - input_dict: dict[str, t.Any] + input_dict: ExplodedComplexUntypedOptionsDict expected: bool @@ -39,12 +40,12 @@ class SparseArrayValuesTestCase(t.NamedTuple): IsSparseArrayListTestCase("empty_dict", {}, True), IsSparseArrayListTestCase( "sparse_arrays_only", - {"hook1": SparseArray(), "hook2": SparseArray()}, + {"hook1": SparseArray[str | int](), "hook2": SparseArray[str | int]()}, True, ), IsSparseArrayListTestCase( "mixed_values", - {"hook1": SparseArray(), "opt": "string"}, + {"hook1": SparseArray[str | int](), "opt": "string"}, False, ), IsSparseArrayListTestCase( From d26d3cb26a603b06c10b5b54d05fe9521adc68b3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:27:37 -0600 Subject: [PATCH 05/19] Tests(refactor[typing]): Tighten deprecated window method args why: Replace Any in deprecated window method fixture types with concrete unions. what: - Narrow args/kwargs types and return ParameterSet list --- tests/test_window.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_window.py b/tests/test_window.py index 5ea57f3c5..f4360f777 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -23,6 +23,8 @@ from libtmux.window import Window if t.TYPE_CHECKING: + from _pytest.mark.structures import ParameterSet + from libtmux._internal.types import StrPath from libtmux.session import Session @@ -720,8 +722,8 @@ class DeprecatedMethodTestCase(t.NamedTuple): test_id: str method_name: str # Name of deprecated method to call - args: tuple[t.Any, ...] # Positional args - kwargs: dict[str, t.Any] # Keyword args + args: tuple[str | int | bool, ...] # Positional args + kwargs: dict[str, str | int | bool] # Keyword args expected_error_match: str # Regex pattern to match error message @@ -751,7 +753,7 @@ class DeprecatedMethodTestCase(t.NamedTuple): ] -def _build_deprecated_warning_method_params() -> list[t.Any]: +def _build_deprecated_warning_method_params() -> list[ParameterSet]: """Build pytest params for deprecated method warning tests.""" return [ pytest.param(tc, id=tc.test_id) From 9fdbfa7b7debddddbcbde6a8bcc6c18d1a97d9a6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:29:08 -0600 Subject: [PATCH 06/19] Tests(refactor[typing]): Narrow dataclasses output alias why: Remove Any from test-only output type alias. what: - Replace OutputRaw value type with str --- tests/test_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index 4fcc05207..a2cac3731 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -25,7 +25,7 @@ ListExtraArgs = tuple[str] | None -OutputRaw = dict[str, t.Any] +OutputRaw = dict[str, str] OutputsRaw = list[OutputRaw] From 29b9b1f54a830e71a7702d1394edbbd108c07320 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:30:58 -0600 Subject: [PATCH 07/19] Tests(refactor[typing]): Narrow option fixture expected type why: Replace Any in option fixture expected values with specific dict unions. what: - Set expected type to dict[str, str | list[str]] --- tests/test_options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_options.py b/tests/test_options.py index 9cca8bd9b..c9026a8e8 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -409,7 +409,7 @@ class OptionDataclassTestFixture(t.NamedTuple): tmux_option: str # e.g. terminal-features # results - expected: t.Any # e.g. 50, TerminalFeatures({}), etc. + expected: dict[str, str | list[str]] dataclass_attribute: str # e.g. terminal_features @@ -469,7 +469,7 @@ def test_mocked_cmd_stdoutclass_fixture( test_id: str, mocked_cmd_stdout: list[str], tmux_option: str, - expected: t.Any, + expected: dict[str, str | list[str]], dataclass_attribute: str, server: Server, ) -> None: @@ -494,7 +494,7 @@ def test_show_option_pane_fixture( test_id: str, mocked_cmd_stdout: list[str], tmux_option: str, - expected: t.Any, + expected: dict[str, str | list[str]], dataclass_attribute: str, server: Server, ) -> None: From 30ab125063e5bca705d5e9057209cb7b4085aadf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:34:53 -0600 Subject: [PATCH 08/19] Tests(refactor[typing]): Tighten hook fixture kwarg shapes why: Replace Any in hook test helper types with TypedDicts and ParameterSet. what: - Add SetHooksOperationArgs/SetHookFlagArgs for kwargs - Use ParameterSet and MarkDecorator in helper annotations --- tests/test_hooks.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 4e03f0a26..8e600cb19 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -11,6 +11,8 @@ from libtmux.common import has_gte_version if t.TYPE_CHECKING: + from _pytest.mark.structures import ParameterSet + from libtmux.server import Server @@ -420,11 +422,11 @@ class HookTestCase(t.NamedTuple): ) -def _build_hook_params() -> list[t.Any]: +def _build_hook_params() -> list[ParameterSet]: """Build pytest params with appropriate marks.""" params = [] for tc in ALL_HOOK_TEST_CASES: - marks: list[t.Any] = [] + marks: list[pytest.MarkDecorator] = [] if tc.xfail_reason: marks.append(pytest.mark.xfail(reason=tc.xfail_reason)) params.append(pytest.param(tc, id=tc.test_id, marks=marks)) @@ -521,11 +523,18 @@ class SetHooksTestCase(t.NamedTuple): test_id: str hook: str # Hook name to test setup_hooks: dict[int, str] # Initial hooks to set (index -> value) - operation_args: dict[str, t.Any] # Args for set_hooks + operation_args: SetHooksOperationArgs expected_indices: list[int] # Expected indices after operation expected_contains: list[str] | None = None # Strings expected in values +class SetHooksOperationArgs(t.TypedDict, total=False): + """Keyword arguments for set_hooks.""" + + values: dict[int, str] | SparseArray[str] | list[str] + clear_existing: bool + + SET_HOOKS_TESTS: list[SetHooksTestCase] = [ SetHooksTestCase( "set_hooks_with_dict", @@ -565,7 +574,7 @@ class SetHooksTestCase(t.NamedTuple): ] -def _build_set_hooks_params() -> list[t.Any]: +def _build_set_hooks_params() -> list[ParameterSet]: """Build pytest params for set_hooks tests.""" return [pytest.param(tc, id=tc.test_id) for tc in SET_HOOKS_TESTS] @@ -816,7 +825,7 @@ class ShowHooksTestCase(t.NamedTuple): ] -def _build_show_hooks_params() -> list[t.Any]: +def _build_show_hooks_params() -> list[ParameterSet]: """Build pytest params for show_hooks tests.""" return [pytest.param(tc, id=tc.test_id) for tc in SHOW_HOOKS_TEST_CASES] @@ -877,11 +886,19 @@ class SetHookFlagTestCase(t.NamedTuple): """Test case for set_hook flag combinations.""" test_id: str - flag_kwargs: dict[str, t.Any] + flag_kwargs: SetHookFlagArgs expected_behavior: str # "sets_hook", "runs_immediately", "appends", "global" min_version: str = "3.2" +class SetHookFlagArgs(t.TypedDict, total=False): + """Keyword arguments for set_hook flags.""" + + append: bool + global_: bool + run: bool + + SET_HOOK_FLAG_TEST_CASES: list[SetHookFlagTestCase] = [ SetHookFlagTestCase( "append_to_existing", From 7f766f249b98b6c42fecbfee72a6f9568b13d060 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:38:16 -0600 Subject: [PATCH 09/19] Tests(refactor[typing]): Narrow option test case annotations why: Replace Any in option test helpers with concrete unions and ParameterSet. what: - Constrain OptionTestCase.test_value and conversion expectations - Use ParameterSet/MarkDecorator in helper builders --- tests/test_options.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_options.py b/tests/test_options.py index c9026a8e8..e400cd9d2 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -23,6 +23,7 @@ from libtmux.pane import Pane if t.TYPE_CHECKING: + from _pytest.mark.structures import ParameterSet from typing_extensions import LiteralString from libtmux.server import Server @@ -768,7 +769,7 @@ class OptionTestCase(t.NamedTuple): test_id: str option: str # tmux option name (hyphenated) scope: OptionScope - test_value: t.Any # Value to set + test_value: str | int # Value to set expected_type: type # Expected Python type after retrieval min_version: str | None = None # Minimum tmux version required xfail_reason: str | None = None # Mark as expected failure with reason @@ -1120,11 +1121,11 @@ class OptionTestCase(t.NamedTuple): ) -def _build_option_params() -> list[t.Any]: +def _build_option_params() -> list[ParameterSet]: """Build pytest params with appropriate marks.""" params = [] for tc in ALL_OPTION_TEST_CASES: - marks: list[t.Any] = [] + marks: list[pytest.MarkDecorator] = [] if tc.xfail_reason: marks.append(pytest.mark.xfail(reason=tc.xfail_reason)) params.append(pytest.param(tc, id=tc.test_id, marks=marks)) @@ -1233,7 +1234,7 @@ class ShowOptionsTestCase(t.NamedTuple): ] -def _build_show_options_params() -> list[t.Any]: +def _build_show_options_params() -> list[ParameterSet]: """Build pytest params for show_options tests.""" return [pytest.param(tc, id=tc.test_id) for tc in SHOW_OPTIONS_TEST_CASES] @@ -1284,7 +1285,7 @@ class ConvertValuesSparseTestCase(t.NamedTuple): test_id: str initial_values: dict[int, str] # index -> value - expected_converted: dict[int, t.Any] # index -> converted value + expected_converted: dict[int, bool | int | str] CONVERT_SPARSE_TEST_CASES: list[ConvertValuesSparseTestCase] = [ From b69d960b676ce29391c7129577a7e95b966f09e9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:42:26 -0600 Subject: [PATCH 10/19] Tests(refactor[typing]): Tighten tmux_cmd test mocks why: Improve mock signature specificity without changing behavior. what: - Replace Any with object for tmux_cmd mock args/kwargs in common tests - Narrow patched_cmd kwargs to str|int|None in session tests --- tests/legacy_api/test_common.py | 8 ++++---- tests/test_common.py | 2 +- tests/test_session.py | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/legacy_api/test_common.py b/tests/legacy_api/test_common.py index 6c1b03cf4..71178982d 100644 --- a/tests/legacy_api/test_common.py +++ b/tests/legacy_api/test_common.py @@ -43,7 +43,7 @@ class Hi: stdout: t.ClassVar = ["tmux master"] stderr = None - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: + def mock_tmux_cmd(*args: object, **kwargs: object) -> Hi: return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) @@ -64,7 +64,7 @@ class Hi: stdout: t.ClassVar = [f"tmux next-{TMUX_NEXT_VERSION}"] stderr = None - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: + def mock_tmux_cmd(*args: object, **kwargs: object) -> Hi: return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) @@ -81,7 +81,7 @@ def test_get_version_openbsd(monkeypatch: pytest.MonkeyPatch) -> None: class Hi: stderr: t.ClassVar = ["tmux: unknown option -- V"] - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: + def mock_tmux_cmd(*args: object, **kwargs: object) -> Hi: return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) @@ -100,7 +100,7 @@ def test_get_version_too_low(monkeypatch: pytest.MonkeyPatch) -> None: class Hi: stderr: t.ClassVar = ["tmux: unknown option -- V"] - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: + def mock_tmux_cmd(*args: object, **kwargs: object) -> Hi: return Hi() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) diff --git a/tests/test_common.py b/tests/test_common.py index a3345be8d..176ac2fe0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -347,7 +347,7 @@ class MockTmuxOutput: stdout = mock_stdout stderr = mock_stderr - def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> MockTmuxOutput: + def mock_tmux_cmd(*args: object, **kwargs: object) -> MockTmuxOutput: return MockTmuxOutput() monkeypatch.setattr(libtmux.common, "tmux_cmd", mock_tmux_cmd) diff --git a/tests/test_session.py b/tests/test_session.py index e0dc85324..eadfe1ebd 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -555,7 +555,11 @@ def __init__(self) -> None: self.stderr: list[str] = [] self.cmd: list[str] = ["tmux", "attach-session"] - def patched_cmd(cmd_name: str, *args: t.Any, **kwargs: t.Any) -> tmux_cmd: + def patched_cmd( + cmd_name: str, + *args: object, + **kwargs: str | int | None, + ) -> tmux_cmd: """Patched cmd that kills session after attach-session.""" if cmd_name == "attach-session": # Simulate: attach-session succeeded, user worked, then killed session From 103c042b88298a2d734d7dcc7cb4106d7a62fbfa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:49:20 -0600 Subject: [PATCH 11/19] PytestPlugin(refactor[typing]): Type session params fixture why: Provide concrete, key-aware typing for session fixture overrides. what: - Add SessionParams TypedDict aligned with Server.new_session kwargs - Use SessionParams in session_params and session fixture signatures --- src/libtmux/pytest_plugin.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/libtmux/pytest_plugin.py b/src/libtmux/pytest_plugin.py index cc45ce7a9..db9a7348b 100644 --- a/src/libtmux/pytest_plugin.py +++ b/src/libtmux/pytest_plugin.py @@ -24,6 +24,19 @@ USING_ZSH = "zsh" in os.getenv("SHELL", "") +class SessionParams(t.TypedDict, total=False): + """Keyword arguments for :meth:`libtmux.Server.new_session` in tests.""" + + kill_session: bool + attach: bool + start_directory: os.PathLike[str] | str | None + window_name: str | None + window_command: str | None + x: int | t.Literal["-"] | None + y: int | t.Literal["-"] | None + environment: dict[str, str] | None + + @pytest.fixture(scope="session") def home_path(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: """Temporary `/home/` path.""" @@ -151,7 +164,7 @@ def fin() -> None: @pytest.fixture -def session_params() -> dict[str, t.Any]: +def session_params() -> SessionParams: """Return new, temporary :class:`libtmux.Session`. >>> import pytest @@ -191,7 +204,7 @@ def session_params() -> dict[str, t.Any]: @pytest.fixture def session( request: pytest.FixtureRequest, - session_params: dict[str, t.Any], + session_params: SessionParams, server: Server, ) -> Session: """Return new, temporary :class:`libtmux.Session`. From 844f7934d6dc937ba1ae4be6363f95cd3d4a0c7c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 06:59:57 -0600 Subject: [PATCH 12/19] TestHelpers(refactor[typing]): Type temp session/window helpers why: Expose precise, key-aware kwargs for temp helpers without restricting callers. what: - Add TypedDicts and overloads for temp_session/temp_window kwargs - Wrap contextmanager generators to return ContextManager types explicitly --- src/libtmux/test/temporary.py | 134 ++++++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 30 deletions(-) diff --git a/src/libtmux/test/temporary.py b/src/libtmux/test/temporary.py index 2049070dd..af6d52f47 100644 --- a/src/libtmux/test/temporary.py +++ b/src/libtmux/test/temporary.py @@ -12,8 +12,11 @@ if t.TYPE_CHECKING: import sys - from collections.abc import Generator + from typing_extensions import Unpack + + from libtmux._internal.types import StrPath + from libtmux.constants import WindowDirection from libtmux.server import Server from libtmux.session import Session from libtmux.window import Window @@ -22,12 +25,73 @@ pass +class TempSessionParams(t.TypedDict, total=False): + """Keyword arguments for :func:`temp_session`.""" + + session_name: str | None + kill_session: bool + attach: bool + start_directory: StrPath | None + window_name: str | None + window_command: str | None + x: int | t.Literal["-"] | None + y: int | t.Literal["-"] | None + environment: dict[str, str] | None + + +class TempWindowParams(t.TypedDict, total=False): + """Keyword arguments for :func:`temp_window`.""" + + window_name: str | None + start_directory: StrPath | None + attach: bool + window_index: str + window_shell: str | None + environment: dict[str, str] | None + direction: WindowDirection | None + target_window: str | None + + @contextlib.contextmanager +def _temp_session( + server: Server, + *args: t.Any, + **kwargs: t.Any, +) -> t.Iterator[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() + + +@t.overload +def temp_session( + server: Server, + **kwargs: Unpack[TempSessionParams], +) -> contextlib.AbstractContextManager[Session]: ... + + +@t.overload +def temp_session( + server: Server, + *args: t.Any, + **kwargs: t.Any, +) -> contextlib.AbstractContextManager[Session]: ... + + def temp_session( server: Server, *args: t.Any, **kwargs: t.Any, -) -> Generator[Session, None, None]: +) -> contextlib.AbstractContextManager[Session]: """ Return a context manager with a temporary session. @@ -58,27 +122,54 @@ def temp_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") + return _temp_session(server, *args, **kwargs) + + +@contextlib.contextmanager +def _temp_window( + session: Session, + *args: t.Any, + **kwargs: t.Any, +) -> t.Iterator[Window]: + if "window_name" not in kwargs: + window_name = get_test_window_name(session) else: - session_name = get_test_session_name(server) + window_name = kwargs.pop("window_name") - session = server.new_session(session_name, *args, **kwargs) + 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 session + yield window finally: - if server.has_session(session_name): - session.kill() - return + if len(session.windows.filter(window_id=window_id)) > 0: + window.kill() -@contextlib.contextmanager +@t.overload +def temp_window( + session: Session, + **kwargs: Unpack[TempWindowParams], +) -> contextlib.AbstractContextManager[Window]: ... + + +@t.overload def temp_window( session: Session, *args: t.Any, **kwargs: t.Any, -) -> Generator[Window, None, None]: +) -> contextlib.AbstractContextManager[Window]: ... + + +def temp_window( + session: Session, + *args: t.Any, + **kwargs: t.Any, +) -> contextlib.AbstractContextManager[Window]: """ Return a context manager with a temporary window. @@ -115,21 +206,4 @@ def temp_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 + return _temp_window(session, *args, **kwargs) From 681265c949f489b55bef7ad4be4f2ae06ec2f1e8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:08:34 -0600 Subject: [PATCH 13/19] Tests(refactor[typing]): Tighten QueryList filter param types why: Replace Any with object-based aliases for broader but explicit typing. what: - Define FilterExpr/ExpectedResult aliases in QueryList tests - Use list[object] and QueryList[object] in test_filter signature --- tests/_internal/test_query_list.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/_internal/test_query_list.py b/tests/_internal/test_query_list.py index 9559be963..5934c3172 100644 --- a/tests/_internal/test_query_list.py +++ b/tests/_internal/test_query_list.py @@ -12,7 +12,11 @@ ) if t.TYPE_CHECKING: - from collections.abc import Callable + pass + + +FilterExpr: t.TypeAlias = t.Callable[[object], bool] | object | None +ExpectedResult: t.TypeAlias = QueryList[object] | list[object] @dataclasses.dataclass @@ -254,9 +258,9 @@ class Obj: ], ) def test_filter( - items: list[dict[str, t.Any]], - filter_expr: Callable[[t.Any], bool] | t.Any | None, - expected_result: QueryList[t.Any] | list[dict[str, t.Any]], + items: list[object], + filter_expr: FilterExpr, + expected_result: ExpectedResult, ) -> None: qs = QueryList(items) if filter_expr is not None: From 9e77391a45ec79619f5d847025856b709f5f6915 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:15:17 -0600 Subject: [PATCH 14/19] TestHelpers(refactor[typing]): Tighten temp helper kwargs why: Remove Any from temp helper kwargs while preserving flexible passthrough. what: - Split TempSession/TempWindow kwargs into forwarded TypedDicts - Filter name keys before forwarding to avoid duplicate keywords --- src/libtmux/test/temporary.py | 71 ++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/src/libtmux/test/temporary.py b/src/libtmux/test/temporary.py index af6d52f47..b30910f55 100644 --- a/src/libtmux/test/temporary.py +++ b/src/libtmux/test/temporary.py @@ -39,6 +39,19 @@ class TempSessionParams(t.TypedDict, total=False): environment: dict[str, str] | None +class TempSessionKwargs(t.TypedDict, total=False): + """Keyword arguments forwarded to :meth:`Server.new_session`.""" + + kill_session: bool + attach: bool + start_directory: StrPath | None + window_name: str | None + window_command: str | None + x: int | t.Literal["-"] | None + y: int | t.Literal["-"] | None + environment: dict[str, str] | None + + class TempWindowParams(t.TypedDict, total=False): """Keyword arguments for :func:`temp_window`.""" @@ -52,23 +65,44 @@ class TempWindowParams(t.TypedDict, total=False): target_window: str | None +class TempWindowKwargs(t.TypedDict, total=False): + """Keyword arguments forwarded to :meth:`Session.new_window`.""" + + start_directory: StrPath | None + attach: bool + window_index: str + window_shell: str | None + environment: dict[str, str] | None + direction: WindowDirection | None + target_window: str | None + + @contextlib.contextmanager def _temp_session( server: Server, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> t.Iterator[Session]: - if "session_name" in kwargs: - session_name = kwargs.pop("session_name") + kwargs_typed = t.cast("TempSessionParams", dict(kwargs)) + if "session_name" in kwargs_typed: + session_name = kwargs_typed["session_name"] else: session_name = get_test_session_name(server) + kwargs_no_name = t.cast( + "TempSessionKwargs", + {k: v for k, v in kwargs_typed.items() if k != "session_name"}, + ) - session = server.new_session(session_name, *args, **kwargs) + session = server.new_session( + session_name, + *args, + **kwargs_no_name, + ) try: yield session finally: - if server.has_session(session_name): + if isinstance(session_name, str) and server.has_session(session_name): session.kill() @@ -83,14 +117,14 @@ def temp_session( def temp_session( server: Server, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> contextlib.AbstractContextManager[Session]: ... def temp_session( server: Server, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> contextlib.AbstractContextManager[Session]: """ Return a context manager with a temporary session. @@ -129,14 +163,23 @@ def temp_session( def _temp_window( session: Session, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> t.Iterator[Window]: - if "window_name" not in kwargs: - window_name = get_test_window_name(session) + kwargs_typed = t.cast("TempWindowParams", dict(kwargs)) + if "window_name" in kwargs_typed: + window_name = kwargs_typed["window_name"] else: - window_name = kwargs.pop("window_name") + window_name = get_test_window_name(session) + kwargs_no_name = t.cast( + "TempWindowKwargs", + {k: v for k, v in kwargs_typed.items() if k != "window_name"}, + ) - window = session.new_window(window_name, *args, **kwargs) + window = session.new_window( + window_name, + *args, + **kwargs_no_name, + ) # Get ``window_id`` before returning it, it may be killed within context. window_id = window.window_id @@ -161,14 +204,14 @@ def temp_window( def temp_window( session: Session, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> contextlib.AbstractContextManager[Window]: ... def temp_window( session: Session, *args: t.Any, - **kwargs: t.Any, + **kwargs: object, ) -> contextlib.AbstractContextManager[Window]: """ Return a context manager with a temporary window. From 2d91429e0058531356af71b86a80225c618365a7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:27:45 -0600 Subject: [PATCH 15/19] Exc(refactor[typing]): Narrow VariableUnpackingError variable type why: Use object for error payloads instead of Any to avoid implicit typing. what: - Update VariableUnpackingError variable parameter to object | None --- 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 6a47b247c..983aca87e 100644 --- a/src/libtmux/exc.py +++ b/src/libtmux/exc.py @@ -124,7 +124,7 @@ class WaitTimeout(LibTmuxException): class VariableUnpackingError(LibTmuxException): """Error unpacking variable.""" - def __init__(self, variable: t.Any | None = None, *args: object) -> None: + def __init__(self, variable: object | None = None, *args: object) -> None: return super().__init__(f"Unexpected variable: {variable!s}") From 5ce7637cac8773bb26898ab35d4544cc9e5fd41f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:30:08 -0600 Subject: [PATCH 16/19] Session(refactor[typing]): Narrow deprecated lookup signatures why: Deprecated APIs still surface in docs; clearer types help readers. what: - Replace Any with object for get and __getitem__ - Use dict[str, object] for where and find_where parameters --- src/libtmux/session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libtmux/session.py b/src/libtmux/session.py index fe49332c2..38ea8abc6 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -698,7 +698,7 @@ def kill_session(self) -> None: version="0.30.0", ) - def get(self, key: str, default: t.Any | None = None) -> t.Any: + def get(self, key: str, default: object | None = None) -> object: """Return key-based lookup. Deprecated by attributes. .. deprecated:: 0.17 @@ -713,7 +713,7 @@ def get(self, key: str, default: t.Any | None = None) -> t.Any: version="0.17.0", ) - def __getitem__(self, key: str) -> t.Any: + def __getitem__(self, key: str) -> object: """Return item lookup by key. Deprecated in favor of attributes. .. deprecated:: 0.17 @@ -742,7 +742,7 @@ def get_by_id(self, session_id: str) -> Window | None: version="0.16.0", ) - def where(self, kwargs: dict[str, t.Any]) -> list[Window]: + def where(self, kwargs: dict[str, object]) -> list[Window]: """Filter through windows, return list of :class:`Window`. .. deprecated:: 0.17 @@ -756,7 +756,7 @@ def where(self, kwargs: dict[str, t.Any]) -> list[Window]: version="0.17.0", ) - def find_where(self, kwargs: dict[str, t.Any]) -> Window | None: + def find_where(self, kwargs: dict[str, object]) -> Window | None: """Filter through windows, return first :class:`Window`. .. deprecated:: 0.17 From e70c3e9997e1060e10f8d1af63a5c86153e17515 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:33:02 -0600 Subject: [PATCH 17/19] Window(refactor[typing]): Narrow deprecated lookup signatures why: Deprecated methods still appear in docs; more precise types aid usage. what: - Replace Any with object for get and __getitem__ - Use dict[str, object] for where and find_where parameters --- src/libtmux/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 64c251d55..c98edab7d 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -910,7 +910,7 @@ def show_window_option( global_=g, ) - def get(self, key: str, default: t.Any | None = None) -> t.Any: + def get(self, key: str, default: object | None = None) -> object: """Return key-based lookup. Deprecated by attributes. .. deprecated:: 0.17 @@ -925,7 +925,7 @@ def get(self, key: str, default: t.Any | None = None) -> t.Any: version="0.17.0", ) - def __getitem__(self, key: str) -> t.Any: + def __getitem__(self, key: str) -> object: """Return item lookup by key. Deprecated in favor of attributes. .. deprecated:: 0.17 @@ -954,7 +954,7 @@ def get_by_id(self, pane_id: str) -> Pane | None: version="0.16.0", ) - def where(self, kwargs: dict[str, t.Any]) -> list[Pane]: + def where(self, kwargs: dict[str, object]) -> list[Pane]: """Filter through panes, return list of :class:`Pane`. .. deprecated:: 0.17 @@ -968,7 +968,7 @@ def where(self, kwargs: dict[str, t.Any]) -> list[Pane]: version="0.17.0", ) - def find_where(self, kwargs: dict[str, t.Any]) -> Pane | None: + def find_where(self, kwargs: dict[str, object]) -> Pane | None: """Filter through panes, return first :class:`Pane`. .. deprecated:: 0.17 From 654c60b6a003842c3bbed0a0e19d04e72838fa64 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:34:05 -0600 Subject: [PATCH 18/19] Pane(refactor[typing]): Narrow deprecated lookup signatures why: Deprecated lookup helpers are still documented; tighter types are clearer. what: - Replace Any with object for get and __getitem__ defaults/returns --- src/libtmux/pane.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 351a3333f..cddbc9bc2 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -978,7 +978,7 @@ def split_window( version="0.33.0", ) - def get(self, key: str, default: t.Any | None = None) -> t.Any: + def get(self, key: str, default: object | None = None) -> object: """Return key-based lookup. Deprecated by attributes. .. deprecated:: 0.17 @@ -993,7 +993,7 @@ def get(self, key: str, default: t.Any | None = None) -> t.Any: version="0.17.0", ) - def __getitem__(self, key: str) -> t.Any: + def __getitem__(self, key: str) -> object: """Return item lookup by key. Deprecated in favor of attributes. .. deprecated:: 0.17 From 24ee357d73b4b0e5f066ccbc8ce4e77ebb17d566 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 3 Jan 2026 07:35:19 -0600 Subject: [PATCH 19/19] Server(refactor[typing]): Narrow deprecated filter signatures why: Deprecated API hints should be specific even if the runtime path is unused. what: - Replace Any with object in where and find_where parameter types --- src/libtmux/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libtmux/server.py b/src/libtmux/server.py index 71f9f84a7..ee0d237e9 100644 --- a/src/libtmux/server.py +++ b/src/libtmux/server.py @@ -749,7 +749,7 @@ def get_by_id(self, session_id: str) -> Session | None: version="0.16.0", ) - def where(self, kwargs: dict[str, t.Any]) -> list[Session]: + def where(self, kwargs: dict[str, object]) -> list[Session]: """Filter through sessions, return list of :class:`Session`. .. deprecated:: 0.17 @@ -763,7 +763,7 @@ def where(self, kwargs: dict[str, t.Any]) -> list[Session]: version="0.17.0", ) - def find_where(self, kwargs: dict[str, t.Any]) -> Session | None: + def find_where(self, kwargs: dict[str, object]) -> Session | None: """Filter through sessions, return first :class:`Session`. .. deprecated:: 0.17