Skip to content

Commit 5bf186e

Browse files
authored
feat: Add Context managers (#566)
Added context manager support for all main tmux objects: - `Server`: Automatically kills the server when exiting the context - `Session`: Automatically kills the session when exiting the context - `Window`: Automatically kills the window when exiting the context - `Pane`: Automatically kills the pane when exiting the context Example usage: ```python with Server() as server: with server.new_session() as session: with session.new_window() as window: with window.split() as pane: pane.send_keys('echo "Hello"') # Do work with the pane # Everything is cleaned up automatically when exiting contexts ``` This makes it easier to write clean, safe code that properly cleans up tmux resources.
2 parents 9018da6 + 92660e4 commit 5bf186e

File tree

11 files changed

+373
-0
lines changed

11 files changed

+373
-0
lines changed

CHANGES

+25
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ $ pip install --user --upgrade --pre libtmux
1919

2020
### Features
2121

22+
#### Context Managers for tmux Objects
23+
24+
Added context manager support for all main tmux objects:
25+
26+
- `Server`: Automatically kills the server when exiting the context
27+
- `Session`: Automatically kills the session when exiting the context
28+
- `Window`: Automatically kills the window when exiting the context
29+
- `Pane`: Automatically kills the pane when exiting the context
30+
31+
Example usage:
32+
33+
```python
34+
with Server() as server:
35+
with server.new_session() as session:
36+
with session.new_window() as window:
37+
with window.split() as pane:
38+
pane.send_keys('echo "Hello"')
39+
# Do work with the pane
40+
# Everything is cleaned up automatically when exiting contexts
41+
```
42+
43+
This makes it easier to write clean, safe code that properly cleans up tmux resources.
44+
45+
#### Server Initialization Callbacks
46+
2247
Server now accepts 2 new optional params, `socket_name_factory` and `on_init` callbacks (#565):
2348

2449
- `socket_name_factory`: Callable that generates unique socket names for new servers

docs/topics/context_managers.md

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
(context_managers)=
2+
3+
# Context Managers
4+
5+
libtmux provides context managers for all main tmux objects to ensure proper cleanup of resources. This is done through Python's `with` statement, which automatically handles cleanup when you're done with the tmux objects.
6+
7+
Open two terminals:
8+
9+
Terminal one: start tmux in a separate terminal:
10+
11+
```console
12+
$ tmux
13+
```
14+
15+
Terminal two, `python` or `ptpython` if you have it:
16+
17+
```console
18+
$ python
19+
```
20+
21+
Import `libtmux`:
22+
23+
```python
24+
import libtmux
25+
```
26+
27+
## Server Context Manager
28+
29+
Create a temporary server that will be killed when you're done:
30+
31+
```python
32+
>>> with Server() as server:
33+
... session = server.new_session()
34+
... print(server.is_alive())
35+
True
36+
>>> print(server.is_alive()) # Server is killed after exiting context
37+
False
38+
```
39+
40+
## Session Context Manager
41+
42+
Create a temporary session that will be killed when you're done:
43+
44+
```python
45+
>>> server = Server()
46+
>>> with server.new_session() as session:
47+
... print(session in server.sessions)
48+
... window = session.new_window()
49+
True
50+
>>> print(session in server.sessions) # Session is killed after exiting context
51+
False
52+
```
53+
54+
## Window Context Manager
55+
56+
Create a temporary window that will be killed when you're done:
57+
58+
```python
59+
>>> server = Server()
60+
>>> session = server.new_session()
61+
>>> with session.new_window() as window:
62+
... print(window in session.windows)
63+
... pane = window.split()
64+
True
65+
>>> print(window in session.windows) # Window is killed after exiting context
66+
False
67+
```
68+
69+
## Pane Context Manager
70+
71+
Create a temporary pane that will be killed when you're done:
72+
73+
```python
74+
>>> server = Server()
75+
>>> session = server.new_session()
76+
>>> window = session.new_window()
77+
>>> with window.split() as pane:
78+
... print(pane in window.panes)
79+
... pane.send_keys('echo "Hello"')
80+
True
81+
>>> print(pane in window.panes) # Pane is killed after exiting context
82+
False
83+
```
84+
85+
## Nested Context Managers
86+
87+
Context managers can be nested to create a clean hierarchy of tmux objects that are automatically cleaned up:
88+
89+
```python
90+
>>> with Server() as server:
91+
... with server.new_session() as session:
92+
... with session.new_window() as window:
93+
... with window.split() as pane:
94+
... pane.send_keys('echo "Hello"')
95+
... # Do work with the pane
96+
... # Everything is cleaned up automatically when exiting contexts
97+
```
98+
99+
This ensures that:
100+
101+
1. The pane is killed when exiting its context
102+
2. The window is killed when exiting its context
103+
3. The session is killed when exiting its context
104+
4. The server is killed when exiting its context
105+
106+
The cleanup happens in reverse order (pane → window → session → server), ensuring proper resource management.
107+
108+
## Benefits
109+
110+
Using context managers provides several advantages:
111+
112+
1. **Automatic Cleanup**: Resources are automatically cleaned up when you're done with them
113+
2. **Clean Code**: No need to manually call `kill()` methods
114+
3. **Exception Safety**: Resources are cleaned up even if an exception occurs
115+
4. **Hierarchical Cleanup**: Nested contexts ensure proper cleanup order
116+
5. **Resource Management**: Prevents resource leaks by ensuring tmux objects are properly destroyed
117+
118+
## When to Use
119+
120+
Context managers are particularly useful when:
121+
122+
1. Creating temporary tmux objects for testing
123+
2. Running short-lived tmux sessions
124+
3. Managing multiple tmux servers
125+
4. Ensuring cleanup in scripts that may raise exceptions
126+
5. Creating isolated environments that need to be cleaned up afterward
127+
128+
[target]: http://man.openbsd.org/OpenBSD-5.9/man1/tmux.1#COMMANDS

docs/topics/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ Explore libtmux’s core functionalities and underlying principles at a high lev
88

99
```{toctree}
1010
11+
context_managers
1112
traversal
1213
```

src/libtmux/pane.py

+44
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import typing as t
1414
import warnings
1515

16+
from typing_extensions import Self
17+
1618
from libtmux.common import has_gte_version, has_lt_version, tmux_cmd
1719
from libtmux.constants import (
1820
PANE_DIRECTION_FLAG_MAP,
@@ -26,6 +28,8 @@
2628
from . import exc
2729

2830
if t.TYPE_CHECKING:
31+
import types
32+
2933
from .server import Server
3034
from .session import Session
3135
from .window import Window
@@ -59,6 +63,13 @@ class Pane(Obj):
5963
>>> pane.session
6064
Session($1 ...)
6165
66+
The pane can be used as a context manager to ensure proper cleanup:
67+
68+
>>> with window.split() as pane:
69+
... pane.send_keys('echo "Hello"')
70+
... # Do work with the pane
71+
... # Pane will be killed automatically when exiting the context
72+
6273
Notes
6374
-----
6475
.. versionchanged:: 0.8
@@ -77,6 +88,39 @@ class Pane(Obj):
7788

7889
server: Server
7990

91+
def __enter__(self) -> Self:
92+
"""Enter the context, returning self.
93+
94+
Returns
95+
-------
96+
:class:`Pane`
97+
The pane instance
98+
"""
99+
return self
100+
101+
def __exit__(
102+
self,
103+
exc_type: type[BaseException] | None,
104+
exc_value: BaseException | None,
105+
exc_tb: types.TracebackType | None,
106+
) -> None:
107+
"""Exit the context, killing the pane if it exists.
108+
109+
Parameters
110+
----------
111+
exc_type : type[BaseException] | None
112+
The type of the exception that was raised
113+
exc_value : BaseException | None
114+
The instance of the exception that was raised
115+
exc_tb : types.TracebackType | None
116+
The traceback of the exception that was raised
117+
"""
118+
if (
119+
self.pane_id is not None
120+
and len(self.window.panes.filter(pane_id=self.pane_id)) > 0
121+
):
122+
self.kill()
123+
80124
def refresh(self) -> None:
81125
"""Refresh pane attributes from tmux."""
82126
assert isinstance(self.pane_id, str)

src/libtmux/server.py

+41
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import typing as t
1616
import warnings
1717

18+
from typing_extensions import Self
19+
1820
from libtmux._internal.query_list import QueryList
1921
from libtmux.common import tmux_cmd
2022
from libtmux.neo import fetch_objs
@@ -33,6 +35,8 @@
3335
)
3436

3537
if t.TYPE_CHECKING:
38+
import types
39+
3640
from typing_extensions import TypeAlias
3741

3842
DashLiteral: TypeAlias = t.Literal["-"]
@@ -79,6 +83,13 @@ class Server(EnvironmentMixin):
7983
>>> server.sessions[0].active_pane
8084
Pane(%1 Window(@1 1:..., Session($1 ...)))
8185
86+
The server can be used as a context manager to ensure proper cleanup:
87+
88+
>>> with Server() as server:
89+
... session = server.new_session()
90+
... # Do work with the session
91+
... # Server will be killed automatically when exiting the context
92+
8293
References
8394
----------
8495
.. [server_manual] CLIENTS AND SESSIONS. openbsd manpage for TMUX(1)
@@ -146,6 +157,36 @@ def __init__(
146157
if on_init is not None:
147158
on_init(self)
148159

160+
def __enter__(self) -> Self:
161+
"""Enter the context, returning self.
162+
163+
Returns
164+
-------
165+
:class:`Server`
166+
The server instance
167+
"""
168+
return self
169+
170+
def __exit__(
171+
self,
172+
exc_type: type[BaseException] | None,
173+
exc_value: BaseException | None,
174+
exc_tb: types.TracebackType | None,
175+
) -> None:
176+
"""Exit the context, killing the server if it exists.
177+
178+
Parameters
179+
----------
180+
exc_type : type[BaseException] | None
181+
The type of the exception that was raised
182+
exc_value : BaseException | None
183+
The instance of the exception that was raised
184+
exc_tb : types.TracebackType | None
185+
The traceback of the exception that was raised
186+
"""
187+
if self.is_alive():
188+
self.kill()
189+
149190
def is_alive(self) -> bool:
150191
"""Return True if tmux server alive.
151192

src/libtmux/session.py

+41
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import typing as t
1414
import warnings
1515

16+
from typing_extensions import Self
17+
1618
from libtmux._internal.query_list import QueryList
1719
from libtmux.constants import WINDOW_DIRECTION_FLAG_MAP, WindowDirection
1820
from libtmux.formats import FORMAT_SEPARATOR
@@ -31,6 +33,8 @@
3133
)
3234

3335
if t.TYPE_CHECKING:
36+
import types
37+
3438
from libtmux.common import tmux_cmd
3539

3640
from .server import Server
@@ -63,6 +67,13 @@ class Session(Obj, EnvironmentMixin):
6367
>>> session.active_pane
6468
Pane(%1 Window(@1 ...:..., Session($1 ...)))
6569
70+
The session can be used as a context manager to ensure proper cleanup:
71+
72+
>>> with server.new_session() as session:
73+
... window = session.new_window()
74+
... # Do work with the window
75+
... # Session will be killed automatically when exiting the context
76+
6677
References
6778
----------
6879
.. [session_manual] tmux session. openbsd manpage for TMUX(1).
@@ -78,6 +89,36 @@ class Session(Obj, EnvironmentMixin):
7889

7990
server: Server
8091

92+
def __enter__(self) -> Self:
93+
"""Enter the context, returning self.
94+
95+
Returns
96+
-------
97+
:class:`Session`
98+
The session instance
99+
"""
100+
return self
101+
102+
def __exit__(
103+
self,
104+
exc_type: type[BaseException] | None,
105+
exc_value: BaseException | None,
106+
exc_tb: types.TracebackType | None,
107+
) -> None:
108+
"""Exit the context, killing the session if it exists.
109+
110+
Parameters
111+
----------
112+
exc_type : type[BaseException] | None
113+
The type of the exception that was raised
114+
exc_value : BaseException | None
115+
The instance of the exception that was raised
116+
exc_tb : types.TracebackType | None
117+
The traceback of the exception that was raised
118+
"""
119+
if self.session_name is not None and self.server.has_session(self.session_name):
120+
self.kill()
121+
81122
def refresh(self) -> None:
82123
"""Refresh session attributes from tmux."""
83124
assert isinstance(self.session_id, str)

0 commit comments

Comments
 (0)