Skip to content

types: Add StrPath typing, fix new_session, part 2 #598

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 12 commits into from
May 26, 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
14 changes: 14 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ $ pip install --user --upgrade --pre libtmux

- _Future release notes will be placed here_

## libtmux 0.46.2 (Unreleased)

### Development

- Add `StrPath` type support for `start_directory` parameters (#596, #597, #598):
- `Server.new_session`: Accept PathLike objects for session start directory
- `Session.new_window`: Accept PathLike objects for window start directory
- `Pane.split` and `Pane.split_window`: Accept PathLike objects for pane start directory
- `Window.split` and `Window.split_window`: Accept PathLike objects for pane start directory
- Enables `pathlib.Path` objects alongside strings for all start directory parameters
- Includes comprehensive tests for all parameter types (None, empty string, string paths, PathLike objects)

Thank you @Data5tream for the initial commit in #596!

## libtmux 0.46.1 (2025-03-16)

_Maintenance only, no bug fixes or new features_
Expand Down
2 changes: 1 addition & 1 deletion src/libtmux/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Notes
-----
:class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_.
:class:`StrPath` is based on `typeshed's`_.

.. _typeshed's: https://github.com/python/typeshed/blob/5ff32f3/stdlib/_typeshed/__init__.pyi#L176-L179
""" # E501
Expand Down
14 changes: 7 additions & 7 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 libtmux import exc
from libtmux._internal.types import StrPath
from libtmux.common import has_gte_version, has_lt_version, tmux_cmd
from libtmux.constants import (
PANE_DIRECTION_FLAG_MAP,
Expand All @@ -23,8 +25,6 @@
from libtmux.formats import FORMAT_SEPARATOR
from libtmux.neo import Obj, fetch_obj

from . import exc

if t.TYPE_CHECKING:
import sys
import types
Expand Down Expand Up @@ -548,7 +548,7 @@ def split(
self,
/,
target: int | str | None = None,
start_directory: str | None = None,
start_directory: StrPath | None = None,
attach: bool = False,
direction: PaneDirection | None = None,
full_window_split: bool | None = None,
Expand All @@ -566,7 +566,7 @@ def split(
attach : bool, optional
make new window the current window after creating it, default
True.
start_directory : str, optional
start_directory : str or PathLike, optional
specifies the working directory in which the new window is created.
direction : PaneDirection, optional
split in direction. If none is specified, assume down.
Expand Down Expand Up @@ -668,7 +668,7 @@ def split(

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

if start_directory is not None:
if start_directory:
# as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c.
start_path = pathlib.Path(start_directory).expanduser()
tmux_args += (f"-c{start_path}",)
Expand Down Expand Up @@ -870,7 +870,7 @@ def split_window(
self,
target: int | str | None = None,
attach: bool = False,
start_directory: str | None = None,
start_directory: StrPath | None = None,
vertical: bool = True,
shell: str | None = None,
size: str | int | None = None,
Expand All @@ -883,7 +883,7 @@ def split_window(
----------
attach : bool, optional
Attach / select pane after creation.
start_directory : str, optional
start_directory : str or PathLike, optional
specifies the working directory in which the new pane is created.
vertical : bool, optional
split vertically
Expand Down
11 changes: 7 additions & 4 deletions src/libtmux/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import typing as t
import warnings

from libtmux import exc, formats
from libtmux._internal.query_list import QueryList
from libtmux._internal.types import StrPath
from libtmux.common import tmux_cmd
from libtmux.neo import fetch_objs
from libtmux.pane import Pane
from libtmux.session import Session
from libtmux.window import Window

from . import exc, formats
from .common import (
EnvironmentMixin,
PaneDict,
Expand Down Expand Up @@ -431,7 +432,7 @@ def new_session(
session_name: str | None = None,
kill_session: bool = False,
attach: bool = False,
start_directory: str | None = None,
start_directory: StrPath | None = None,
window_name: str | None = None,
window_command: str | None = None,
x: int | DashLiteral | None = None,
Expand Down Expand Up @@ -466,7 +467,7 @@ def new_session(
kill_session : bool, optional
Kill current session if ``$ tmux has-session``.
Useful for testing workspaces.
start_directory : str, optional
start_directory : str or PathLike, optional
specifies the working directory in which the
new session is created.
window_name : str, optional
Expand Down Expand Up @@ -542,7 +543,9 @@ def new_session(
tmux_args += ("-d",)

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

if window_name:
tmux_args += ("-n", window_name)
Expand Down
2 changes: 1 addition & 1 deletion src/libtmux/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ def new_window(
window_args += ("-P",)

# Catch empty string and default (`None`)
if start_directory and isinstance(start_directory, str):
if start_directory:
# as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c.
start_directory = pathlib.Path(start_directory).expanduser()
window_args += (f"-c{start_directory}",)
Expand Down
7 changes: 4 additions & 3 deletions src/libtmux/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import warnings

from libtmux._internal.query_list import QueryList
from libtmux._internal.types import StrPath
from libtmux.common import has_gte_version, tmux_cmd
from libtmux.constants import (
RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP,
Expand Down Expand Up @@ -258,7 +259,7 @@ def split(
self,
/,
target: int | str | None = None,
start_directory: str | None = None,
start_directory: StrPath | None = None,
attach: bool = False,
direction: PaneDirection | None = None,
full_window_split: bool | None = None,
Expand All @@ -274,7 +275,7 @@ def split(
attach : bool, optional
make new window the current window after creating it, default
True.
start_directory : str, optional
start_directory : str or PathLike, optional
specifies the working directory in which the new window is created.
direction : PaneDirection, optional
split in direction. If none is specified, assume down.
Expand Down Expand Up @@ -864,7 +865,7 @@ def width(self) -> str | None:
def split_window(
self,
target: int | str | None = None,
start_directory: str | None = None,
start_directory: StrPath | None = None,
attach: bool = False,
vertical: bool = True,
shell: str | None = None,
Expand Down
116 changes: 112 additions & 4 deletions tests/test_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from __future__ import annotations

import logging
import pathlib
import shutil
import typing as t

import pytest

from libtmux._internal.types import StrPath
from libtmux.common import has_gte_version, has_lt_version, has_lte_version
from libtmux.constants import PaneDirection, ResizeAdjustmentDirection
from libtmux.test.retry import retry_until
Expand Down Expand Up @@ -327,11 +329,117 @@ def test_split_pane_size(session: Session) -> None:
def test_pane_context_manager(session: Session) -> None:
"""Test Pane context manager functionality."""
window = session.new_window()
initial_pane_count = len(window.panes)

with window.split() as pane:
pane.send_keys('echo "Hello"')
assert len(window.panes) == initial_pane_count + 1
assert pane in window.panes
assert len(window.panes) == 2 # Initial pane + new pane

# Pane should be killed after exiting context
assert pane not in window.panes
assert len(window.panes) == 1 # Only initial pane remains
window.refresh()
assert len(window.panes) == initial_pane_count


class StartDirectoryTestFixture(t.NamedTuple):
"""Test fixture for start_directory parameter testing."""

test_id: str
start_directory: StrPath | None
description: str


START_DIRECTORY_TEST_FIXTURES: list[StartDirectoryTestFixture] = [
StartDirectoryTestFixture(
test_id="none_value",
start_directory=None,
description="None should not add -c flag",
),
StartDirectoryTestFixture(
test_id="empty_string",
start_directory="",
description="Empty string should not add -c flag",
),
StartDirectoryTestFixture(
test_id="user_path",
start_directory="{user_path}",
description="User path should add -c flag",
),
StartDirectoryTestFixture(
test_id="relative_path",
start_directory="./relative/path",
description="Relative path should add -c flag",
),
]


@pytest.mark.parametrize(
list(StartDirectoryTestFixture._fields),
START_DIRECTORY_TEST_FIXTURES,
ids=[test.test_id for test in START_DIRECTORY_TEST_FIXTURES],
)
def test_split_start_directory(
test_id: str,
start_directory: StrPath | None,
description: str,
session: Session,
monkeypatch: pytest.MonkeyPatch,
tmp_path: pathlib.Path,
user_path: pathlib.Path,
) -> None:
"""Test Pane.split start_directory parameter handling."""
monkeypatch.chdir(tmp_path)

window = session.new_window(window_name=f"test_split_{test_id}")
pane = window.active_pane
assert pane is not None

# Format path placeholders with actual fixture values
actual_start_directory = start_directory
expected_path = None

if start_directory and str(start_directory) not in ["", "None"]:
if "{user_path}" in str(start_directory):
# Replace placeholder with actual user_path
actual_start_directory = str(start_directory).format(user_path=user_path)
expected_path = str(user_path)
elif str(start_directory).startswith("./"):
# For relative paths, use tmp_path as base
temp_dir = tmp_path / "relative" / "path"
temp_dir.mkdir(parents=True, exist_ok=True)
actual_start_directory = str(temp_dir)
expected_path = str(temp_dir.resolve())

# Should not raise an error
new_pane = pane.split(start_directory=actual_start_directory)

assert new_pane in window.panes
assert len(window.panes) == 2

# Verify working directory if we have an expected path
if expected_path:
new_pane.refresh()
assert new_pane.pane_current_path is not None
actual_path = str(pathlib.Path(new_pane.pane_current_path).resolve())
assert actual_path == expected_path


def test_split_start_directory_pathlib(
session: Session, user_path: pathlib.Path
) -> None:
"""Test Pane.split accepts pathlib.Path for start_directory."""
window = session.new_window(window_name="test_split_pathlib")
pane = window.active_pane
assert pane is not None

# Pass pathlib.Path directly to test pathlib.Path acceptance
new_pane = pane.split(start_directory=user_path)

assert new_pane in window.panes
assert len(window.panes) == 2

# Verify working directory
new_pane.refresh()
assert new_pane.pane_current_path is not None
actual_path = str(pathlib.Path(new_pane.pane_current_path).resolve())
expected_path = str(user_path.resolve())
assert actual_path == expected_path
Loading