Skip to content

Commit c579a5c

Browse files
authored
types: Add StrPath typing, fix new_session, part 2 (#598)
I'm making mistakes, but follow up to #597, #596 ## Summary This PR adds uniform `StrPath` type support for `start_directory` parameters across all methods in libtmux, enabling PathLike objects (like `pathlib.Path`) to be used alongside strings. ## Changes Made ### Type Annotations Updated - `Server.new_session`: `start_directory: str | None` → `start_directory: StrPath | None` - `Session.new_window`: `start_directory: str | None` → `start_directory: StrPath | None` - `Pane.split` and `Pane.split_window`: `start_directory: str | None` → `start_directory: StrPath | None` - `Window.split` and `Window.split_window`: `start_directory: str | None` → `start_directory: StrPath | None` ### Implementation Details - Added `StrPath` import to all affected modules - Updated docstrings to reflect "str or PathLike" support - Standardized logic patterns using `if start_directory:` (not `if start_directory is not None:`) to properly handle empty strings - Added path expansion logic with `pathlib.Path(start_directory).expanduser()` for proper tilde expansion ### Testing - Added comprehensive parametrized tests for all affected methods - Test cases cover: `None`, empty string, absolute path string, and `pathlib.Path` objects - Added separate pathlib-specific tests using temporary directories - Tests verify operations complete successfully with all input types - Integrated tests into existing test files following project conventions
2 parents e58766a + 8483a5b commit c579a5c

File tree

10 files changed

+471
-23
lines changed

10 files changed

+471
-23
lines changed

CHANGES

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ $ pip install --user --upgrade --pre libtmux
1515

1616
- _Future release notes will be placed here_
1717

18+
## libtmux 0.46.2 (Unreleased)
19+
20+
### Development
21+
22+
- Add `StrPath` type support for `start_directory` parameters (#596, #597, #598):
23+
- `Server.new_session`: Accept PathLike objects for session start directory
24+
- `Session.new_window`: Accept PathLike objects for window start directory
25+
- `Pane.split` and `Pane.split_window`: Accept PathLike objects for pane start directory
26+
- `Window.split` and `Window.split_window`: Accept PathLike objects for pane start directory
27+
- Enables `pathlib.Path` objects alongside strings for all start directory parameters
28+
- Includes comprehensive tests for all parameter types (None, empty string, string paths, PathLike objects)
29+
30+
Thank you @Data5tream for the initial commit in #596!
31+
1832
## libtmux 0.46.1 (2025-03-16)
1933

2034
_Maintenance only, no bug fixes or new features_

src/libtmux/_internal/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
Notes
44
-----
5-
:class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_.
5+
:class:`StrPath` is based on `typeshed's`_.
66
77
.. _typeshed's: https://github.com/python/typeshed/blob/5ff32f3/stdlib/_typeshed/__init__.pyi#L176-L179
88
""" # E501

src/libtmux/pane.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import typing as t
1414
import warnings
1515

16+
from libtmux import exc
17+
from libtmux._internal.types import StrPath
1618
from libtmux.common import has_gte_version, has_lt_version, tmux_cmd
1719
from libtmux.constants import (
1820
PANE_DIRECTION_FLAG_MAP,
@@ -23,8 +25,6 @@
2325
from libtmux.formats import FORMAT_SEPARATOR
2426
from libtmux.neo import Obj, fetch_obj
2527

26-
from . import exc
27-
2828
if t.TYPE_CHECKING:
2929
import sys
3030
import types
@@ -548,7 +548,7 @@ def split(
548548
self,
549549
/,
550550
target: int | str | None = None,
551-
start_directory: str | None = None,
551+
start_directory: StrPath | None = None,
552552
attach: bool = False,
553553
direction: PaneDirection | None = None,
554554
full_window_split: bool | None = None,
@@ -566,7 +566,7 @@ def split(
566566
attach : bool, optional
567567
make new window the current window after creating it, default
568568
True.
569-
start_directory : str, optional
569+
start_directory : str or PathLike, optional
570570
specifies the working directory in which the new window is created.
571571
direction : PaneDirection, optional
572572
split in direction. If none is specified, assume down.
@@ -668,7 +668,7 @@ def split(
668668

669669
tmux_args += ("-P", "-F{}".format("".join(tmux_formats))) # output
670670

671-
if start_directory is not None:
671+
if start_directory:
672672
# as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c.
673673
start_path = pathlib.Path(start_directory).expanduser()
674674
tmux_args += (f"-c{start_path}",)
@@ -870,7 +870,7 @@ def split_window(
870870
self,
871871
target: int | str | None = None,
872872
attach: bool = False,
873-
start_directory: str | None = None,
873+
start_directory: StrPath | None = None,
874874
vertical: bool = True,
875875
shell: str | None = None,
876876
size: str | int | None = None,
@@ -883,7 +883,7 @@ def split_window(
883883
----------
884884
attach : bool, optional
885885
Attach / select pane after creation.
886-
start_directory : str, optional
886+
start_directory : str or PathLike, optional
887887
specifies the working directory in which the new pane is created.
888888
vertical : bool, optional
889889
split vertically

src/libtmux/server.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
import typing as t
1616
import warnings
1717

18+
from libtmux import exc, formats
1819
from libtmux._internal.query_list import QueryList
20+
from libtmux._internal.types import StrPath
1921
from libtmux.common import tmux_cmd
2022
from libtmux.neo import fetch_objs
2123
from libtmux.pane import Pane
2224
from libtmux.session import Session
2325
from libtmux.window import Window
2426

25-
from . import exc, formats
2627
from .common import (
2728
EnvironmentMixin,
2829
PaneDict,
@@ -431,7 +432,7 @@ def new_session(
431432
session_name: str | None = None,
432433
kill_session: bool = False,
433434
attach: bool = False,
434-
start_directory: str | None = None,
435+
start_directory: StrPath | None = None,
435436
window_name: str | None = None,
436437
window_command: str | None = None,
437438
x: int | DashLiteral | None = None,
@@ -466,7 +467,7 @@ def new_session(
466467
kill_session : bool, optional
467468
Kill current session if ``$ tmux has-session``.
468469
Useful for testing workspaces.
469-
start_directory : str, optional
470+
start_directory : str or PathLike, optional
470471
specifies the working directory in which the
471472
new session is created.
472473
window_name : str, optional
@@ -542,7 +543,9 @@ def new_session(
542543
tmux_args += ("-d",)
543544

544545
if start_directory:
545-
tmux_args += ("-c", start_directory)
546+
# as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-session -c.
547+
start_directory = pathlib.Path(start_directory).expanduser()
548+
tmux_args += ("-c", str(start_directory))
546549

547550
if window_name:
548551
tmux_args += ("-n", window_name)

src/libtmux/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ def new_window(
679679
window_args += ("-P",)
680680

681681
# Catch empty string and default (`None`)
682-
if start_directory and isinstance(start_directory, str):
682+
if start_directory:
683683
# as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c.
684684
start_directory = pathlib.Path(start_directory).expanduser()
685685
window_args += (f"-c{start_directory}",)

src/libtmux/window.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import warnings
1515

1616
from libtmux._internal.query_list import QueryList
17+
from libtmux._internal.types import StrPath
1718
from libtmux.common import has_gte_version, tmux_cmd
1819
from libtmux.constants import (
1920
RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP,
@@ -258,7 +259,7 @@ def split(
258259
self,
259260
/,
260261
target: int | str | None = None,
261-
start_directory: str | None = None,
262+
start_directory: StrPath | None = None,
262263
attach: bool = False,
263264
direction: PaneDirection | None = None,
264265
full_window_split: bool | None = None,
@@ -274,7 +275,7 @@ def split(
274275
attach : bool, optional
275276
make new window the current window after creating it, default
276277
True.
277-
start_directory : str, optional
278+
start_directory : str or PathLike, optional
278279
specifies the working directory in which the new window is created.
279280
direction : PaneDirection, optional
280281
split in direction. If none is specified, assume down.
@@ -864,7 +865,7 @@ def width(self) -> str | None:
864865
def split_window(
865866
self,
866867
target: int | str | None = None,
867-
start_directory: str | None = None,
868+
start_directory: StrPath | None = None,
868869
attach: bool = False,
869870
vertical: bool = True,
870871
shell: str | None = None,

tests/test_pane.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from __future__ import annotations
44

55
import logging
6+
import pathlib
67
import shutil
78
import typing as t
89

910
import pytest
1011

12+
from libtmux._internal.types import StrPath
1113
from libtmux.common import has_gte_version, has_lt_version, has_lte_version
1214
from libtmux.constants import PaneDirection, ResizeAdjustmentDirection
1315
from libtmux.test.retry import retry_until
@@ -327,11 +329,117 @@ def test_split_pane_size(session: Session) -> None:
327329
def test_pane_context_manager(session: Session) -> None:
328330
"""Test Pane context manager functionality."""
329331
window = session.new_window()
332+
initial_pane_count = len(window.panes)
333+
330334
with window.split() as pane:
331-
pane.send_keys('echo "Hello"')
335+
assert len(window.panes) == initial_pane_count + 1
332336
assert pane in window.panes
333-
assert len(window.panes) == 2 # Initial pane + new pane
334337

335338
# Pane should be killed after exiting context
336-
assert pane not in window.panes
337-
assert len(window.panes) == 1 # Only initial pane remains
339+
window.refresh()
340+
assert len(window.panes) == initial_pane_count
341+
342+
343+
class StartDirectoryTestFixture(t.NamedTuple):
344+
"""Test fixture for start_directory parameter testing."""
345+
346+
test_id: str
347+
start_directory: StrPath | None
348+
description: str
349+
350+
351+
START_DIRECTORY_TEST_FIXTURES: list[StartDirectoryTestFixture] = [
352+
StartDirectoryTestFixture(
353+
test_id="none_value",
354+
start_directory=None,
355+
description="None should not add -c flag",
356+
),
357+
StartDirectoryTestFixture(
358+
test_id="empty_string",
359+
start_directory="",
360+
description="Empty string should not add -c flag",
361+
),
362+
StartDirectoryTestFixture(
363+
test_id="user_path",
364+
start_directory="{user_path}",
365+
description="User path should add -c flag",
366+
),
367+
StartDirectoryTestFixture(
368+
test_id="relative_path",
369+
start_directory="./relative/path",
370+
description="Relative path should add -c flag",
371+
),
372+
]
373+
374+
375+
@pytest.mark.parametrize(
376+
list(StartDirectoryTestFixture._fields),
377+
START_DIRECTORY_TEST_FIXTURES,
378+
ids=[test.test_id for test in START_DIRECTORY_TEST_FIXTURES],
379+
)
380+
def test_split_start_directory(
381+
test_id: str,
382+
start_directory: StrPath | None,
383+
description: str,
384+
session: Session,
385+
monkeypatch: pytest.MonkeyPatch,
386+
tmp_path: pathlib.Path,
387+
user_path: pathlib.Path,
388+
) -> None:
389+
"""Test Pane.split start_directory parameter handling."""
390+
monkeypatch.chdir(tmp_path)
391+
392+
window = session.new_window(window_name=f"test_split_{test_id}")
393+
pane = window.active_pane
394+
assert pane is not None
395+
396+
# Format path placeholders with actual fixture values
397+
actual_start_directory = start_directory
398+
expected_path = None
399+
400+
if start_directory and str(start_directory) not in ["", "None"]:
401+
if "{user_path}" in str(start_directory):
402+
# Replace placeholder with actual user_path
403+
actual_start_directory = str(start_directory).format(user_path=user_path)
404+
expected_path = str(user_path)
405+
elif str(start_directory).startswith("./"):
406+
# For relative paths, use tmp_path as base
407+
temp_dir = tmp_path / "relative" / "path"
408+
temp_dir.mkdir(parents=True, exist_ok=True)
409+
actual_start_directory = str(temp_dir)
410+
expected_path = str(temp_dir.resolve())
411+
412+
# Should not raise an error
413+
new_pane = pane.split(start_directory=actual_start_directory)
414+
415+
assert new_pane in window.panes
416+
assert len(window.panes) == 2
417+
418+
# Verify working directory if we have an expected path
419+
if expected_path:
420+
new_pane.refresh()
421+
assert new_pane.pane_current_path is not None
422+
actual_path = str(pathlib.Path(new_pane.pane_current_path).resolve())
423+
assert actual_path == expected_path
424+
425+
426+
def test_split_start_directory_pathlib(
427+
session: Session, user_path: pathlib.Path
428+
) -> None:
429+
"""Test Pane.split accepts pathlib.Path for start_directory."""
430+
window = session.new_window(window_name="test_split_pathlib")
431+
pane = window.active_pane
432+
assert pane is not None
433+
434+
# Pass pathlib.Path directly to test pathlib.Path acceptance
435+
new_pane = pane.split(start_directory=user_path)
436+
437+
assert new_pane in window.panes
438+
assert len(window.panes) == 2
439+
440+
# Verify working directory
441+
new_pane.refresh()
442+
assert new_pane.pane_current_path is not None
443+
actual_path = str(pathlib.Path(new_pane.pane_current_path).resolve())
444+
expected_path = str(user_path.resolve())
445+
assert actual_path == expected_path

0 commit comments

Comments
 (0)