Skip to content

Context managers #566

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

Merged
merged 4 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ $ pip install --user --upgrade --pre libtmux

### Features

#### Context Managers for tmux Objects

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.

#### Server Initialization Callbacks

Server now accepts 2 new optional params, `socket_name_factory` and `on_init` callbacks (#565):

- `socket_name_factory`: Callable that generates unique socket names for new servers
Expand Down
128 changes: 128 additions & 0 deletions docs/topics/context_managers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
(context_managers)=

# Context Managers

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.

Open two terminals:

Terminal one: start tmux in a separate terminal:

```console
$ tmux
```

Terminal two, `python` or `ptpython` if you have it:

```console
$ python
```

Import `libtmux`:

```python
import libtmux
```

## Server Context Manager

Create a temporary server that will be killed when you're done:

```python
>>> with Server() as server:
... session = server.new_session()
... print(server.is_alive())
True
>>> print(server.is_alive()) # Server is killed after exiting context
False
```

## Session Context Manager

Create a temporary session that will be killed when you're done:

```python
>>> server = Server()
>>> with server.new_session() as session:
... print(session in server.sessions)
... window = session.new_window()
True
>>> print(session in server.sessions) # Session is killed after exiting context
False
```

## Window Context Manager

Create a temporary window that will be killed when you're done:

```python
>>> server = Server()
>>> session = server.new_session()
>>> with session.new_window() as window:
... print(window in session.windows)
... pane = window.split()
True
>>> print(window in session.windows) # Window is killed after exiting context
False
```

## Pane Context Manager

Create a temporary pane that will be killed when you're done:

```python
>>> server = Server()
>>> session = server.new_session()
>>> window = session.new_window()
>>> with window.split() as pane:
... print(pane in window.panes)
... pane.send_keys('echo "Hello"')
True
>>> print(pane in window.panes) # Pane is killed after exiting context
False
```

## Nested Context Managers

Context managers can be nested to create a clean hierarchy of tmux objects that are automatically cleaned up:

```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 ensures that:

1. The pane is killed when exiting its context
2. The window is killed when exiting its context
3. The session is killed when exiting its context
4. The server is killed when exiting its context

The cleanup happens in reverse order (pane → window → session → server), ensuring proper resource management.

## Benefits

Using context managers provides several advantages:

1. **Automatic Cleanup**: Resources are automatically cleaned up when you're done with them
2. **Clean Code**: No need to manually call `kill()` methods
3. **Exception Safety**: Resources are cleaned up even if an exception occurs
4. **Hierarchical Cleanup**: Nested contexts ensure proper cleanup order
5. **Resource Management**: Prevents resource leaks by ensuring tmux objects are properly destroyed

## When to Use

Context managers are particularly useful when:

1. Creating temporary tmux objects for testing
2. Running short-lived tmux sessions
3. Managing multiple tmux servers
4. Ensuring cleanup in scripts that may raise exceptions
5. Creating isolated environments that need to be cleaned up afterward

[target]: http://man.openbsd.org/OpenBSD-5.9/man1/tmux.1#COMMANDS
1 change: 1 addition & 0 deletions docs/topics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Explore libtmux’s core functionalities and underlying principles at a high lev

```{toctree}
context_managers
traversal
```
44 changes: 44 additions & 0 deletions src/libtmux/pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import typing as t
import warnings

from typing_extensions import Self

from libtmux.common import has_gte_version, has_lt_version, tmux_cmd
from libtmux.constants import (
PANE_DIRECTION_FLAG_MAP,
Expand All @@ -26,6 +28,8 @@
from . import exc

if t.TYPE_CHECKING:
import types

from .server import Server
from .session import Session
from .window import Window
Expand Down Expand Up @@ -59,6 +63,13 @@ class Pane(Obj):
>>> pane.session
Session($1 ...)
The pane can be used as a context manager to ensure proper cleanup:
>>> with window.split() as pane:
... pane.send_keys('echo "Hello"')
... # Do work with the pane
... # Pane will be killed automatically when exiting the context
Notes
-----
.. versionchanged:: 0.8
Expand All @@ -77,6 +88,39 @@ class Pane(Obj):

server: Server

def __enter__(self) -> Self:
"""Enter the context, returning self.
Returns
-------
:class:`Pane`
The pane instance
"""
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_tb: types.TracebackType | None,
) -> None:
"""Exit the context, killing the pane if it exists.
Parameters
----------
exc_type : type[BaseException] | None
The type of the exception that was raised
exc_value : BaseException | None
The instance of the exception that was raised
exc_tb : types.TracebackType | None
The traceback of the exception that was raised
"""
if (
self.pane_id is not None
and len(self.window.panes.filter(pane_id=self.pane_id)) > 0
):
self.kill()

def refresh(self) -> None:
"""Refresh pane attributes from tmux."""
assert isinstance(self.pane_id, str)
Expand Down
41 changes: 41 additions & 0 deletions src/libtmux/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import typing as t
import warnings

from typing_extensions import Self

from libtmux._internal.query_list import QueryList
from libtmux.common import tmux_cmd
from libtmux.neo import fetch_objs
Expand All @@ -33,6 +35,8 @@
)

if t.TYPE_CHECKING:
import types

from typing_extensions import TypeAlias

DashLiteral: TypeAlias = t.Literal["-"]
Expand Down Expand Up @@ -79,6 +83,13 @@ class Server(EnvironmentMixin):
>>> server.sessions[0].active_pane
Pane(%1 Window(@1 1:..., Session($1 ...)))
The server can be used as a context manager to ensure proper cleanup:
>>> with Server() as server:
... session = server.new_session()
... # Do work with the session
... # Server will be killed automatically when exiting the context
References
----------
.. [server_manual] CLIENTS AND SESSIONS. openbsd manpage for TMUX(1)
Expand Down Expand Up @@ -146,6 +157,36 @@ def __init__(
if on_init is not None:
on_init(self)

def __enter__(self) -> Self:
"""Enter the context, returning self.
Returns
-------
:class:`Server`
The server instance
"""
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_tb: types.TracebackType | None,
) -> None:
"""Exit the context, killing the server if it exists.
Parameters
----------
exc_type : type[BaseException] | None
The type of the exception that was raised
exc_value : BaseException | None
The instance of the exception that was raised
exc_tb : types.TracebackType | None
The traceback of the exception that was raised
"""
if self.is_alive():
self.kill()

def is_alive(self) -> bool:
"""Return True if tmux server alive.
Expand Down
41 changes: 41 additions & 0 deletions src/libtmux/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import typing as t
import warnings

from typing_extensions import Self

from libtmux._internal.query_list import QueryList
from libtmux.constants import WINDOW_DIRECTION_FLAG_MAP, WindowDirection
from libtmux.formats import FORMAT_SEPARATOR
Expand All @@ -31,6 +33,8 @@
)

if t.TYPE_CHECKING:
import types

from libtmux.common import tmux_cmd

from .server import Server
Expand Down Expand Up @@ -63,6 +67,13 @@ class Session(Obj, EnvironmentMixin):
>>> session.active_pane
Pane(%1 Window(@1 ...:..., Session($1 ...)))
The session can be used as a context manager to ensure proper cleanup:
>>> with server.new_session() as session:
... window = session.new_window()
... # Do work with the window
... # Session will be killed automatically when exiting the context
References
----------
.. [session_manual] tmux session. openbsd manpage for TMUX(1).
Expand All @@ -78,6 +89,36 @@ class Session(Obj, EnvironmentMixin):

server: Server

def __enter__(self) -> Self:
"""Enter the context, returning self.
Returns
-------
:class:`Session`
The session instance
"""
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_tb: types.TracebackType | None,
) -> None:
"""Exit the context, killing the session if it exists.
Parameters
----------
exc_type : type[BaseException] | None
The type of the exception that was raised
exc_value : BaseException | None
The instance of the exception that was raised
exc_tb : types.TracebackType | None
The traceback of the exception that was raised
"""
if self.session_name is not None and self.server.has_session(self.session_name):
self.kill()

def refresh(self) -> None:
"""Refresh session attributes from tmux."""
assert isinstance(self.session_id, str)
Expand Down
Loading