Skip to content

Commit e74b8ab

Browse files
committed
feat: make tmux_cmd_async awaitable for typing
1 parent 469ff11 commit e74b8ab

File tree

11 files changed

+189
-139
lines changed

11 files changed

+189
-139
lines changed

conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010

1111
from __future__ import annotations
1212

13+
import asyncio
1314
import shutil
1415
import typing as t
1516

1617
import pytest
18+
import pytest_asyncio
1719
from _pytest.doctest import DoctestItem
1820

21+
from libtmux.common_async import get_version, tmux_cmd_async
1922
from libtmux.pane import Pane
2023
from libtmux.pytest_plugin import USING_ZSH
2124
from libtmux.server import Server
@@ -50,7 +53,6 @@ def add_doctest_fixtures(
5053

5154
# Add async support for async doctests
5255
doctest_namespace["asyncio"] = asyncio
53-
from libtmux.common_async import tmux_cmd_async, get_version
5456
doctest_namespace["tmux_cmd_async"] = tmux_cmd_async
5557
doctest_namespace["get_version"] = get_version
5658

@@ -83,9 +85,6 @@ def setup_session(
8385

8486
# Async test fixtures
8587
# These require pytest-asyncio to be installed
86-
import asyncio
87-
88-
import pytest_asyncio
8988

9089

9190
@pytest_asyncio.fixture

examples/async_demo.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
from __future__ import annotations
88

99
import asyncio
10+
import contextlib
1011
import sys
12+
import time
1113
from pathlib import Path
1214

1315
# Try importing from installed package, fallback to development mode
1416
try:
15-
from libtmux.common_async import tmux_cmd_async, get_version
17+
from libtmux.common_async import get_version, tmux_cmd_async
1618
except ImportError:
1719
# Development mode: add parent to path
1820
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
19-
from libtmux.common_async import tmux_cmd_async, get_version
21+
from libtmux.common_async import get_version, tmux_cmd_async
2022

2123

2224
async def demo_basic_command() -> None:
@@ -60,7 +62,7 @@ async def demo_concurrent_commands() -> None:
6062
)
6163

6264
commands = ["list-sessions", "list-windows", "list-panes", "show-options -g"]
63-
for cmd, result in zip(commands, results):
65+
for cmd, result in zip(commands, results, strict=True):
6466
if isinstance(result, Exception):
6567
print(f"\n[{cmd}] Error: {result}")
6668
else:
@@ -75,7 +77,6 @@ async def demo_comparison_with_sync() -> None:
7577
print("Demo 3: Performance Comparison")
7678
print("=" * 60)
7779

78-
import time
7980
from libtmux.common import tmux_cmd
8081

8182
# Commands to run
@@ -95,10 +96,8 @@ async def demo_comparison_with_sync() -> None:
9596
print("\nSync execution (sequential)...")
9697
start = time.time()
9798
for cmd in commands:
98-
try:
99+
with contextlib.suppress(Exception):
99100
tmux_cmd(*cmd.split())
100-
except Exception:
101-
pass
102101
sync_time = time.time() - start
103102
print(f" Time: {sync_time:.4f} seconds")
104103

@@ -155,6 +154,7 @@ async def main() -> None:
155154
except Exception as e:
156155
print(f"\nDemo failed with error: {e}")
157156
import traceback
157+
158158
traceback.print_exc()
159159

160160

examples/hybrid_async_demo.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@
1212

1313
import asyncio
1414
import sys
15+
import time
1516
from pathlib import Path
1617

1718
# Try importing from installed package, fallback to development mode
1819
try:
19-
from libtmux.common import AsyncTmuxCmd
20-
from libtmux.common_async import tmux_cmd_async, get_version
20+
from libtmux.common_async import get_version, tmux_cmd_async
2121
from libtmux.server import Server
2222
except ImportError:
2323
# Development mode: add parent to path
2424
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
25-
from libtmux.common import AsyncTmuxCmd
26-
from libtmux.common_async import tmux_cmd_async, get_version
25+
from libtmux.common_async import get_version, tmux_cmd_async
2726
from libtmux.server import Server
2827

2928

@@ -56,13 +55,24 @@ async def demo_pattern_a_acmd_methods() -> None:
5655

5756
# Get session details
5857
print("\n2. Getting session details...")
59-
result = await server.acmd("display-message", "-p", "-t", session_id, "-F#{session_name}")
58+
result = await server.acmd(
59+
"display-message",
60+
"-p",
61+
"-t",
62+
session_id,
63+
"-F#{session_name}",
64+
)
6065
session_name = result.stdout[0] if result.stdout else "unknown"
6166
print(f" Session name: {session_name}")
6267

6368
# List windows
6469
print("\n3. Listing windows in session...")
65-
result = await server.acmd("list-windows", "-t", session_id, "-F#{window_index}:#{window_name}")
70+
result = await server.acmd(
71+
"list-windows",
72+
"-t",
73+
session_id,
74+
"-F#{window_index}:#{window_name}",
75+
)
6676
print(f" Found {len(result.stdout)} windows")
6777
for window in result.stdout:
6878
print(f" - {window}")
@@ -182,12 +192,10 @@ async def demo_performance_comparison() -> None:
182192
print("=" * 70)
183193
print()
184194

185-
import time
186-
187195
# Create test sessions
188196
print("Setting up test sessions...")
189197
sessions = []
190-
for i in range(4):
198+
for _ in range(4):
191199
cmd = await tmux_cmd_async("new-session", "-d", "-P", "-F#{session_id}")
192200
sessions.append(cmd.stdout[0])
193201
print(f"Created {len(sessions)} test sessions")
@@ -203,10 +211,9 @@ async def demo_performance_comparison() -> None:
203211
# Parallel execution
204212
print("\n2. Parallel execution (all at once)...")
205213
start = time.time()
206-
await asyncio.gather(*[
207-
tmux_cmd_async("list-windows", "-t", session_id)
208-
for session_id in sessions
209-
])
214+
await asyncio.gather(
215+
*[tmux_cmd_async("list-windows", "-t", session_id) for session_id in sessions]
216+
)
210217
parallel_time = time.time() - start
211218
print(f" Time: {parallel_time:.4f} seconds")
212219

@@ -216,10 +223,9 @@ async def demo_performance_comparison() -> None:
216223

217224
# Cleanup
218225
print("\nCleaning up test sessions...")
219-
await asyncio.gather(*[
220-
tmux_cmd_async("kill-session", "-t", session_id)
221-
for session_id in sessions
222-
])
226+
await asyncio.gather(
227+
*[tmux_cmd_async("kill-session", "-t", session_id) for session_id in sessions]
228+
)
223229

224230

225231
async def main() -> None:
@@ -265,6 +271,7 @@ async def main() -> None:
265271
except Exception as e:
266272
print(f"\n❌ Demo failed with error: {e}")
267273
import traceback
274+
268275
traceback.print_exc()
269276

270277

examples/test_examples.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ def test_examples_directory_structure() -> None:
6565
"""Verify examples directory has expected structure."""
6666
assert EXAMPLES_DIR.exists(), "Examples directory not found"
6767
assert (EXAMPLES_DIR / "async_demo.py").exists(), "async_demo.py not found"
68-
assert (
69-
EXAMPLES_DIR / "hybrid_async_demo.py"
70-
).exists(), "hybrid_async_demo.py not found"
68+
assert (EXAMPLES_DIR / "hybrid_async_demo.py").exists(), (
69+
"hybrid_async_demo.py not found"
70+
)
7171

7272

7373
def test_example_has_docstring() -> None:
@@ -80,9 +80,7 @@ def test_example_has_docstring() -> None:
8080
assert '"""' in content, f"{script} missing docstring"
8181

8282
# Check for shebang (makes it executable)
83-
assert content.startswith("#!/usr/bin/env python"), (
84-
f"{script} missing shebang"
85-
)
83+
assert content.startswith("#!/usr/bin/env python"), f"{script} missing shebang"
8684

8785

8886
def test_example_is_self_contained() -> None:

src/libtmux/common_async.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@
6565
import shutil
6666
import sys
6767
import typing as t
68+
from collections.abc import Awaitable, Generator
6869

6970
from . import exc
7071
from ._compat import LooseVersion
7172

7273
if t.TYPE_CHECKING:
73-
from collections.abc import Callable, Coroutine
74+
from collections.abc import Callable
7475

7576
logger = logging.getLogger(__name__)
7677

@@ -88,11 +89,11 @@
8889

8990

9091
class AsyncEnvironmentMixin:
91-
"""Async mixin for manager session and server level environment variables in tmux."""
92+
"""Async mixin for managing session and server-level environment variables."""
9293

9394
_add_option = None
9495

95-
acmd: Callable[[t.Any, t.Any], Coroutine[t.Any, t.Any, tmux_cmd_async]]
96+
acmd: Callable[[t.Any, t.Any], Awaitable[tmux_cmd_async]]
9697

9798
def __init__(self, add_option: str | None = None) -> None:
9899
self._add_option = add_option
@@ -179,7 +180,8 @@ async def show_environment(self) -> dict[str, bool | str]:
179180
180181
.. versionchanged:: 0.13
181182
182-
Removed per-item lookups. Use :meth:`libtmux.common_async.AsyncEnvironmentMixin.getenv`.
183+
Removed per-item lookups.
184+
Use :meth:`libtmux.common_async.AsyncEnvironmentMixin.getenv`.
183185
184186
Returns
185187
-------
@@ -242,7 +244,7 @@ async def getenv(self, name: str) -> str | bool | None:
242244
return opts_dict.get(name)
243245

244246

245-
class tmux_cmd_async:
247+
class tmux_cmd_async(Awaitable["tmux_cmd_async"]):
246248
"""Run any :term:`tmux(1)` command through :py:mod:`asyncio.subprocess`.
247249
248250
This is the async-first implementation. The tmux_cmd class is auto-generated
@@ -254,7 +256,9 @@ class tmux_cmd_async:
254256
255257
>>> async def basic_example():
256258
... # Execute command with isolated socket
257-
... proc = await tmux_cmd_async('-L', server.socket_name, 'new-session', '-d', '-P', '-F#S')
259+
... proc = await tmux_cmd_async(
260+
... '-L', server.socket_name, 'new-session', '-d', '-P', '-F#S'
261+
... )
258262
... # Verify command executed successfully
259263
... return len(proc.stdout) > 0 and not proc.stderr
260264
>>> asyncio.run(basic_example())
@@ -279,7 +283,9 @@ class tmux_cmd_async:
279283
>>> async def check_session():
280284
... # Non-existent session returns non-zero returncode
281285
... sock = server.socket_name
282-
... result = await tmux_cmd_async('-L', sock, 'has-session', '-t', 'nonexistent_12345')
286+
... result = await tmux_cmd_async(
287+
... '-L', sock, 'has-session', '-t', 'nonexistent_12345'
288+
... )
283289
... return result.returncode != 0
284290
>>> asyncio.run(check_session())
285291
True
@@ -341,10 +347,10 @@ def __init__(
341347
self.returncode = returncode
342348
self._executed = False
343349

344-
async def execute(self) -> None:
350+
async def execute(self) -> tmux_cmd_async:
345351
"""Execute the tmux command asynchronously."""
346352
if self._executed:
347-
return
353+
return self
348354

349355
try:
350356
process = await asyncio.create_subprocess_exec(
@@ -361,6 +367,15 @@ async def execute(self) -> None:
361367
raise
362368

363369
self._executed = True
370+
return self
371+
372+
async def _run(self) -> tmux_cmd_async:
373+
await self.execute()
374+
return self
375+
376+
def __await__(self) -> Generator[t.Any, None, tmux_cmd_async]:
377+
"""Allow ``await tmux_cmd_async(...)`` to execute the command."""
378+
return self._run().__await__()
364379

365380
@property
366381
def stdout(self) -> list[str]:
@@ -387,12 +402,9 @@ def stderr(self) -> list[str]:
387402
stderr_split = self._stderr.split("\n")
388403
return list(filter(None, stderr_split)) # filter empty values
389404

390-
async def __new__(cls, *args: t.Any, **kwargs: t.Any) -> tmux_cmd_async:
391-
"""Create and execute tmux command asynchronously."""
392-
instance = object.__new__(cls)
393-
instance.__init__(*args, **kwargs)
394-
await instance.execute()
395-
return instance
405+
def __new__(cls, *args: t.Any, **kwargs: t.Any) -> tmux_cmd_async:
406+
"""Create tmux command instance (execution happens when awaited)."""
407+
return super().__new__(cls)
396408

397409

398410
async def get_version() -> LooseVersion:

tests/asyncio/test_acmd.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,7 @@ async def test_concurrent_acmd_operations(async_server: Server) -> None:
142142
async def test_acmd_error_handling(async_server: Server) -> None:
143143
"""Test .acmd() properly handles errors."""
144144
# Create a session first to ensure server socket exists
145-
result = await async_server.acmd("new-session", "-d", "-P", "-F#{session_id}")
146-
session_id = result.stdout[0]
145+
await async_server.acmd("new-session", "-d", "-P", "-F#{session_id}")
147146

148147
# Invalid command (server socket now exists)
149148
result = await async_server.acmd("invalid-command-12345")

0 commit comments

Comments
 (0)