From 7b3ff2642c0c3c7eecb6b67c4a9c87206f837e7a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 25 Dec 2024 06:51:25 -0600 Subject: [PATCH 1/8] common(cmd) AsyncTmuxCmd --- src/libtmux/common.py | 133 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 06c92320f..db43004d0 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -7,6 +7,7 @@ from __future__ import annotations +import asyncio import functools import logging import re @@ -378,6 +379,138 @@ def __init__(self, *args: t.Any, tmux_bin: str | None = None) -> None: ) +class AsyncTmuxCmd: + """An asyncio-compatible class for running any tmux command via subprocess. + + Attributes + ---------- + cmd : list[str] + The full command (including the "tmux" binary path). + stdout : list[str] + Lines of stdout output from tmux. + stderr : list[str] + Lines of stderr output from tmux. + returncode : int + The process return code. + + Examples + -------- + >>> import asyncio + >>> + >>> async def main(): + ... proc = await AsyncTmuxCmd.run('-V') + ... if proc.stderr: + ... raise exc.LibTmuxException( + ... f"Error invoking tmux: {proc.stderr}" + ... ) + ... print("tmux version:", proc.stdout) + ... + >>> asyncio.run(main()) + tmux version: [...] + + This is equivalent to calling: + + .. code-block:: console + + $ tmux -V + """ + + def __init__( + self, + cmd: list[str], + stdout: list[str], + stderr: list[str], + returncode: int, + ) -> None: + """Store the results of a completed tmux subprocess run. + + Parameters + ---------- + cmd : list[str] + The command used to invoke tmux. + stdout : list[str] + Captured lines from tmux stdout. + stderr : list[str] + Captured lines from tmux stderr. + returncode : int + Subprocess exit code. + """ + self.cmd: list[str] = cmd + self.stdout: list[str] = stdout + self.stderr: list[str] = stderr + self.returncode: int = returncode + + @classmethod + async def run(cls, *args: t.Any) -> AsyncTmuxCmd: + """Execute a tmux command asynchronously and capture its output. + + Parameters + ---------- + *args : str + Arguments to be passed after the "tmux" binary name. + + Returns + ------- + AsyncTmuxCmd + An instance containing the cmd, stdout, stderr, and returncode. + + Raises + ------ + exc.TmuxCommandNotFound + If no "tmux" executable is found in the user's PATH. + exc.LibTmuxException + If there's any unexpected exception creating or communicating + with the tmux subprocess. + """ + tmux_bin: str | None = shutil.which("tmux") + if not tmux_bin: + msg = "tmux executable not found in PATH" + raise exc.TmuxCommandNotFound( + msg, + ) + + cmd: list[str] = [tmux_bin] + [str_from_console(a) for a in args] + + try: + process: asyncio.subprocess.Process = ( + await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + ) + raw_stdout, raw_stderr = await process.communicate() + returncode: int = ( + process.returncode if process.returncode is not None else -1 + ) + + except Exception as e: + logger.exception("Exception for %s", " ".join(cmd)) + msg = f"Exception while running tmux command: {e}" + raise exc.LibTmuxException( + msg, + ) from e + + stdout_str: str = console_to_str(raw_stdout) + stderr_str: str = console_to_str(raw_stderr) + + stdout_split: list[str] = [line for line in stdout_str.split("\n") if line] + stderr_split: list[str] = [line for line in stderr_str.split("\n") if line] + + if "has-session" in cmd and stderr_split and not stdout_split: + stdout_split = [stderr_split[0]] + + logger.debug("stdout for %s: %s", " ".join(cmd), stdout_split) + logger.debug("stderr for %s: %s", " ".join(cmd), stderr_split) + + return cls( + cmd=cmd, + stdout=stdout_split, + stderr=stderr_split, + returncode=returncode, + ) + + @functools.cache def get_version(tmux_bin: str | None = None) -> LooseVersion: """Return tmux version. From 2fa56657212aa0c33c528cf9d0a42067d2f0e853 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 25 Dec 2024 07:00:30 -0600 Subject: [PATCH 2/8] Server,Session,Window,Pane: Add `.acmd` --- src/libtmux/pane.py | 49 ++++++++++++++++++++- src/libtmux/server.py | 97 ++++++++++++++++++++++++++++++++++++++++-- src/libtmux/session.py | 57 +++++++++++++++++++++++++ src/libtmux/window.py | 51 +++++++++++++++++++++- 4 files changed, 249 insertions(+), 5 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index e5833111f..b1c8d77bf 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, raise_if_stderr, tmux_cmd +from libtmux.common import AsyncTmuxCmd, has_gte_version, raise_if_stderr, tmux_cmd from libtmux.constants import ( PANE_DIRECTION_FLAG_MAP, RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, @@ -220,6 +220,53 @@ def cmd( return self.server.cmd(cmd, *args, target=target) + async def acmd( + self, + cmd: str, + *args: t.Any, + target: str | int | None = None, + ) -> AsyncTmuxCmd: + """Execute tmux subcommand within pane context. + + Automatically binds target by adding ``-t`` for object's pane ID to the + command. Pass ``target`` to keyword arguments to override. + + Examples + -------- + >>> import asyncio + >>> async def test_acmd(): + ... result = await pane.acmd('split-window', '-P') + ... print(result.stdout[0]) + >>> asyncio.run(test_acmd()) + libtmux...:... + + From raw output to an enriched `Pane` object: + + >>> async def test_from_pane(): + ... pane_id_result = await pane.acmd( + ... 'split-window', '-P', '-F#{pane_id}' + ... ) + ... return Pane.from_pane_id( + ... pane_id=pane_id_result.stdout[0], + ... server=session.server + ... ) + >>> asyncio.run(test_from_pane()) + Pane(%... Window(@... ...:..., Session($1 libtmux_...))) + + Parameters + ---------- + target : str, optional + Optional custom target override. By default, the target is the pane ID. + + Returns + ------- + :meth:`server.cmd` + """ + if target is None: + target = self.pane_id + + return await self.server.acmd(cmd, *args, target=target) + """ Commands (tmux-like) """ diff --git a/src/libtmux/server.py b/src/libtmux/server.py index 30ec0d699..916b6944e 100644 --- a/src/libtmux/server.py +++ b/src/libtmux/server.py @@ -27,6 +27,7 @@ from libtmux.window import Window from .common import ( + AsyncTmuxCmd, EnvironmentMixin, PaneDict, SessionDict, @@ -311,8 +312,12 @@ def cmd( Output of `tmux -L ... new-window -P -F#{window_id}` to a `Window` object: - >>> Window.from_window_id(window_id=session.cmd( - ... 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server) + >>> Window.from_window_id( + ... window_id=session.cmd( + ... 'new-window', '-P', '-F#{window_id}' + ... ).stdout[0], + ... server=session.server, + ... ) Window(@4 3:..., Session($1 libtmux_...)) Create a pane from a window: @@ -323,7 +328,9 @@ def cmd( Output of `tmux -L ... split-window -P -F#{pane_id}` to a `Pane` object: >>> Pane.from_pane_id(pane_id=window.cmd( - ... 'split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server) + ... 'split-window', '-P', '-F#{pane_id}').stdout[0], + ... server=window.server + ... ) Pane(%... Window(@... ...:..., Session($1 libtmux_...))) Parameters @@ -361,6 +368,90 @@ def cmd( return tmux_cmd(*svr_args, *cmd_args, tmux_bin=self.tmux_bin) + async def acmd( + self, + cmd: str, + *args: t.Any, + target: str | int | None = None, + ) -> AsyncTmuxCmd: + """Execute tmux command respective of socket name and file, return output. + + Examples + -------- + >>> import asyncio + >>> async def test_acmd(): + ... result = await server.acmd('display-message', 'hi') + ... print(result.stdout) + >>> asyncio.run(test_acmd()) + [] + + New session: + + >>> async def test_new_session(): + ... result = await server.acmd( + ... 'new-session', '-d', '-P', '-F#{session_id}' + ... ) + ... print(result.stdout[0]) + >>> asyncio.run(test_new_session()) + $... + + Output of `tmux -L ... new-window -P -F#{window_id}` to a `Window` object: + + >>> async def test_new_window(): + ... result = await session.acmd('new-window', '-P', '-F#{window_id}') + ... window_id = result.stdout[0] + ... window = Window.from_window_id(window_id=window_id, server=server) + ... print(window) + >>> asyncio.run(test_new_window()) + Window(@... ...:..., Session($... libtmux_...)) + + Create a pane from a window: + + >>> async def test_split_window(): + ... result = await server.acmd('split-window', '-P', '-F#{pane_id}') + ... print(result.stdout[0]) + >>> asyncio.run(test_split_window()) + %... + + Output of `tmux -L ... split-window -P -F#{pane_id}` to a `Pane` object: + + >>> async def test_pane(): + ... result = await window.acmd('split-window', '-P', '-F#{pane_id}') + ... pane_id = result.stdout[0] + ... pane = Pane.from_pane_id(pane_id=pane_id, server=server) + ... print(pane) + >>> asyncio.run(test_pane()) + Pane(%... Window(@... ...:..., Session($1 libtmux_...))) + + Parameters + ---------- + target : str, optional + Optional custom target. + + Returns + ------- + :class:`common.AsyncTmuxCmd` + """ + svr_args: list[str | int] = [cmd] + cmd_args: list[str | int] = [] + if self.socket_name: + svr_args.insert(0, f"-L{self.socket_name}") + if self.socket_path: + svr_args.insert(0, f"-S{self.socket_path}") + if self.config_file: + svr_args.insert(0, f"-f{self.config_file}") + if self.colors: + if self.colors == 256: + svr_args.insert(0, "-2") + elif self.colors == 88: + svr_args.insert(0, "-8") + else: + raise exc.UnknownColorOption + + cmd_args = ["-t", str(target), *args] if target is not None else [*args] + + return await AsyncTmuxCmd.run(*svr_args, *cmd_args) + @property def attached_sessions(self) -> list[Session]: """Return active :class:`Session` instances. diff --git a/src/libtmux/session.py b/src/libtmux/session.py index ff9a851e7..49e427e2a 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -24,6 +24,7 @@ from . import exc from .common import ( + AsyncTmuxCmd, EnvironmentMixin, WindowDict, session_check_name, @@ -361,6 +362,62 @@ def cmd( target = self.session_id return self.server.cmd(cmd, *args, target=target) + async def acmd( + self, + cmd: str, + *args: t.Any, + target: str | int | None = None, + ) -> AsyncTmuxCmd: + """Execute tmux subcommand within session context. + + Automatically binds target by adding ``-t`` for object's session ID to the + command. Pass ``target`` to keyword arguments to override. + + Examples + -------- + >>> import asyncio + >>> async def test_acmd(): + ... result = await session.acmd('new-window', '-P') + ... print(result.stdout[0]) + >>> asyncio.run(test_acmd()) + libtmux...:....0 + + From raw output to an enriched `Window` object: + + >>> async def test_from_window(): + ... window_id_result = await session.acmd( + ... 'new-window', '-P', '-F#{window_id}' + ... ) + ... return Window.from_window_id( + ... window_id=window_id_result.stdout[0], + ... server=session.server + ... ) + >>> asyncio.run(test_from_window()) + Window(@... ...:..., Session($1 libtmux_...)) + + Parameters + ---------- + target : str, optional + Optional custom target override. By default, the target is the session ID. + + Returns + ------- + :meth:`server.cmd` + + Notes + ----- + .. versionchanged:: 0.34 + + Passing target by ``-t`` is ignored. Use ``target`` keyword argument instead. + + .. versionchanged:: 0.8 + + Renamed from ``.tmux`` to ``.cmd``. + """ + if target is None: + target = self.session_id + return await self.server.acmd(cmd, *args, target=target) + """ Commands (tmux-like) """ diff --git a/src/libtmux/window.py b/src/libtmux/window.py index f0ce1142d..31743f1ea 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -28,7 +28,7 @@ from libtmux.pane import Pane from . import exc -from .common import PaneDict, WindowOptionDict +from .common import AsyncTmuxCmd, PaneDict, WindowOptionDict from .options import OptionsMixin if t.TYPE_CHECKING: @@ -304,6 +304,55 @@ def cmd( return self.server.cmd(cmd, *args, target=target) + async def acmd( + self, + cmd: str, + *args: t.Any, + target: str | int | None = None, + ) -> AsyncTmuxCmd: + """Execute tmux subcommand within window context. + + Automatically binds target by adding ``-t`` for object's window ID to the + command. Pass ``target`` to keyword arguments to override. + + Examples + -------- + Create a pane from a window: + + >>> import asyncio + >>> async def test_acmd(): + ... result = await window.acmd('split-window', '-P', '-F#{pane_id}') + ... print(result.stdout[0]) + >>> asyncio.run(test_acmd()) + %... + + Magic, directly to a `Pane`: + + >>> async def test_from_pane(): + ... pane_id_result = await session.acmd( + ... 'split-window', '-P', '-F#{pane_id}' + ... ) + ... return Pane.from_pane_id( + ... pane_id=pane_id_result.stdout[0], + ... server=session.server + ... ) + >>> asyncio.run(test_from_pane()) + Pane(%... Window(@... ...:..., Session($1 libtmux_...))) + + Parameters + ---------- + target : str, optional + Optional custom target override. By default, the target is the window ID. + + Returns + ------- + :meth:`server.cmd` + """ + if target is None: + target = self.window_id + + return await self.server.acmd(cmd, *args, target=target) + """ Commands (tmux-like) """ From 4cb6868e27c328ae8b0de3ad6a901e9f6b8f291a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 31 Dec 2024 12:50:33 -0600 Subject: [PATCH 3/8] py(deps[dev]) Add `pytest-asyncio` See also: - https://github.com/pytest-dev/pytest-asyncio - https://pypi.python.org/pypi/pytest-asyncio --- pyproject.toml | 2 ++ uv.lock | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5b3b682c2..a44de2c81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "typing-extensions; python_version < '3.11'", "gp-libs", "pytest", + "pytest-asyncio", "pytest-rerunfailures", "pytest-mock", "pytest-watcher", @@ -84,6 +85,7 @@ testing = [ "typing-extensions; python_version < '3.11'", "gp-libs", "pytest", + "pytest-asyncio", "pytest-rerunfailures", "pytest-mock", "pytest-watcher", diff --git a/uv.lock b/uv.lock index ed2b5f74b..0ee785278 100644 --- a/uv.lock +++ b/uv.lock @@ -634,6 +634,7 @@ dev = [ { name = "gp-sphinx" }, { name = "mypy" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, @@ -662,6 +663,7 @@ lint = [ testing = [ { name = "gp-libs" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, @@ -683,6 +685,7 @@ dev = [ { name = "gp-sphinx", specifier = "==0.0.1a26" }, { name = "mypy" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, @@ -709,6 +712,7 @@ lint = [ testing = [ { name = "gp-libs" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, @@ -1028,6 +1032,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, +] + [[package]] name = "pytest-cov" version = "7.1.0" From 6639f6b10b13c41f6cb3eee52b5b8eeb6b297844 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 31 Dec 2024 12:54:38 -0600 Subject: [PATCH 4/8] tests(async) Basic example --- tests/test_async.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_async.py diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 000000000..29a55fdf4 --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,27 @@ +"""Tests for libtmux with :mod`asyncio` support.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import pytest + +from libtmux.session import Session + +if TYPE_CHECKING: + from libtmux.server import Server + +logger = logging.getLogger(__name__) + + +@pytest.mark.asyncio +async def test_asyncio(server: Server) -> None: + """Test basic asyncio usage.""" + result = await server.acmd("new-session", "-d", "-P", "-F#{session_id}") + session_id = result.stdout[0] + session = Session.from_session_id( + session_id=session_id, + server=server, + ) + assert isinstance(session, Session) From e402d703adfee1a5618fd679903d033ff7e48b1b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 04:47:40 -0600 Subject: [PATCH 5/8] AsyncTmuxCmd: Updates for TmuxCmd --- src/libtmux/common.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index db43004d0..9801a0042 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -469,7 +469,7 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: msg, ) - cmd: list[str] = [tmux_bin] + [str_from_console(a) for a in args] + cmd: list[str] = [tmux_bin] + [str(c) for c in args] try: process: asyncio.subprocess.Process = ( @@ -479,7 +479,7 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: stderr=asyncio.subprocess.PIPE, ) ) - raw_stdout, raw_stderr = await process.communicate() + stdout, stderr = await process.communicate() returncode: int = ( process.returncode if process.returncode is not None else -1 ) @@ -491,11 +491,14 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: msg, ) from e - stdout_str: str = console_to_str(raw_stdout) - stderr_str: str = console_to_str(raw_stderr) + # Split on newlines and filter empty lines + stdout_split: list[str] = stdout.split("\n") + # remove trailing newlines from stdout + while stdout_split and stdout_split[-1] == "": + stdout_split.pop() - stdout_split: list[str] = [line for line in stdout_str.split("\n") if line] - stderr_split: list[str] = [line for line in stderr_str.split("\n") if line] + stderr_split = stderr.split("\n") + stderr_split = list(filter(None, stderr_split)) if "has-session" in cmd and stderr_split and not stdout_split: stdout_split = [stderr_split[0]] From 0deba7811e48b88bddf2b0a6ee4128939d7fcfbb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 23 Feb 2025 04:49:31 -0600 Subject: [PATCH 6/8] fix(AsyncTmuxCmd): Handle text decoding manually for async subprocess The AsyncTmuxCmd class was updated to handle text decoding manually since asyncio.create_subprocess_exec() doesn't support the text=True parameter that subprocess.Popen() supports. Changes: - Remove text=True and errors=backslashreplace from create_subprocess_exec() - Handle bytes output by manually decoding with decode(errors="backslashreplace") - Keep string processing logic consistent with tmux_cmd class This fixes the ValueError("text must be False") error that occurred when trying to use text mode with asyncio subprocesses. The async version now properly handles text decoding while maintaining the same behavior as the synchronous tmux_cmd class. --- src/libtmux/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 9801a0042..2289eab9f 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -479,7 +479,7 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: stderr=asyncio.subprocess.PIPE, ) ) - stdout, stderr = await process.communicate() + stdout_bytes, stderr_bytes = await process.communicate() returncode: int = ( process.returncode if process.returncode is not None else -1 ) @@ -491,6 +491,10 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: msg, ) from e + # Decode bytes to string with error handling + stdout = stdout_bytes.decode(errors="backslashreplace") + stderr = stderr_bytes.decode(errors="backslashreplace") + # Split on newlines and filter empty lines stdout_split: list[str] = stdout.split("\n") # remove trailing newlines from stdout From 273ef3f2a9b54a7e0744fe024e3a6fbbed426819 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 26 May 2025 04:17:31 -0500 Subject: [PATCH 7/8] py(deps[dev]) Bump dev packages --- uv.lock | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 0ee785278..1d5de8cc2 100644 --- a/uv.lock +++ b/uv.lock @@ -114,6 +114,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -407,7 +416,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1034,14 +1043,16 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.3" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] From df0a8ac79db14b7ec76f4992d8a23a5ffe88bc71 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 30 May 2026 08:53:45 -0500 Subject: [PATCH 8/8] style(ruff): apply auto-fixes after rebase onto master --- src/libtmux/common.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 2289eab9f..ee3737ad1 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -472,12 +472,10 @@ async def run(cls, *args: t.Any) -> AsyncTmuxCmd: cmd: list[str] = [tmux_bin] + [str(c) for c in args] try: - process: asyncio.subprocess.Process = ( - await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) + process: asyncio.subprocess.Process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) stdout_bytes, stderr_bytes = await process.communicate() returncode: int = (