Skip to content

Commit f7e7d9f

Browse files
committed
Pane(fix[swap]): make target optional when move_up/move_down is set
why: pane.swap(move_up=True) raised TypeError because target was positionally required, contradicting the docstring claim that move_up/move_down "overrides target". The body also emitted -s <target> unconditionally even with -U/-D. what: - Make target optional with default None - Raise LibTmuxException for missing/conflicting flag combinations (no flags, both move flags, target combined with a move flag) - Skip -s <target> when target is None - Update docstring: "Mutually exclusive with target" + versionchanged 0.56 - Add tests covering move_up/move_down happy path and three invalid argument combinations
1 parent 78d4247 commit f7e7d9f

2 files changed

Lines changed: 75 additions & 6 deletions

File tree

src/libtmux/pane.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,7 +2030,7 @@ def break_pane(
20302030

20312031
def swap(
20322032
self,
2033-
target: str | Pane,
2033+
target: str | Pane | None = None,
20342034
*,
20352035
detach: bool | None = None,
20362036
move_up: bool | None = None,
@@ -2041,14 +2041,21 @@ def swap(
20412041
20422042
Parameters
20432043
----------
2044-
target : str or Pane
2044+
target : str or Pane, optional
20452045
Target pane to swap with. Can be a pane ID string or Pane object.
2046+
Mutually exclusive with *move_up* / *move_down*.
2047+
2048+
.. versionchanged:: 0.56
2049+
Now optional; required only when *move_up* / *move_down* are
2050+
not set.
20462051
detach : bool, optional
20472052
Do not change the active pane (``-d`` flag).
20482053
move_up : bool, optional
2049-
Swap with the pane above (``-U`` flag). Overrides *target*.
2054+
Swap with the pane above (``-U`` flag). Mutually exclusive with
2055+
*target* and *move_down*.
20502056
move_down : bool, optional
2051-
Swap with the pane below (``-D`` flag). Overrides *target*.
2057+
Swap with the pane below (``-D`` flag). Mutually exclusive with
2058+
*target* and *move_up*.
20522059
keep_zoom : bool, optional
20532060
Keep the window zoomed if it was zoomed (``-Z`` flag).
20542061
@@ -2061,6 +2068,18 @@ def swap(
20612068
>>> pane1.refresh()
20622069
>>> pane2.refresh()
20632070
"""
2071+
if move_up and move_down:
2072+
msg = "move_up and move_down are mutually exclusive"
2073+
raise exc.LibTmuxException(msg)
2074+
2075+
if target is not None and (move_up or move_down):
2076+
msg = "target is mutually exclusive with move_up/move_down"
2077+
raise exc.LibTmuxException(msg)
2078+
2079+
if target is None and not move_up and not move_down:
2080+
msg = "swap requires target or move_up=True or move_down=True"
2081+
raise exc.LibTmuxException(msg)
2082+
20642083
tmux_args: tuple[str, ...] = ()
20652084

20662085
if detach:
@@ -2075,8 +2094,9 @@ def swap(
20752094
if keep_zoom:
20762095
tmux_args += ("-Z",)
20772096

2078-
target_id = target.pane_id if isinstance(target, Pane) else target
2079-
tmux_args += ("-s", str(target_id))
2097+
if target is not None:
2098+
target_id = target.pane_id if isinstance(target, Pane) else target
2099+
tmux_args += ("-s", str(target_id))
20802100

20812101
proc = self.cmd("swap-pane", *tmux_args)
20822102

tests/test_pane.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pytest
1111

12+
from libtmux import exc
1213
from libtmux.common import has_gte_version
1314
from libtmux.constants import PaneDirection, ResizeAdjustmentDirection
1415
from libtmux.test.retry import retry_until
@@ -1180,6 +1181,54 @@ def test_swap_pane(session: Session) -> None:
11801181
assert pane2.pane_id == pane2_id
11811182

11821183

1184+
def test_swap_pane_move_up_down(session: Session) -> None:
1185+
"""Pane.swap(move_up=True) / (move_down=True) work without a target."""
1186+
window = session.new_window(window_name="test_swap_move")
1187+
window.resize(height=40, width=80)
1188+
pane1 = window.active_pane
1189+
assert pane1 is not None
1190+
pane2 = pane1.split()
1191+
1192+
pane1.refresh()
1193+
pane2.refresh()
1194+
pane1_idx = pane1.pane_index
1195+
pane2_idx = pane2.pane_index
1196+
1197+
# move_down on pane1: swap with the next pane (pane2)
1198+
pane1.swap(move_down=True)
1199+
pane1.refresh()
1200+
pane2.refresh()
1201+
assert pane1.pane_index == pane2_idx
1202+
assert pane2.pane_index == pane1_idx
1203+
1204+
# move_up on pane1: swap back to the original layout
1205+
pane1.swap(move_up=True)
1206+
pane1.refresh()
1207+
pane2.refresh()
1208+
assert pane1.pane_index == pane1_idx
1209+
assert pane2.pane_index == pane2_idx
1210+
1211+
1212+
@pytest.mark.parametrize(
1213+
("kwargs", "match"),
1214+
[
1215+
({}, "target or move_up"),
1216+
({"move_up": True, "move_down": True}, "mutually exclusive"),
1217+
({"move_up": True, "target": "%0"}, "mutually exclusive"),
1218+
],
1219+
)
1220+
def test_swap_pane_invalid_args(
1221+
session: Session,
1222+
kwargs: dict[str, t.Any],
1223+
match: str,
1224+
) -> None:
1225+
"""Pane.swap() rejects missing or conflicting arguments."""
1226+
pane = session.active_window.active_pane
1227+
assert pane is not None
1228+
with pytest.raises(exc.LibTmuxException, match=match):
1229+
pane.swap(**kwargs)
1230+
1231+
11831232
def test_clear_history(session: Session) -> None:
11841233
"""Test Pane.clear_history()."""
11851234
env = shutil.which("env")

0 commit comments

Comments
 (0)