-
Notifications
You must be signed in to change notification settings - Fork 111
asyncio
: Asynchronous libtmux
#554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
e7fdf41
to
1d31a1f
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #554 +/- ##
==========================================
- Coverage 81.39% 81.02% -0.38%
==========================================
Files 37 37
Lines 2430 2493 +63
Branches 368 380 +12
==========================================
+ Hits 1978 2020 +42
- Misses 310 322 +12
- Partials 142 151 +9 ☔ View full report in Codecov by Sentry. |
d0d85ec
to
1103e6a
Compare
@sourcery-ai review |
Reviewer's Guide by SourceryThis pull request introduces asynchronous functionality to libtmux using Python's Sequence diagram for async tmux command executionsequenceDiagram
participant C as Client Code
participant A as AsyncTmuxCmd
participant S as Subprocess
participant T as tmux
C->>+A: run(*args)
A->>A: validate tmux binary
A->>+S: create_subprocess_exec
S->>+T: execute command
T-->>-S: command output
S-->>-A: stdout, stderr, returncode
A->>A: process output
A-->>-C: AsyncTmuxCmd instance
Class diagram for new AsyncTmuxCmd and its integrationclassDiagram
class AsyncTmuxCmd {
+list[str] cmd
+list[str] stdout
+list[str] stderr
+int returncode
+__init__(cmd, stdout, stderr, returncode)
+run(*args) AsyncTmuxCmd
}
class Server {
+cmd(cmd, *args, target)
+acmd(cmd, *args, target) AsyncTmuxCmd
}
class Session {
+cmd(cmd, *args, target)
+acmd(cmd, *args, target) AsyncTmuxCmd
}
class Window {
+cmd(cmd, *args, target)
+acmd(cmd, *args, target) AsyncTmuxCmd
}
class Pane {
+cmd(cmd, *args, target)
+acmd(cmd, *args, target) AsyncTmuxCmd
}
Server --> AsyncTmuxCmd : uses
Session --> Server : uses
Window --> Server : uses
Pane --> Server : uses
note for AsyncTmuxCmd "New async class for tmux commands"
note for Server "Added async support via acmd"
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @tony - I've reviewed your changes - here's some feedback:
Overall Comments:
- Consider adding more test cases to cover error conditions and different tmux command scenarios in test_async.py
Here's what I looked at during the review
- 🟢 General issues: all looks good
- 🟢 Security: all looks good
- 🟡 Testing: 2 issues found
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
session_id=session_id, | ||
server=server, | ||
) | ||
assert isinstance(session, Session) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Test more aspects of the asynchronous functionality.
The current test only verifies the type of the returned session object. It would be beneficial to interact with the session asynchronously, for example, by creating a window or pane, executing a command within the session, and then verifying the results. This would provide more comprehensive coverage of the asynchronous API.
Suggested implementation:
@pytest.mark.asyncio
async def test_asyncio(server: Server) -> None:
"""Test basic asyncio usage."""
# Create new session
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)
# Create new window
window = await session.new_window(window_name="test_window")
assert window.name == "test_window"
# Create new pane and execute command
pane = await window.split_window()
await pane.send_keys("echo 'Hello, tmux!'")
# Allow time for command execution
await asyncio.sleep(0.5)
# Capture and verify output
output = await pane.capture_pane()
assert "Hello, tmux!" in '\n'.join(output)
# Clean up
await window.kill_window()
You may need to:
- Import asyncio if it's not already imported
- Adjust the sleep duration (0.5s) based on your system's performance
- Ensure the Session class has the async methods new_window(), and Window class has split_window() and kill_window()
- Ensure Pane class has send_keys() and capture_pane() async methods
@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}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Add tests for error handling.
Include tests to verify the behavior of the acmd
function when the tmux command fails, for example, by trying to create a session with an already existing name. Check that the appropriate exceptions are raised and handled correctly.
Suggested implementation:
@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)
@pytest.mark.asyncio
async def test_asyncio_duplicate_session(server: Server) -> None:
"""Test error handling when creating duplicate sessions."""
# Create first session
session_name = "test_duplicate"
await server.acmd("new-session", "-d", "-s", session_name)
# Attempt to create second session with same name
with pytest.raises(LibTmuxException) as excinfo:
await server.acmd("new-session", "-d", "-s", session_name)
assert "duplicate session" in str(excinfo.value).lower()
@pytest.mark.asyncio
async def test_asyncio_invalid_command(server: Server) -> None:
"""Test error handling with invalid tmux commands."""
with pytest.raises(LibTmuxException) as excinfo:
await server.acmd("invalid-command")
assert "unknown command" in str(excinfo.value).lower()
You'll need to:
- Import LibTmuxException if not already imported (likely from libtmux.exc)
- Ensure any existing sessions are cleaned up in test teardown to prevent test interference
3b26b45
to
327b098
Compare
0188aed
to
ea5b0c4
Compare
The pytest-asyncio issue about |
@seifertm Thank you! I will take a look! |
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.
@seifertm Aside, not sure if this worth filing an issue downstream for: I get These are trickier because they're sandbox'd, but still seem to get
|
@tony In pytest-asyncio, we have the same issue. There are plans to remove the deprecation, but until then we use pytester to create a config that silences the warning. Does this help? |
Docs
Asyncio
Adding Asyncio Support to libtmux (Python 3.9+)
Key Functions Requiring Asynchronous Execution
Several core functions in libtmux currently execute tmux commands synchronously (blocking the main thread). These need refactoring for async operation:
libtmux.common.tmux_cmd
– This helper (class/function) runs tmux commands viasubprocess.Popen
and waits for completion. It blocks while reading tmux’s output, so making it async is crucial.libtmux.neo
module – Helper functions (e.g.fetch_obj
, list/query functions for sessions, windows, panes) call tmux’slist-*
commands and parse results synchronously, blocking the thread. These should be awaitable to avoid blocking.libtmux.server.Server
methods – Methods likeServer.cmd(...)
,Server.new_session(...)
,Server.attach_session(...)
, andServer.kill_server()
execute tmux commands synchronously. These must be adapted for asynchronous operation.libtmux.session.Session
methods – Methods such asSession.new_window(...)
,Session.rename_session(...)
, andSession.kill_session()
invoke synchronous tmux commands internally and should provide async equivalents or become fully non-blocking.By identifying these functions, we can target them for asyncio integration so managing tmux sessions and windows no longer halts the main thread.
Approach 1: Using
asyncio.subprocess
for Non-Blocking Tmux CallsPython’s asyncio provides native support for spawning subprocesses without blocking the event loop. This would involve rewriting libtmux’s command execution to an
async def
style. For example:In a
Server
class, we could add an async method:Using this async API, multiple tmux commands can run concurrently without blocking:
Pros:
Cons:
Approach 2: Async Wrappers using Threads or Process Pools
Maintain existing synchronous implementations, adding asynchronous wrappers via threads (
asyncio.to_thread
) or executors:Example (
Pane.send_keys
adapted to async):Now,
await pane.asend_keys("ls -la")
executes asynchronously, without blocking the event loop.Pros:
Cons:
Approach 3: Persistent Tmux Session with Async I/O (Control Mode)
Use tmux’s control mode (
tmux -C
) to maintain a persistent process for asynchronous command execution:Pros:
Cons:
Trade-Offs Between Approaches
Recommendation
A combined approach might be ideal:
Each option involves trade-offs between performance, implementation complexity, and compatibility. Selection depends on libtmux’s roadmap priorities.
Sources
Python asyncio subprocess documentation:
https://docs.python.org/3/library/asyncio-subprocess.html
Running blocking functions asynchronously with executors:
https://docs.python.org/3/library/asyncio-eventloop.html#running-in-executor
Tmux control mode documentation:
https://github.com/tmux/tmux/wiki/Control-Mode
Current synchronous implementation in libtmux:
https://github.com/tmux-python/libtmux
See also
Summary by Sourcery
Introduce asynchronous support for libtmux using asyncio.
New Features:
AsyncTmuxCmd
to run tmux commands asynchronously.acmd
method toServer
,Session
,Window
, andPane
objects for asynchronous command execution.Tests: