diff --git a/conftest.py b/conftest.py index ada5aae3f..9306d9a4c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,11 +1,14 @@ -"""Conftest.py (root-level). +"""Configure root-level pytest fixtures for libtmux. -We keep this in root pytest fixtures in pytest's doctest plugin to be available, as well -as avoiding conftest.py from being included in the wheel, in addition to pytest_plugin -for pytester only being available via the root directory. +We keep this file at the root to make these fixtures available to all +tests, while also preventing unwanted inclusion in the distributed +wheel. Additionally, `pytest_plugins` references ensure that the +`pytester` plugin is accessible for test generation and execution. -See "pytest_plugins in non-top-level conftest files" in -https://docs.pytest.org/en/stable/deprecations.html +See Also +-------- +pytest_plugins in non-top-level conftest files + https://docs.pytest.org/en/stable/deprecations.html """ from __future__ import annotations @@ -33,7 +36,13 @@ def add_doctest_fixtures( request: pytest.FixtureRequest, doctest_namespace: dict[str, t.Any], ) -> None: - """Configure doctest fixtures for pytest-doctest.""" + """Configure doctest fixtures for pytest-doctest. + + Automatically sets up tmux-related classes and default fixtures, + making them available in doctest namespaces if `tmux` is found + on the system. This ensures that doctest blocks referencing tmux + structures can execute smoothly in the test environment. + """ if isinstance(request._pyfuncitem, DoctestItem) and shutil.which("tmux"): request.getfixturevalue("set_home") doctest_namespace["Server"] = Server @@ -54,7 +63,7 @@ def set_home( monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, ) -> None: - """Configure home directory for pytest tests.""" + """Set the HOME environment variable to the temporary user directory.""" monkeypatch.setenv("HOME", str(user_path)) @@ -62,7 +71,7 @@ def set_home( def setup_fn( clear_env: None, ) -> None: - """Function-level test configuration fixtures for pytest.""" + """Apply function-level test fixture configuration (e.g., environment cleanup).""" @pytest.fixture(autouse=True, scope="session") @@ -70,6 +79,10 @@ def setup_session( request: pytest.FixtureRequest, config_file: pathlib.Path, ) -> None: - """Session-level test configuration for pytest.""" + """Apply session-level test fixture configuration for libtmux testing. + + If zsh is in use, applies a suppressing `.zshrc` fix to avoid + default interactive messages that might disrupt tmux sessions. + """ if USING_ZSH: request.getfixturevalue("zshrc") diff --git a/pyproject.toml b/pyproject.toml index 1115cd419..d22d26a3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,11 @@ files = [ "tests", ] +[[tool.mypy.overrides]] +module = "tests.examples.pytest_plugin.*" +disallow_untyped_defs = false +disallow_incomplete_defs = false + [tool.coverage.run] branch = true parallel = true diff --git a/src/libtmux/common.py b/src/libtmux/common.py index db0b4151f..c0815cad0 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -1,8 +1,11 @@ -"""Helper methods and mixins for libtmux. +"""Provide helper methods and mixins for libtmux. + +This module includes helper functions for version checking, environment variable +management, tmux command execution, and other miscellaneous utilities used by +libtmux. It preserves and respects existing doctests without removal. libtmux.common ~~~~~~~~~~~~~~ - """ from __future__ import annotations @@ -20,8 +23,8 @@ if t.TYPE_CHECKING: from collections.abc import Callable -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) #: Minimum version of tmux required to run libtmux TMUX_MIN_VERSION = "1.8" @@ -36,7 +39,7 @@ class EnvironmentMixin: - """Mixin for manager session and server level environment variables in tmux.""" + """Manage session- and server-level environment variables within tmux.""" _add_option = None @@ -46,39 +49,32 @@ def __init__(self, add_option: str | None = None) -> None: self._add_option = add_option def set_environment(self, name: str, value: str) -> None: - """Set environment ``$ tmux set-environment ``. + """Set an environment variable via ``tmux set-environment ``. Parameters ---------- name : str - the environment variable name. such as 'PATH'. - option : str - environment value. + Name of the environment variable (e.g. 'PATH'). + value : str + Value of the environment variable. """ args = ["set-environment"] if self._add_option: args += [self._add_option] - args += [name, value] cmd = self.cmd(*args) - if cmd.stderr: - ( - cmd.stderr[0] - if isinstance(cmd.stderr, list) and len(cmd.stderr) == 1 - else cmd.stderr - ) msg = f"tmux set-environment stderr: {cmd.stderr}" raise ValueError(msg) def unset_environment(self, name: str) -> None: - """Unset environment variable ``$ tmux set-environment -u ``. + """Unset an environment variable via ``tmux set-environment -u ``. Parameters ---------- name : str - the environment variable name. such as 'PATH'. + Name of the environment variable (e.g. 'PATH'). """ args = ["set-environment"] if self._add_option: @@ -86,23 +82,17 @@ def unset_environment(self, name: str) -> None: args += ["-u", name] cmd = self.cmd(*args) - if cmd.stderr: - ( - cmd.stderr[0] - if isinstance(cmd.stderr, list) and len(cmd.stderr) == 1 - else cmd.stderr - ) msg = f"tmux set-environment stderr: {cmd.stderr}" raise ValueError(msg) def remove_environment(self, name: str) -> None: - """Remove environment variable ``$ tmux set-environment -r ``. + """Remove an environment variable via ``tmux set-environment -r ``. Parameters ---------- name : str - the environment variable name. such as 'PATH'. + Name of the environment variable (e.g. 'PATH'). """ args = ["set-environment"] if self._add_option: @@ -110,34 +100,25 @@ def remove_environment(self, name: str) -> None: args += ["-r", name] cmd = self.cmd(*args) - if cmd.stderr: - ( - cmd.stderr[0] - if isinstance(cmd.stderr, list) and len(cmd.stderr) == 1 - else cmd.stderr - ) msg = f"tmux set-environment stderr: {cmd.stderr}" raise ValueError(msg) def show_environment(self) -> dict[str, bool | str]: - """Show environment ``$ tmux show-environment -t [session]``. - - Return dict of environment variables for the session. - - .. versionchanged:: 0.13 - - Removed per-item lookups. Use :meth:`libtmux.common.EnvironmentMixin.getenv`. + """Show environment variables via ``tmux show-environment``. Returns ------- dict - environmental variables in dict, if no name, or str if name - entered. + Dictionary of environment variables for the session. + + .. versionchanged:: 0.13 + Removed per-item lookups. Use :meth:`.getenv` to get a single env var. """ tmux_args = ["show-environment"] if self._add_option: tmux_args += [self._add_option] + cmd = self.cmd(*tmux_args) output = cmd.stdout opts = [tuple(item.split("=", 1)) for item in output] @@ -153,28 +134,26 @@ def show_environment(self) -> dict[str, bool | str]: return opts_dict def getenv(self, name: str) -> str | bool | None: - """Show environment variable ``$ tmux show-environment -t [session] ``. - - Return the value of a specific variable if the name is specified. - - .. versionadded:: 0.13 + """Show value of an environment variable via ``tmux show-environment ``. Parameters ---------- name : str - the environment variable name. such as 'PATH'. + The environment variable name (e.g. 'PATH'). Returns ------- - str - Value of environment variable - """ - tmux_args: tuple[str | int, ...] = () + str or bool or None + The environment variable value, True if set without an '=' value, or + None if not set. - tmux_args += ("show-environment",) + .. versionadded:: 0.13 + """ + tmux_args: list[str | int] = ["show-environment"] if self._add_option: - tmux_args += (self._add_option,) - tmux_args += (name,) + tmux_args += [self._add_option] + tmux_args.append(name) + cmd = self.cmd(*tmux_args) output = cmd.stdout opts = [tuple(item.split("=", 1)) for item in output] @@ -191,7 +170,7 @@ def getenv(self, name: str) -> str | bool | None: class tmux_cmd: - """Run any :term:`tmux(1)` command through :py:mod:`subprocess`. + """Execute a tmux command via :py:mod:`subprocess`. Examples -------- @@ -203,7 +182,6 @@ class tmux_cmd: ... 'Command: %s returned error: %s' % (proc.cmd, proc.stderr) ... ) ... - >>> print(f'tmux command returned {" ".join(proc.stdout)}') tmux command returned 2 @@ -216,7 +194,7 @@ class tmux_cmd: Notes ----- .. versionchanged:: 0.8 - Renamed from ``tmux`` to ``tmux_cmd``. + Renamed from ``tmux`` to ``tmux_cmd``. """ def __init__(self, *args: t.Any) -> None: @@ -229,7 +207,6 @@ def __init__(self, *args: t.Any) -> None: cmd = [str(c) for c in cmd] self.cmd = cmd - try: self.process = subprocess.Popen( cmd, @@ -248,38 +225,36 @@ def __init__(self, *args: t.Any) -> None: stdout_split = stdout.split("\n") # remove trailing newlines from stdout + # remove trailing empty lines while stdout_split and stdout_split[-1] == "": stdout_split.pop() stderr_split = stderr.split("\n") self.stderr = list(filter(None, stderr_split)) # filter empty values + # fix for 'has-session' command output edge cases if "has-session" in cmd and len(self.stderr) and not stdout_split: self.stdout = [self.stderr[0]] else: self.stdout = stdout_split logger.debug( - "self.stdout for {cmd}: {stdout}".format( - cmd=" ".join(cmd), - stdout=self.stdout, - ), + "self.stdout for %s: %s", + " ".join(cmd), + self.stdout, ) def get_version() -> LooseVersion: - """Return tmux version. - - If tmux is built from git master, the version returned will be the latest - version appended with -master, e.g. ``2.4-master``. + """Return the installed tmux version. - If using OpenBSD's base system tmux, the version will have ``-openbsd`` - appended to the latest version, e.g. ``2.4-openbsd``. + If tmux is built from git master, appends '-master', e.g. '2.4-master'. + If using OpenBSD's base system tmux, appends '-openbsd', e.g. '2.4-openbsd'. Returns ------- - :class:`distutils.version.LooseVersion` - tmux version according to :func:`shtuil.which`'s tmux + LooseVersion + Detected tmux version. """ proc = tmux_cmd("-V") if proc.stderr: @@ -287,132 +262,128 @@ def get_version() -> LooseVersion: if sys.platform.startswith("openbsd"): # openbsd has no tmux -V return LooseVersion(f"{TMUX_MAX_VERSION}-openbsd") msg = ( - f"libtmux supports tmux {TMUX_MIN_VERSION} and greater. This system" - " is running tmux 1.3 or earlier." - ) - raise exc.LibTmuxException( - msg, + f"libtmux supports tmux {TMUX_MIN_VERSION} and greater. " + "This system is running tmux 1.3 or earlier." ) + raise exc.LibTmuxException(msg) raise exc.VersionTooLow(proc.stderr) version = proc.stdout[0].split("tmux ")[1] - # Allow latest tmux HEAD + # allow HEAD to be recognized if version == "master": return LooseVersion(f"{TMUX_MAX_VERSION}-master") version = re.sub(r"[a-z-]", "", version) - return LooseVersion(version) def has_version(version: str) -> bool: - """Return True if tmux version installed. + """Return True if the installed tmux version matches exactly. Parameters ---------- version : str - version number, e.g. '1.8' + e.g. '1.8' Returns ------- bool - True if version matches + True if installed tmux matches the version exactly. """ return get_version() == LooseVersion(version) def has_gt_version(min_version: str) -> bool: - """Return True if tmux version greater than minimum. + """Return True if the installed tmux version is greater than min_version. Parameters ---------- min_version : str - tmux version, e.g. '1.8' + e.g. '1.8' Returns ------- bool - True if version above min_version + True if version above min_version. """ return get_version() > LooseVersion(min_version) def has_gte_version(min_version: str) -> bool: - """Return True if tmux version greater or equal to minimum. + """Return True if the installed tmux version is >= min_version. Parameters ---------- min_version : str - tmux version, e.g. '1.8' + e.g. '1.8' Returns ------- bool - True if version above or equal to min_version + True if version is above or equal to min_version. """ return get_version() >= LooseVersion(min_version) def has_lte_version(max_version: str) -> bool: - """Return True if tmux version less or equal to minimum. + """Return True if the installed tmux version is <= max_version. Parameters ---------- max_version : str - tmux version, e.g. '1.8' + e.g. '1.8' Returns ------- bool - True if version below or equal to max_version + True if version is below or equal to max_version. """ return get_version() <= LooseVersion(max_version) def has_lt_version(max_version: str) -> bool: - """Return True if tmux version less than minimum. + """Return True if the installed tmux version is < max_version. Parameters ---------- max_version : str - tmux version, e.g. '1.8' + e.g. '1.8' Returns ------- bool - True if version below max_version + True if version is below max_version. """ return get_version() < LooseVersion(max_version) def has_minimum_version(raises: bool = True) -> bool: - """Return True if tmux meets version requirement. Version >1.8 or above. + """Return True if tmux meets the required minimum version. + + The minimum version is defined by ``TMUX_MIN_VERSION``, default '1.8'. Parameters ---------- - raises : bool - raise exception if below minimum version requirement + raises : bool, optional + If True (default), raise an exception if below the min version. Returns ------- bool - True if tmux meets minimum required version. + True if tmux meets the minimum required version, otherwise False. Raises ------ - libtmux.exc.VersionTooLow - tmux version below minimum required for libtmux + exc.VersionTooLow + If `raises=True` and tmux is below the minimum required version. Notes ----- .. versionchanged:: 0.7.0 - No longer returns version, returns True or False - + No longer returns version, returns True/False. .. versionchanged:: 0.1.7 - Versions will now remove trailing letters per `Issue 55`_. - - .. _Issue 55: https://github.com/tmux-python/tmuxp/issues/55. + Versions remove trailing letters per Issue #55. """ if get_version() < LooseVersion(TMUX_MIN_VERSION): if raises: @@ -427,20 +398,17 @@ def has_minimum_version(raises: bool = True) -> bool: def session_check_name(session_name: str | None) -> None: - """Raise exception session name invalid, modeled after tmux function. - - tmux(1) session names may not be empty, or include periods or colons. - These delimiters are reserved for noting session, window and pane. + """Raise if session name is invalid, as tmux forbids periods/colons. Parameters ---------- session_name : str - Name of session. + The session name to validate. Raises ------ - :exc:`exc.BadSessionName` - Invalid session name. + exc.BadSessionName + If the session name is empty, contains colons, or contains periods. """ if session_name is None or len(session_name) == 0: raise exc.BadSessionName(reason="empty", session_name=session_name) @@ -450,32 +418,26 @@ def session_check_name(session_name: str | None) -> None: raise exc.BadSessionName(reason="contains colons", session_name=session_name) -def handle_option_error(error: str) -> type[exc.OptionError]: - """Raise exception if error in option command found. - - In tmux 3.0, show-option and show-window-option return invalid option instead of - unknown option. See https://github.com/tmux/tmux/blob/3.0/cmd-show-options.c. - - In tmux >2.4, there are 3 different types of option errors: - - - unknown option - - invalid option - - ambiguous option +def handle_option_error(error: str) -> t.NoReturn: + """Raise appropriate exception if an option error is encountered. - In tmux <2.4, unknown option was the only option. + In tmux 3.0, 'show-option' or 'show-window-option' return 'invalid option' + instead of 'unknown option'. In tmux >=2.4, there are three types of + option errors: unknown, invalid, ambiguous. - All errors raised will have the base error of :exc:`exc.OptionError`. So to - catch any option error, use ``except exc.OptionError``. + For older tmux (<2.4), 'unknown option' was the only possibility. Parameters ---------- error : str - Error response from subprocess call. + Error string from tmux. Raises ------ - :exc:`exc.OptionError`, :exc:`exc.UnknownOption`, :exc:`exc.InvalidOption`, - :exc:`exc.AmbiguousOption` + exc.UnknownOption + exc.InvalidOption + exc.AmbiguousOption + exc.OptionError """ if "unknown option" in error: raise exc.UnknownOption(error) @@ -483,16 +445,16 @@ def handle_option_error(error: str) -> type[exc.OptionError]: raise exc.InvalidOption(error) if "ambiguous option" in error: raise exc.AmbiguousOption(error) - raise exc.OptionError(error) # Raise generic option error + raise exc.OptionError(error) def get_libtmux_version() -> LooseVersion: - """Return libtmux version is a PEP386 compliant format. + """Return the PEP386-compliant libtmux version. Returns ------- - distutils.version.LooseVersion - libtmux version + LooseVersion + The libtmux version. """ from libtmux.__about__ import __version__ diff --git a/src/libtmux/exc.py b/src/libtmux/exc.py index 7777403f3..1d5822eec 100644 --- a/src/libtmux/exc.py +++ b/src/libtmux/exc.py @@ -1,8 +1,17 @@ -"""libtmux exceptions. +"""Provide exceptions used by libtmux. libtmux.exc ~~~~~~~~~~~ +This module implements exceptions used throughout libtmux for error +handling in sessions, windows, panes, and general usage. It preserves +existing exception definitions for backward compatibility and does not +remove any doctests. + +Notes +----- +Exceptions in this module inherit from :exc:`LibTmuxException` or +specialized base classes to form a hierarchy of tmux-related errors. """ from __future__ import annotations @@ -16,19 +25,19 @@ class LibTmuxException(Exception): - """Base Exception for libtmux Errors.""" + """Base exception for all libtmux errors.""" class TmuxSessionExists(LibTmuxException): - """Session does not exist in the server.""" + """Raised if a tmux session with the requested name already exists.""" class TmuxCommandNotFound(LibTmuxException): - """Application binary for tmux not found.""" + """Raised when the tmux binary cannot be found on the system.""" class TmuxObjectDoesNotExist(ObjectDoesNotExist): - """The query returned multiple objects when only one was expected.""" + """Raised when a tmux object cannot be found in the server output.""" def __init__( self, @@ -39,19 +48,20 @@ def __init__( *args: object, ) -> None: if all(arg is not None for arg in [obj_key, obj_id, list_cmd, list_extra_args]): - return super().__init__( + super().__init__( f"Could not find {obj_key}={obj_id} for {list_cmd} " f"{list_extra_args if list_extra_args is not None else ''}", ) - return super().__init__("Could not find object") + else: + super().__init__("Could not find object") class VersionTooLow(LibTmuxException): - """Raised if tmux below the minimum version to use libtmux.""" + """Raised if the installed tmux version is below the minimum required.""" class BadSessionName(LibTmuxException): - """Disallowed session name for tmux (empty, contains periods or colons).""" + """Raised if a tmux session name is disallowed (e.g., empty, has colons/periods).""" def __init__( self, @@ -62,83 +72,84 @@ def __init__( msg = f"Bad session name: {reason}" if session_name is not None: msg += f" (session name: {session_name})" - return super().__init__(msg) + super().__init__(msg) class OptionError(LibTmuxException): - """Root error for any error involving invalid, ambiguous or bad options.""" + """Base exception for errors involving invalid, ambiguous, or unknown options.""" class UnknownOption(OptionError): - """Option unknown to tmux show-option(s) or show-window-option(s).""" + """Raised if tmux reports an unknown option.""" class UnknownColorOption(UnknownOption): - """Unknown color option.""" + """Raised if a server color option is unknown (must be 88 or 256).""" def __init__(self, *args: object) -> None: - return super().__init__("Server.colors must equal 88 or 256") + super().__init__("Server.colors must equal 88 or 256") class InvalidOption(OptionError): - """Option invalid to tmux, introduced in tmux v2.4.""" + """Raised if tmux reports an invalid option (tmux >= 2.4).""" class AmbiguousOption(OptionError): - """Option that could potentially match more than one.""" + """Raised if tmux reports an option that could match more than one.""" class WaitTimeout(LibTmuxException): - """Function timed out without meeting condition.""" + """Raised when a function times out waiting for a condition.""" class VariableUnpackingError(LibTmuxException): - """Error unpacking variable.""" + """Raised when an environment variable cannot be unpacked as expected.""" def __init__(self, variable: t.Any | None = None, *args: object) -> None: - return super().__init__(f"Unexpected variable: {variable!s}") + super().__init__(f"Unexpected variable: {variable!s}") class PaneError(LibTmuxException): - """Any type of pane related error.""" + """Base exception for pane-related errors.""" class PaneNotFound(PaneError): - """Pane not found.""" + """Raised if a specified pane cannot be found.""" def __init__(self, pane_id: str | None = None, *args: object) -> None: if pane_id is not None: - return super().__init__(f"Pane not found: {pane_id}") - return super().__init__("Pane not found") + super().__init__(f"Pane not found: {pane_id}") + else: + super().__init__("Pane not found") class WindowError(LibTmuxException): - """Any type of window related error.""" + """Base exception for window-related errors.""" class MultipleActiveWindows(WindowError): - """Multiple active windows.""" + """Raised if multiple active windows are detected (where only one is expected).""" def __init__(self, count: int, *args: object) -> None: - return super().__init__(f"Multiple active windows: {count} found") + super().__init__(f"Multiple active windows: {count} found") class NoActiveWindow(WindowError): - """No active window found.""" + """Raised if no active window exists when one is expected.""" def __init__(self, *args: object) -> None: - return super().__init__("No active windows found") + super().__init__("No active windows found") class NoWindowsExist(WindowError): - """No windows exist for object.""" + """Raised if a session or server has no windows.""" def __init__(self, *args: object) -> None: - return super().__init__("No windows exist for object") + super().__init__("No windows exist for object") class AdjustmentDirectionRequiresAdjustment(LibTmuxException, ValueError): - """If *adjustment_direction* is set, *adjustment* must be set.""" + """Raised if an adjustment direction is set, but no adjustment value is provided.""" def __init__(self) -> None: super().__init__("adjustment_direction requires adjustment") @@ -148,18 +159,18 @@ class WindowAdjustmentDirectionRequiresAdjustment( WindowError, AdjustmentDirectionRequiresAdjustment, ): - """ValueError for :meth:`libtmux.Window.resize_window`.""" + """Raised if window resizing requires an adjustment value, but none is provided.""" class PaneAdjustmentDirectionRequiresAdjustment( WindowError, AdjustmentDirectionRequiresAdjustment, ): - """ValueError for :meth:`libtmux.Pane.resize_pane`.""" + """Raised if pane resizing requires an adjustment value, but none is provided.""" class RequiresDigitOrPercentage(LibTmuxException, ValueError): - """Requires digit (int or str digit) or a percentage.""" + """Raised if a sizing argument must be a digit or a percentage.""" def __init__(self) -> None: super().__init__("Requires digit (int or str digit) or a percentage.") diff --git a/src/libtmux/neo.py b/src/libtmux/neo.py index ab5cd712b..e7c914dc1 100644 --- a/src/libtmux/neo.py +++ b/src/libtmux/neo.py @@ -1,4 +1,21 @@ -"""Tools for hydrating tmux data into python dataclass objects.""" +"""Provide tools for hydrating tmux data into Python dataclass objects. + +This module defines mechanisms for fetching and converting tmux command outputs +into Python dataclasses (via the :class:`Obj` base class). This facilitates +more structured and Pythonic interaction with tmux objects such as sessions, +windows, and panes. + +Implementation Notes +-------------------- +- :func:`fetch_objs` retrieves lists of raw field data from tmux. +- :func:`fetch_obj` retrieves a single tmux object by its key and ID. +- :class:`Obj` is a base dataclass that holds common tmux fields. + +See Also +-------- +:func:`fetch_objs` +:func:`fetch_obj` +""" from __future__ import annotations @@ -12,14 +29,13 @@ from libtmux.formats import FORMAT_SEPARATOR if t.TYPE_CHECKING: + from libtmux.server import Server + ListCmd = t.Literal["list-sessions", "list-windows", "list-panes"] ListExtraArgs = t.Optional[Iterable[str]] - from libtmux.server import Server - logger = logging.getLogger(__name__) - OutputRaw = dict[str, t.Any] OutputsRaw = list[OutputRaw] @@ -36,7 +52,26 @@ @dataclasses.dataclass() class Obj: - """Dataclass of generic tmux object.""" + """Represent a generic tmux dataclass object with standard fields. + + Objects extending this base class derive many fields from tmux commands + via the :func:`fetch_objs` and :func:`fetch_obj` functions. + + Parameters + ---------- + server + The :class:`Server` instance owning this tmux object. + + Attributes + ---------- + pane_id, window_id, session_id, etc. + Various tmux-specific fields automatically populated when refreshed. + + Examples + -------- + Subclasses of :class:`Obj` typically represent concrete tmux entities + (e.g., sessions, windows, and panes). + """ server: Server @@ -91,7 +126,7 @@ class Obj: mouse_standard_flag: str | None = None next_session_id: str | None = None origin_flag: str | None = None - pane_active: str | None = None # Not detected by script + pane_active: str | None = None pane_at_bottom: str | None = None pane_at_left: str | None = None pane_at_right: str | None = None @@ -146,7 +181,7 @@ class Obj: uid: str | None = None user: str | None = None version: str | None = None - window_active: str | None = None # Not detected by script + window_active: str | None = None window_active_clients: str | None = None window_active_sessions: str | None = None window_activity: str | None = None @@ -176,6 +211,24 @@ def _refresh( list_cmd: ListCmd = "list-panes", list_extra_args: ListExtraArgs | None = None, ) -> None: + """Refresh fields for this object by re-fetching from tmux. + + Parameters + ---------- + obj_key + The field name to match (e.g. 'pane_id'). + obj_id + The object identifier (e.g. '%1'). + list_cmd + The tmux command to use (e.g. 'list-panes'). + list_extra_args + Additional arguments to pass to the tmux command. + + Raises + ------ + exc.TmuxObjectDoesNotExist + If the requested object does not exist in tmux's output. + """ assert isinstance(obj_id, str) obj = fetch_obj( obj_key=obj_key, @@ -185,9 +238,8 @@ def _refresh( server=self.server, ) assert obj is not None - if obj is not None: - for k, v in obj.items(): - setattr(self, k, v) + for k, v in obj.items(): + setattr(self, k, v) def fetch_objs( @@ -195,40 +247,54 @@ def fetch_objs( list_cmd: ListCmd, list_extra_args: ListExtraArgs | None = None, ) -> OutputsRaw: - """Fetch a listing of raw data from a tmux command.""" + """Fetch a list of raw data from a tmux command. + + Parameters + ---------- + server + The :class:`Server` against which to run the command. + list_cmd + The tmux command to run (e.g. 'list-sessions', 'list-windows', 'list-panes'). + list_extra_args + Any extra arguments (e.g. ['-a']). + + Returns + ------- + list of dict + A list of dictionaries of field-name to field-value mappings. + + Raises + ------ + exc.LibTmuxException + If tmux reports an error in stderr. + """ formats = list(Obj.__dataclass_fields__.keys()) cmd_args: list[str | int] = [] - if server.socket_name: cmd_args.insert(0, f"-L{server.socket_name}") if server.socket_path: cmd_args.insert(0, f"-S{server.socket_path}") - tmux_formats = [f"#{{{f}}}{FORMAT_SEPARATOR}" for f in formats] - tmux_cmds = [ - *cmd_args, - list_cmd, - ] + tmux_formats = [f"#{{{f}}}{FORMAT_SEPARATOR}" for f in formats] + tmux_cmds = [*cmd_args, list_cmd] if list_extra_args is not None and isinstance(list_extra_args, Iterable): tmux_cmds.extend(list(list_extra_args)) tmux_cmds.append("-F{}".format("".join(tmux_formats))) - - proc = tmux_cmd(*tmux_cmds) # output + proc = tmux_cmd(*tmux_cmds) if proc.stderr: raise exc.LibTmuxException(proc.stderr) obj_output = proc.stdout - obj_formatters = [ dict(zip(formats, formatter.split(FORMAT_SEPARATOR))) for formatter in obj_output ] - # Filter empty values + # Filter out empty values return [{k: v for k, v in formatter.items() if v} for formatter in obj_formatters] @@ -239,7 +305,31 @@ def fetch_obj( list_cmd: ListCmd = "list-panes", list_extra_args: ListExtraArgs | None = None, ) -> OutputRaw: - """Fetch raw data from tmux command.""" + """Fetch a single tmux object by key and ID. + + Parameters + ---------- + server + The :class:`Server` instance to query. + obj_key + The field name to look for (e.g., 'pane_id'). + obj_id + The specific ID to match (e.g., '%0'). + list_cmd + The tmux command to run ('list-panes', 'list-windows', etc.). + list_extra_args + Extra arguments to pass (e.g., ['-a']). + + Returns + ------- + dict + A dictionary of field-name to field-value mappings for the object. + + Raises + ------ + exc.TmuxObjectDoesNotExist + If no matching object is found in tmux's output. + """ obj_formatters_filtered = fetch_objs( server=server, list_cmd=list_cmd, @@ -250,6 +340,7 @@ def fetch_obj( for _obj in obj_formatters_filtered: if _obj.get(obj_key) == obj_id: obj = _obj + break if obj is None: raise exc.TmuxObjectDoesNotExist( @@ -259,6 +350,4 @@ def fetch_obj( list_extra_args=list_extra_args, ) - assert obj is not None - return obj diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index a60bb36f6..6858c4889 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -1,8 +1,12 @@ -"""Pythonization of the :ref:`tmux(1)` pane. +"""Provide a Pythonic representation of the :ref:`tmux(1)` pane. + +The :class:`Pane` class models a single tmux pane, allowing commands to be +sent directly to it, as well as traversal to related :class:`Window` and +:class:`Session` objects. It offers convenience methods for splitting, resizing, +and interacting with the pane's contents. libtmux.pane ~~~~~~~~~~~~ - """ from __future__ import annotations @@ -50,7 +54,9 @@ class Pane(Obj): Attributes ---------- - window : :class:`Window` + server : Server + pane_id : str + For example '%1'. Examples -------- @@ -145,12 +151,9 @@ def from_pane_id(cls, server: Server, pane_id: str) -> Pane: ) return cls(server=server, **pane) - # - # Relations - # @property def window(self) -> Window: - """Parent window of pane.""" + """Return the parent :class:`Window` of this pane.""" assert isinstance(self.window_id, str) from libtmux.window import Window @@ -158,23 +161,35 @@ def window(self) -> Window: @property def session(self) -> Session: - """Parent session of pane.""" + """Return the parent :class:`Session` of this pane.""" return self.window.session - """ - Commands (pane-scoped) - """ - + # + # Commands (pane-scoped) + # def cmd( self, cmd: str, *args: t.Any, target: str | int | None = None, ) -> tmux_cmd: - """Execute tmux subcommand within pane context. + """Execute a tmux command in the context of this pane. - Automatically binds target by adding ``-t`` for object's pane ID to the - command. Pass ``target`` to keyword arguments to override. + Automatically sets ``-t `` unless overridden by `target`. + + Parameters + ---------- + cmd + The tmux subcommand to run (e.g., 'split-window'). + *args + Additional arguments for the tmux command. + target, optional + Custom target. Default is the current pane's ID. + + Returns + ------- + tmux_cmd + Result of the tmux command execution. Examples -------- @@ -183,75 +198,62 @@ def cmd( From raw output to an enriched `Pane` object: - >>> Pane.from_pane_id(pane_id=pane.cmd( - ... 'split-window', '-P', '-F#{pane_id}').stdout[0], server=pane.server) + >>> Pane.from_pane_id( + ... pane_id=pane.cmd('split-window', '-P', '-F#{pane_id}').stdout[0], + ... server=pane.server + ... ) Pane(%... Window(@... ...:..., Session($1 libtmux_...))) - - Parameters - ---------- - target : str, optional - Optional custom target override. By default, the target is the pane ID. - - Returns - ------- - :meth:`server.cmd` """ if target is None: target = self.pane_id - return self.server.cmd(cmd, *args, target=target) - """ - Commands (tmux-like) - """ - + # + # Commands (tmux-like) + # def resize( self, /, - # Adjustments adjustment_direction: ResizeAdjustmentDirection | None = None, adjustment: int | None = None, - # Manual height: str | int | None = None, width: str | int | None = None, - # Zoom zoom: bool | None = None, - # Mouse mouse: bool | None = None, - # Optional flags trim_below: bool | None = None, ) -> Pane: - """Resize tmux pane. + """Resize this tmux pane. Parameters ---------- adjustment_direction : ResizeAdjustmentDirection, optional - direction to adjust, ``Up``, ``Down``, ``Left``, ``Right``. - adjustment : ResizeAdjustmentDirection, optional - - height : int, optional - ``resize-pane -y`` dimensions - width : int, optional - ``resize-pane -x`` dimensions - - zoom : bool - expand pane - - mouse : bool - resize via mouse - - trim_below : bool - trim below cursor + Direction to adjust, ``Up``, ``Down``, ``Left``, ``Right``. + adjustment : int, optional + Number of cells to move in the specified direction. + height : int or str, optional + ``resize-pane -y`` dimension, e.g. 20 or "50%". + width : int or str, optional + ``resize-pane -x`` dimension, e.g. 80 or "25%". + zoom : bool, optional + If True, expand (zoom) the pane to occupy the entire window. + mouse : bool, optional + If True, resize via mouse (``-M``). + trim_below : bool, optional + If True, trim below cursor (``-T``). Raises ------ - :exc:`exc.LibTmuxException`, - :exc:`exc.PaneAdjustmentDirectionRequiresAdjustment`, + :exc:`exc.LibTmuxException` + If tmux reports an error. + :exc:`exc.PaneAdjustmentDirectionRequiresAdjustment` + If `adjustment_direction` is given but no `adjustment`. :exc:`exc.RequiresDigitOrPercentage` + If a provided dimension is neither a digit nor ends with "%". Returns ------- :class:`Pane` + This pane. Notes ----- @@ -271,13 +273,13 @@ def resize( f"{RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP[adjustment_direction]}", str(adjustment), ) + # Manual resizing elif height or width: - # Manual resizing if height: if isinstance(height, str): if height.endswith("%") and not has_gte_version("3.1"): raise exc.VersionTooLow - if not height.isdigit() and not height.endswith("%"): + if not (height.isdigit() or height.endswith("%")): raise exc.RequiresDigitOrPercentage tmux_args += (f"-y{height}",) @@ -285,13 +287,13 @@ def resize( if isinstance(width, str): if width.endswith("%") and not has_gte_version("3.1"): raise exc.VersionTooLow - if not width.isdigit() and not width.endswith("%"): + if not (width.isdigit() or width.endswith("%")): raise exc.RequiresDigitOrPercentage - tmux_args += (f"-x{width}",) + # Zoom / Unzoom elif zoom: - # Zoom / Unzoom tmux_args += ("-Z",) + # Mouse-based resize elif mouse: tmux_args += ("-M",) @@ -299,7 +301,6 @@ def resize( tmux_args += ("-T",) proc = self.cmd("resize-pane", *tmux_args) - if proc.stderr: raise exc.LibTmuxException(proc.stderr) @@ -311,29 +312,28 @@ def capture_pane( start: t.Literal["-"] | int | None = None, end: t.Literal["-"] | int | None = None, ) -> str | list[str]: - """Capture text from pane. + """Capture text from this pane (``tmux capture-pane -p``). - ``$ tmux capture-pane`` to pane. - ``$ tmux capture-pane -S -10`` to pane. - ``$ tmux capture-pane`-E 3` to pane. - ``$ tmux capture-pane`-S - -E -` to pane. + ``$ tmux capture-pane -S -10`` etc. Parameters ---------- - start: [str,int] - Specify the starting line number. - Zero is the first line of the visible pane. - Positive numbers are lines in the visible pane. - Negative numbers are lines in the history. - `-` is the start of the history. - Default: None - end: [str,int] - Specify the ending line number. - Zero is the first line of the visible pane. - Positive numbers are lines in the visible pane. - Negative numbers are lines in the history. - `-` is the end of the visible pane - Default: None + start : int, '-', optional + Starting line number. + end : int, '-', optional + Ending line number. + + Returns + ------- + str or list[str] + The captured pane text as a list of lines (by default). + + Examples + -------- + Basic usage: + + >>> pane.capture_pane() + [...] """ cmd = ["capture-pane", "-p"] if start is not None: @@ -349,25 +349,22 @@ def send_keys( suppress_history: bool | None = False, literal: bool | None = False, ) -> None: - r"""``$ tmux send-keys`` to the pane. + r"""Send keys (as keyboard input) to this pane. - A leading space character is added to cmd to avoid polluting the - user's history. + A leading space character can be added to `cmd` to avoid polluting + the user's shell history. Parameters ---------- cmd : str - Text or input into pane + Text or input to send. enter : bool, optional - Send enter after sending the input, default True. + If True, send Enter after the input (default). suppress_history : bool, optional - Prepend a space to command to suppress shell history, default False. - - .. versionchanged:: 0.14 - - Default changed from True to False. + If True, prepend a space to the command, preventing it from + appearing in shell history. Default is False. literal : bool, optional - Send keys literally, default True. + If True, send keys literally (``-l``). Default is False. Examples -------- @@ -410,21 +407,24 @@ def display_message( cmd: str, get_text: bool = False, ) -> str | list[str] | None: - """Display message to pane. + """Display or retrieve a message in this pane. - Displays a message in target-client status line. + Uses ``$ tmux display-message``. Parameters ---------- cmd : str - Special parameters to request from pane. + The message or format string to display. get_text : bool, optional - Returns only text without displaying a message in - target-client status line. + If True, return the text instead of displaying it. + + Returns + ------- + str, list[str], or None + The displayed text if `get_text` is True, else None. """ if get_text: return self.cmd("display-message", "-p", cmd).stdout - self.cmd("display-message", cmd) return None @@ -432,9 +432,17 @@ def kill( self, all_except: bool | None = None, ) -> None: - """Kill :class:`Pane`. + """Kill this :class:`Pane` (``tmux kill-pane``). + + Parameters + ---------- + all_except : bool, optional + If True, kill all panes except this one. - ``$ tmux kill-pane``. + Raises + ------ + exc.LibTmuxException + If tmux reports an error. Examples -------- @@ -472,27 +480,28 @@ def kill( True """ flags: tuple[str, ...] = () - if all_except: flags += ("-a",) - proc = self.cmd( - "kill-pane", - *flags, - ) - + proc = self.cmd("kill-pane", *flags) if proc.stderr: raise exc.LibTmuxException(proc.stderr) - """ - Commands ("climber"-helpers) + # + # "Climber"-helpers + # + def select(self) -> Pane: + """Select this pane (make it the active pane in its window). - These are commands that climb to the parent scope's methods with - additional scoped window info. - """ + Returns + ------- + Pane + This :class:`Pane`. - def select(self) -> Pane: - """Select pane. + Raises + ------ + exc.LibTmuxException + If tmux reports an error. Examples -------- @@ -516,22 +525,16 @@ def select(self) -> Pane: True """ proc = self.cmd("select-pane") - if proc.stderr: raise exc.LibTmuxException(proc.stderr) - self.refresh() - return self def select_pane(self) -> Pane: - """Select pane. + """Select this pane (deprecated). - Notes - ----- .. deprecated:: 0.30 - - Deprecated in favor of :meth:`.select()`. + Use :meth:`.select()`. """ warnings.warn( "Pane.select_pane() is deprecated in favor of Pane.select()", @@ -557,34 +560,33 @@ def split( size: str | int | None = None, environment: dict[str, str] | None = None, ) -> Pane: - """Split window and return :class:`Pane`, by default beneath current pane. + """Split this pane, returning a new :class:`Pane`. + + By default, splits beneath the current pane. Specify a direction to + split horizontally or vertically, a size, and optionally run a shell + command in the new pane. Parameters ---------- - target : optional - Optional, custom *target-pane*, used by :meth:`Window.split`. - attach : bool, optional - make new window the current window after creating it, default - True. + target : int or str, optional + Custom *target-pane*. Defaults to this pane's ID. start_directory : str, optional - specifies the working directory in which the new window is created. + Working directory for the new pane. + attach : bool, optional + If True, select the new pane immediately (default is False). direction : PaneDirection, optional - split in direction. If none is specified, assume down. - full_window_split: bool, optional - split across full window width or height, rather than active pane. - zoom: bool, optional - expand pane + Direction to split, e.g. :attr:`PaneDirection.Right`. + full_window_split : bool, optional + If True, split across the entire window height/width. + zoom : bool, optional + If True, zoom the new pane (``-Z``). shell : str, optional - execute a command on splitting the window. The pane will close - when the command exits. - - NOTE: When this command exits the pane will close. This feature - is useful for long-running processes where the closing of the - window upon completion is desired. - size: int, optional - Cell/row or percentage to occupy with respect to current window. - environment: dict, optional - Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + Command to run immediately in the new pane. The pane closes when + the command exits. + size : int or str, optional + Size for the new pane (cells or percentage). + environment : dict, optional + Environment variables for the new pane (tmux 3.0+). Examples -------- @@ -623,7 +625,8 @@ def split( >>> pane = session.new_window().active_pane - >>> top_pane = pane.split(direction=PaneDirection.Above, full_window_split=True) + >>> top_pane = pane.split(direction=PaneDirection.Above, + ... full_window_split=True) >>> (top_pane.at_left, top_pane.at_right, ... top_pane.at_top, top_pane.at_bottom) @@ -631,16 +634,15 @@ def split( True, False) >>> bottom_pane = pane.split( - ... direction=PaneDirection.Below, - ... full_window_split=True) + ... direction=PaneDirection.Below, + ... full_window_split=True) >>> (bottom_pane.at_left, bottom_pane.at_right, ... bottom_pane.at_top, bottom_pane.at_bottom) (True, True, False, True) """ - tmux_formats = ["#{pane_id}" + FORMAT_SEPARATOR] - + tmux_formats = [f"#{'{'}pane_id{'}'}{FORMAT_SEPARATOR}"] tmux_args: tuple[str, ...] = () if direction: @@ -654,7 +656,7 @@ def split( tmux_args += (f"-p{str(size).rstrip('%')}",) else: warnings.warn( - 'Ignored size. Use percent in tmux < 3.1, e.g. "size=50%"', + 'Ignored size. Use percent in tmux < 3.1, e.g. "50%"', stacklevel=2, ) else: @@ -662,14 +664,12 @@ def split( if full_window_split: tmux_args += ("-f",) - if zoom: tmux_args += ("-Z",) - tmux_args += ("-P", "-F{}".format("".join(tmux_formats))) # output + tmux_args += ("-P", "-F{}".format("".join(tmux_formats))) if start_directory is not None: - # 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}",) @@ -689,12 +689,9 @@ def split( tmux_args += (shell,) pane_cmd = self.cmd("split-window", *tmux_args, target=target) - - # tmux < 1.7. This is added in 1.7. if pane_cmd.stderr: if "pane too small" in pane_cmd.stderr: raise exc.LibTmuxException(pane_cmd.stderr) - raise exc.LibTmuxException( pane_cmd.stderr, self.__dict__, @@ -702,52 +699,34 @@ def split( ) pane_output = pane_cmd.stdout[0] - pane_formatters = dict(zip(["pane_id"], pane_output.split(FORMAT_SEPARATOR))) - return self.from_pane_id(server=self.server, pane_id=pane_formatters["pane_id"]) - """ - Commands (helpers) - """ - + # + # Commands (helpers) + # def set_width(self, width: int) -> Pane: - """Set pane width. - - Parameters - ---------- - width : int - pane width, in cells - """ + """Set pane width in cells.""" self.resize_pane(width=width) return self def set_height(self, height: int) -> Pane: - """Set pane height. - - Parameters - ---------- - height : int - height of pain, in cells - """ + """Set pane height in cells.""" self.resize_pane(height=height) return self def enter(self) -> Pane: - """Send carriage return to pane. - - ``$ tmux send-keys`` send Enter to the pane. - """ + """Send an Enter keypress to this pane.""" self.cmd("send-keys", "Enter") return self def clear(self) -> Pane: - """Clear pane.""" + """Clear the pane by sending 'reset' command.""" self.send_keys("reset") return self def reset(self) -> Pane: - """Reset and clear pane history.""" + """Reset the pane and clear its history.""" self.cmd("send-keys", r"-R \; clear-history") return self @@ -755,13 +734,13 @@ def reset(self) -> Pane: # Dunder # def __eq__(self, other: object) -> bool: - """Equal operator for :class:`Pane` object.""" + """Compare two panes by their ``pane_id``.""" if isinstance(other, Pane): return self.pane_id == other.pane_id return False def __repr__(self) -> str: - """Representation of :class:`Pane` object.""" + """Return a string representation of this :class:`Pane`.""" return f"{self.__class__.__name__}({self.pane_id} {self.window})" # @@ -876,27 +855,31 @@ def split_window( size: str | int | None = None, percent: int | None = None, # deprecated environment: dict[str, str] | None = None, - ) -> Pane: # New Pane, not self - """Split window at pane and return newly created :class:`Pane`. + ) -> Pane: + """Split this pane and return the newly created :class:`Pane` (deprecated). + + .. deprecated:: 0.33 + Use :meth:`.split`. Parameters ---------- + target, optional + Target for the new pane. attach : bool, optional - Attach / select pane after creation. + If True, select the new pane immediately. start_directory : str, optional - specifies the working directory in which the new pane is created. + Working directory for the new pane. vertical : bool, optional - split vertically - percent: int, optional - percentage to occupy with respect to current pane - environment: dict, optional - Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. - - Notes - ----- - .. deprecated:: 0.33 - - Deprecated in favor of :meth:`.split`. + If True (default), split vertically (below). + shell : str, optional + Command to run in the new pane. Pane closes when command exits. + size : str or int, optional + Size for the new pane (cells or percentage). + percent : int, optional + If provided, is converted to a string with a trailing '%' for + older tmux. E.g. '25%'. + environment : dict[str, str], optional + Environment variables for the new pane (tmux 3.0+). """ warnings.warn( "Pane.split_window() is deprecated in favor of Pane.split()", @@ -916,13 +899,10 @@ def split_window( ) def get(self, key: str, default: t.Any | None = None) -> t.Any: - """Return key-based lookup. Deprecated by attributes. + """Return a key-based lookup (deprecated). .. deprecated:: 0.16 - - Deprecated by attribute lookup, e.g. ``pane['window_name']`` is now - accessed via ``pane.window_name``. - + Deprecated by attribute lookup, e.g. ``pane.window_name``. """ warnings.warn( "Pane.get() is deprecated", @@ -932,13 +912,10 @@ def get(self, key: str, default: t.Any | None = None) -> t.Any: return getattr(self, key, default) def __getitem__(self, key: str) -> t.Any: - """Return item lookup by key. Deprecated in favor of attributes. + """Return an item by key (deprecated). .. deprecated:: 0.16 - - Deprecated in favor of attributes. e.g. ``pane['window_name']`` is now - accessed via ``pane.window_name``. - + Deprecated in favor of attributes. e.g. ``pane.window_name``. """ warnings.warn( f"Item lookups, e.g. pane['{key}'] is deprecated", @@ -949,24 +926,18 @@ def __getitem__(self, key: str) -> t.Any: def resize_pane( self, - # Adjustments adjustment_direction: ResizeAdjustmentDirection | None = None, adjustment: int | None = None, - # Manual height: str | int | None = None, width: str | int | None = None, - # Zoom zoom: bool | None = None, - # Mouse mouse: bool | None = None, - # Optional flags trim_below: bool | None = None, ) -> Pane: - """Resize pane, deprecated by :meth:`Pane.resize`. + """Resize this pane (deprecated). .. deprecated:: 0.28 - - Deprecated by :meth:`Pane.resize`. + Use :meth:`.resize`. """ warnings.warn( "Deprecated: Use Pane.resize() instead of Pane.resize_pane()", diff --git a/src/libtmux/pytest_plugin.py b/src/libtmux/pytest_plugin.py index 92da4676d..4c660fb6d 100644 --- a/src/libtmux/pytest_plugin.py +++ b/src/libtmux/pytest_plugin.py @@ -1,4 +1,16 @@ -"""libtmux pytest plugin.""" +"""Provide a pytest plugin that supplies libtmux testing fixtures. + +This plugin integrates with pytest to offer session, window, and environment +fixtures tailored for tmux-based tests. It ensures stable test environments by +creating and tearing down temporary sessions and windows for each test as +needed. + +Notes +----- +The existing doctests embedded within each fixture are preserved to maintain +clarity and verify core behaviors. + +""" from __future__ import annotations @@ -68,7 +80,8 @@ def config_file(user_path: pathlib.Path) -> pathlib.Path: - ``base-index -g 1`` - These guarantee pane and windows targets can be reliably referenced and asserted. + These guarantee pane and windows targets can be reliably referenced + and asserted. Note: You will need to set the home directory, see :ref:`set_home`. """ @@ -86,7 +99,8 @@ def config_file(user_path: pathlib.Path) -> pathlib.Path: def clear_env(monkeypatch: pytest.MonkeyPatch) -> None: """Clear out any unnecessary environment variables that could interrupt tests. - tmux show-environment tests were being interrupted due to a lot of crazy env vars. + tmux show-environment tests were being interrupted due to a lot of + crazy env vars. """ for k in os.environ: if not any( diff --git a/src/libtmux/server.py b/src/libtmux/server.py index 1eaf82f66..ef443efc2 100644 --- a/src/libtmux/server.py +++ b/src/libtmux/server.py @@ -1,12 +1,32 @@ -"""Wrapper for :term:`tmux(1)` server. +"""Wrap the :term:`tmux(1)` server. + +This module manages the top-level tmux server, allowing for the creation, +control, and inspection of sessions, windows, and panes across a single server +instance. It provides the :class:`Server` class, which acts as a gateway +to the tmux server process. libtmux.server ~~~~~~~~~~~~~~ +Examples +-------- +>>> server.is_alive() # Check if tmux server is running +True +>>> # Clean up any existing test session first +>>> if server.has_session("test_session"): +... server.kill_session("test_session") +>>> new_session = server.new_session(session_name="test_session") +>>> new_session.name +'test_session' +>>> server.has_session("test_session") +True +>>> server.kill_session("test_session") # Clean up +Server(socket_name=libtmux_test...) """ from __future__ import annotations +import contextlib import logging import os import pathlib @@ -47,24 +67,31 @@ class Server(EnvironmentMixin): - """:term:`tmux(1)` :term:`Server` [server_manual]_. + """Represent a :term:`tmux(1)` server [server_manual]_. - - :attr:`Server.sessions` [:class:`Session`, ...] + This class provides the ability to create, manage, and destroy tmux + sessions and their associated windows and panes. It is the top-level + interface to the tmux server process, allowing you to query and control + all sessions within it. - - :attr:`Session.windows` [:class:`Window`, ...] + - :attr:`Server.sessions` => list of :class:`Session` - - :attr:`Window.panes` [:class:`Pane`, ...] + - :attr:`Session.windows` => list of :class:`Window` - - :class:`Pane` + - :attr:`Window.panes` => list of :class:`Pane` - When instantiated stores information on live, running tmux server. + When instantiated, it stores information about a live, running tmux server. Parameters ---------- socket_name : str, optional - socket_path : str, optional + Equivalent to tmux's ``-L `` option. + socket_path : str or pathlib.Path, optional + Equivalent to tmux's ``-S `` option. config_file : str, optional - colors : str, optional + Equivalent to tmux's ``-f `` option. + colors : int, optional + Can be 88 or 256 to specify supported colors (via ``-2`` or ``-8``). on_init : callable, optional socket_name_factory : callable, optional @@ -102,22 +129,22 @@ class Server(EnvironmentMixin): into it. Windows may be linked to multiple sessions and are made up of one or more panes, each of which contains a pseudo terminal." - https://man.openbsd.org/tmux.1#CLIENTS_AND_SESSIONS. + https://man.openbsd.org/tmux.1#CLIENTS_AND_SESSIONS Accessed April 1st, 2018. """ socket_name = None - """Passthrough to ``[-L socket-name]``""" + """Passthrough to ``[-L socket-name]``.""" socket_path = None - """Passthrough to ``[-S socket-path]``""" + """Passthrough to ``[-S socket-path]``.""" config_file = None - """Passthrough to ``[-f file]``""" + """Passthrough to ``[-f file]``.""" colors = None - """``256`` or ``88``""" + """May be ``-2`` or ``-8`` depending on color support (256 or 88).""" child_id_attribute = "session_id" - """Unique child ID used by :class:`~libtmux.common.TmuxRelationalObject`""" + """Unique child ID used by :class:`~libtmux.common.TmuxRelationalObject`.""" formatter_prefix = "server_" - """Namespace used for :class:`~libtmux.common.TmuxMappingObject`""" + """Namespace used for :class:`~libtmux.common.TmuxMappingObject`.""" def __init__( self, @@ -129,6 +156,27 @@ def __init__( socket_name_factory: t.Callable[[], str] | None = None, **kwargs: t.Any, ) -> None: + """Initialize the Server object, optionally specifying socket and config. + + If both ``socket_path`` and ``socket_name`` are provided, ``socket_path`` + takes precedence. + + Parameters + ---------- + socket_name : str, optional + Socket name for tmux server (-L flag). + socket_path : str or pathlib.Path, optional + Socket path for tmux server (-S flag). + config_file : str, optional + Path to a tmux config file (-f flag). + colors : int, optional + If 256, pass ``-2`` to tmux; if 88, pass ``-8``. + + Other Parameters + ---------------- + **kwargs + Additional keyword arguments are ignored. + """ EnvironmentMixin.__init__(self, "-g") self._windows: list[WindowDict] = [] self._panes: list[PaneDict] = [] @@ -142,6 +190,8 @@ def __init__( tmux_tmpdir = pathlib.Path(os.getenv("TMUX_TMPDIR", "/tmp")) socket_name = self.socket_name or "default" + + # If no path is given and socket_name is not the default, build a path if ( tmux_tmpdir is not None and self.socket_path is None @@ -190,8 +240,10 @@ def __exit__( self.kill() def is_alive(self) -> bool: - """Return True if tmux server alive. + """Return True if the tmux server is alive and responding. + Examples + -------- >>> tmux = Server(socket_name="no_exist") >>> assert not tmux.is_alive() """ @@ -202,7 +254,7 @@ def is_alive(self) -> bool: return res.returncode == 0 def raise_if_dead(self) -> None: - """Raise if server not connected. + """Raise an error if the tmux server is not reachable. >>> tmux = Server(socket_name="no_exist") >>> try: @@ -210,6 +262,13 @@ def raise_if_dead(self) -> None: ... except Exception as e: ... print(type(e)) + + Raises + ------ + exc.TmuxCommandNotFound + If the tmux binary is not found in PATH. + subprocess.CalledProcessError + If the tmux server is not responding properly. """ tmux_bin = shutil.which("tmux") if tmux_bin is None: @@ -225,16 +284,26 @@ def raise_if_dead(self) -> None: subprocess.check_call([tmux_bin, *cmd_args]) - # - # Command - # def cmd( self, cmd: str, *args: t.Any, target: str | int | None = None, ) -> tmux_cmd: - """Execute tmux command respective of socket name and file, return output. + """Execute a tmux command with this server's configured socket and file. + + The returned object contains information about the tmux command + execution, including stdout, stderr, and exit code. + + Parameters + ---------- + cmd + The tmux subcommand to execute (e.g., 'list-sessions'). + *args + Additional arguments for the subcommand. + target, optional + Optional target for the command (usually specifies a session, + window, or pane). Examples -------- @@ -246,43 +315,36 @@ def cmd( >>> server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0] '$2' - >>> session.cmd('new-window', '-P').stdout[0] - 'libtmux...:2.0' + You can then convert raw tmux output to rich objects: - Output of `tmux -L ... new-window -P -F#{window_id}` to a `Window` object: - - >>> Window.from_window_id(window_id=session.cmd( - ... 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server) - Window(@4 3:..., Session($1 libtmux_...)) + >>> from libtmux.window import Window + >>> Window.from_window_id( + ... window_id=session.cmd('new-window', '-P', '-F#{window_id}').stdout[0], + ... server=window.server + ... ) + Window(@3 2:..., Session($1 libtmux_...)) Create a pane from a window: - >>> window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0] - '%5' - - Output of `tmux -L ... split-window -P -F#{pane_id}` to a `Pane` object: + '%4' + Output of ``tmux -L ... split-window -P -F#{pane_id}`` to a :class:`Pane`: >>> Pane.from_pane_id(pane_id=window.cmd( ... 'split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server) Pane(%... Window(@... ...:..., Session($1 libtmux_...))) - Parameters - ---------- - target : str, optional - Optional custom target. - Returns ------- - :class:`common.tmux_cmd` + tmux_cmd + Object that wraps stdout, stderr, and return code from the tmux call. Notes ----- .. versionchanged:: 0.8 - - Renamed from ``.tmux`` to ``.cmd``. + Renamed from ``.tmux`` to ``.cmd``. """ svr_args: list[str | int] = [cmd] - cmd_args: list[str | int] = [] + if self.socket_name: svr_args.insert(0, f"-L{self.socket_name}") if self.socket_path: @@ -298,21 +360,18 @@ def cmd( raise exc.UnknownColorOption cmd_args = ["-t", str(target), *args] if target is not None else [*args] - return tmux_cmd(*svr_args, *cmd_args) @property def attached_sessions(self) -> list[Session]: - """Return active :class:`Session`s. + """Return a list of currently attached sessions. + + Attached sessions are those where ``session_attached`` is not '1'. Examples -------- >>> server.attached_sessions [] - - Returns - ------- - list of :class:`Session` """ return self.sessions.filter(session_attached__noeq="1") @@ -322,19 +381,28 @@ def has_session(self, target_session: str, exact: bool = True) -> bool: Parameters ---------- target_session : str - session name - exact : bool - match the session name exactly. tmux uses fnmatch by default. - Internally prepends ``=`` to the session in ``$ tmux has-session``. - tmux 2.1 and up only. + Target session name to check + exact : bool, optional + If True, match the name exactly. Otherwise, match as a pattern. - Raises - ------ - :exc:`exc.BadSessionName` - - Returns - ------- - bool + Examples + -------- + >>> # Clean up any existing test session + >>> if server.has_session("test_session"): + ... server.kill_session("test_session") + >>> server.new_session(session_name="test_session") + Session($... test_session) + >>> server.has_session("test_session") + True + >>> server.has_session("nonexistent") + False + >>> server.has_session("test_session", exact=True) # Exact match + True + >>> # Pattern matching (using tmux's pattern matching) + >>> server.has_session("test_sess*", exact=False) # Pattern match + True + >>> server.kill_session("test_session") # Clean up + Server(socket_name=libtmux_test...) """ session_check_name(target_session) @@ -342,87 +410,119 @@ def has_session(self, target_session: str, exact: bool = True) -> bool: target_session = f"={target_session}" proc = self.cmd("has-session", target=target_session) - - return bool(not proc.returncode) + return proc.returncode == 0 def kill(self) -> None: - """Kill tmux server. + """Kill the entire tmux server. - >>> svr = Server(socket_name="testing") - >>> svr - Server(socket_name=testing) + This closes all sessions, windows, and panes associated with it. - >>> svr.new_session() + Examples + -------- + >>> # Create a new server for testing kill() + >>> test_server = Server(socket_name="testing") + >>> test_server.new_session() Session(...) - - >>> svr.is_alive() + >>> test_server.is_alive() True - - >>> svr.kill() - - >>> svr.is_alive() + >>> test_server.kill() + >>> test_server.is_alive() False """ self.cmd("kill-server") def kill_session(self, target_session: str | int) -> Server: - """Kill tmux session. + """Kill a session by name. Parameters ---------- - target_session : str, optional - target_session: str. note this accepts ``fnmatch(3)``. 'asdf' will - kill 'asdfasd'. + target_session : str or int + Name of the session or session ID to kill - Returns - ------- - :class:`Server` - - Raises - ------ - :exc:`exc.BadSessionName` + Examples + -------- + >>> # Clean up any existing session first + >>> if server.has_session("temp"): + ... server.kill_session("temp") + >>> session = server.new_session(session_name="temp") + >>> server.has_session("temp") + True + >>> server.kill_session("temp") + Server(socket_name=libtmux_test...) + >>> server.has_session("temp") + False """ proc = self.cmd("kill-session", target=target_session) - if proc.stderr: raise exc.LibTmuxException(proc.stderr) - return self def switch_client(self, target_session: str) -> None: - """Switch tmux client. + """Switch a client to a different session. Parameters ---------- - target_session : str - name of the session. fnmatch(3) works. + target_session + The name or pattern of the target session. + + Examples + -------- + >>> # Create two test sessions + >>> for name in ["session1", "session2"]: + ... if server.has_session(name): + ... server.kill_session(name) + >>> session1 = server.new_session(session_name="session1") + >>> session2 = server.new_session(session_name="session2") + >>> # Note: switch_client() requires an interactive terminal + >>> # so we can't demonstrate it in doctests + >>> # Clean up + >>> server.kill_session("session1") + Server(socket_name=libtmux_test...) + >>> server.kill_session("session2") + Server(socket_name=libtmux_test...) Raises ------ - :exc:`exc.BadSessionName` + exc.BadSessionName + If the session name is invalid. + exc.LibTmuxException + If tmux reports an error (stderr output). """ session_check_name(target_session) - proc = self.cmd("switch-client", target=target_session) - if proc.stderr: raise exc.LibTmuxException(proc.stderr) def attach_session(self, target_session: str | None = None) -> None: - """Attach tmux session. + """Attach to a specific session, making it the active client. Parameters ---------- - target_session : str - name of the session. fnmatch(3) works. + target_session : str, optional + The name or pattern of the target session. If None, attaches to + the most recently used session. + + Examples + -------- + >>> # Create a test session + >>> if server.has_session("test_attach"): + ... server.kill_session("test_attach") + >>> session = server.new_session(session_name="test_attach") + >>> # Note: attach_session() requires an interactive terminal + >>> # so we can't demonstrate it in doctests + >>> # Clean up + >>> server.kill_session("test_attach") + Server(socket_name=libtmux_test...) Raises ------ - :exc:`exc.BadSessionName` + exc.BadSessionName + If the session name is invalid. + exc.LibTmuxException + If tmux reports an error (stderr output). """ session_check_name(target_session) proc = self.cmd("attach-session", target=target_session) - if proc.stderr: raise exc.LibTmuxException(proc.stderr) @@ -440,75 +540,62 @@ def new_session( *args: t.Any, **kwargs: t.Any, ) -> Session: - """Create new session, returns new :class:`Session`. - - Uses ``-P`` flag to print session info, ``-F`` for return formatting - returns new Session object. - - ``$ tmux new-session -d`` will create the session in the background - ``$ tmux new-session -Ad`` will move to the session name if it already - exists. todo: make an option to handle this. + """Create a new session. Parameters ---------- session_name : str, optional - :: - - $ tmux new-session -s - attach : bool, optional - create session in the foreground. ``attach=False`` is equivalent - to:: - - $ tmux new-session -d - - Other Parameters - ---------------- + Name of the session kill_session : bool, optional - Kill current session if ``$ tmux has-session``. - Useful for testing workspaces. + Kill session if it exists + attach : bool, optional + Attach to session after creating it start_directory : str, optional - specifies the working directory in which the - new session is created. + Working directory for the session window_name : str, optional - :: - - $ tmux new-session -n + Name of the initial window window_command : str, optional - execute a command on starting the session. The window will close - when the command exits. NOTE: When this command exits the window - will close. This feature is useful for long-running processes - where the closing of the window upon completion is desired. - x : [int, str], optional - Force the specified width instead of the tmux default for a - detached session - y : [int, str], optional - Force the specified height instead of the tmux default for a - detached session - - Returns - ------- - :class:`Session` - - Raises - ------ - :exc:`exc.BadSessionName` + Command to run in the initial window + x : int or "-", optional + Width of new window + y : int or "-", optional + Height of new window + environment : dict, optional + Dictionary of environment variables to set Examples -------- - Sessions can be created without a session name (0.14.2+): - - >>> server.new_session() - Session($2 2) - - Creating them in succession will enumerate IDs (via tmux): - - >>> server.new_session() - Session($3 3) - - With a `session_name`: - - >>> server.new_session(session_name='my session') - Session($4 my session) + >>> # Clean up any existing sessions first + >>> for name in ["basic", "custom", "env_test"]: + ... if server.has_session(name): + ... server.kill_session(name) + >>> # Create a basic session + >>> session1 = server.new_session(session_name="basic") + >>> session1.name + 'basic' + + >>> # Create session with custom window name + >>> session2 = server.new_session( + ... session_name="custom", + ... window_name="editor" + ... ) + >>> session2.windows[0].name + 'editor' + + >>> # Create session with environment variables + >>> session3 = server.new_session( + ... session_name="env_test", + ... environment={"TEST_VAR": "test_value"} + ... ) + >>> session3.name + 'env_test' + + >>> # Clean up + >>> for name in ["basic", "custom", "env_test"]: + ... server.kill_session(name) + Server(socket_name=libtmux_test...) + Server(socket_name=libtmux_test...) + Server(socket_name=libtmux_test...) """ if session_name is not None: session_check_name(session_name) @@ -518,101 +605,127 @@ def new_session( self.cmd("kill-session", target=session_name) logger.info(f"session {session_name} exists. killed it.") else: - msg = f"Session named {session_name} exists" - raise exc.TmuxSessionExists( - msg, - ) + msg = f"Session named {session_name} exists." + raise exc.TmuxSessionExists(msg) logger.debug(f"creating session {session_name}") - env = os.environ.get("TMUX") - if env: del os.environ["TMUX"] - tmux_args: tuple[str | int, ...] = ( - "-P", - "-F#{session_id}", # output - ) - + tmux_args: list[str | int] = ["-P", "-F#{session_id}"] if session_name is not None: - tmux_args += (f"-s{session_name}",) - + tmux_args.append(f"-s{session_name}") if not attach: - tmux_args += ("-d",) - + tmux_args.append("-d") if start_directory: - tmux_args += ("-c", start_directory) - + tmux_args += ["-c", start_directory] if window_name: - tmux_args += ("-n", window_name) - + tmux_args += ["-n", window_name] if x is not None: - tmux_args += ("-x", x) - + tmux_args += ["-x", x] if y is not None: - tmux_args += ("-y", y) - + tmux_args += ["-y", y] if environment: if has_gte_version("3.2"): for k, v in environment.items(): - tmux_args += (f"-e{k}={v}",) + tmux_args.append(f"-e{k}={v}") else: logger.warning( "Environment flag ignored, tmux 3.2 or newer required.", ) - if window_command: - tmux_args += (window_command,) + tmux_args.append(window_command) proc = self.cmd("new-session", *tmux_args) - if proc.stderr: raise exc.LibTmuxException(proc.stderr) session_stdout = proc.stdout[0] - if env: os.environ["TMUX"] = env session_formatters = dict( zip(["session_id"], session_stdout.split(formats.FORMAT_SEPARATOR)), ) - return Session.from_session_id( server=self, session_id=session_formatters["session_id"], ) - # - # Relations - # @property def sessions(self) -> QueryList[Session]: - """Sessions contained in server. + """Return list of sessions. - Can be accessed via - :meth:`.sessions.get() ` and - :meth:`.sessions.filter() ` + Examples + -------- + >>> # Clean up any existing test sessions first + >>> for name in ["test1", "test2"]: + ... if server.has_session(name): + ... server.kill_session(name) + >>> # Create some test sessions + >>> session1 = server.new_session(session_name="test1") + >>> session2 = server.new_session(session_name="test2") + >>> len(server.sessions) >= 2 # May have other sessions + True + >>> sorted([s.name for s in server.sessions if s.name in ["test1", "test2"]]) + ['test1', 'test2'] + >>> # Clean up + >>> server.kill_session("test1") + Server(socket_name=libtmux_test...) + >>> server.kill_session("test2") + Server(socket_name=libtmux_test...) """ sessions: list[Session] = [] - - try: - for obj in fetch_objs( - list_cmd="list-sessions", - server=self, - ): - sessions.append(Session(server=self, **obj)) # noqa: PERF401 - except Exception: - pass - + with contextlib.suppress(Exception): + sessions.extend( + Session(server=self, **obj) + for obj in fetch_objs( + list_cmd="list-sessions", + server=self, + ) + ) return QueryList(sessions) @property def windows(self) -> QueryList[Window]: - """Windows contained in server's sessions. + """Return a :class:`QueryList` of all :class:`Window` objects in this server. - Can be accessed via + This includes windows in all sessions. + + Examples + -------- + >>> # Clean up any existing test sessions + >>> for name in ["test_windows1", "test_windows2"]: + ... if server.has_session(name): + ... server.kill_session(name) + >>> # Create sessions with windows + >>> session1 = server.new_session(session_name="test_windows1") + >>> session2 = server.new_session(session_name="test_windows2") + >>> # Create additional windows + >>> _ = session1.new_window(window_name="win1") # Create window + >>> _ = session2.new_window(window_name="win2") # Create window + >>> # Each session should have 2 windows (default + new) + >>> len([w for w in server.windows if w.session.name == "test_windows1"]) + 2 + >>> len([w for w in server.windows if w.session.name == "test_windows2"]) + 2 + >>> # Verify window names + >>> wins1 = [w for w in server.windows if w.session.name == "test_windows1"] + >>> wins2 = [w for w in server.windows if w.session.name == "test_windows2"] + >>> # Default window name can vary (bash, zsh), but win1 should be there + >>> "win1" in [w.name for w in wins1] + True + >>> # Default window name can vary, but win2 should be there + >>> "win2" in [w.name for w in wins2] + True + >>> # Clean up + >>> server.kill_session("test_windows1") + Server(socket_name=libtmux_test...) + >>> server.kill_session("test_windows2") + Server(socket_name=libtmux_test...) + + Access advanced filtering and retrieval with: :meth:`.windows.get() ` and :meth:`.windows.filter() ` """ @@ -624,14 +737,33 @@ def windows(self) -> QueryList[Window]: server=self, ) ] - return QueryList(windows) @property def panes(self) -> QueryList[Pane]: - """Panes contained in tmux server (across all windows in all sessions). + """Return a :class:`QueryList` of all :class:`Pane` objects in this server. + + This includes panes from all windows in all sessions. - Can be accessed via + Examples + -------- + >>> # Clean up any existing test session + >>> if server.has_session("test_panes"): + ... server.kill_session("test_panes") + >>> # Create a session and split some panes + >>> session = server.new_session(session_name="test_panes") + >>> window = session.attached_window + >>> # Split into two panes + >>> window.split_window() + Pane(%... Window(@... 1:..., Session($... test_panes))) + >>> # Each window starts with 1 pane, split creates another + >>> len([p for p in server.panes if p.window.session.name == "test_panes"]) + 2 + >>> # Clean up + >>> server.kill_session("test_panes") + Server(socket_name=libtmux_test...) + + Access advanced filtering and retrieval with: :meth:`.panes.get() ` and :meth:`.panes.filter() ` """ @@ -643,14 +775,10 @@ def panes(self) -> QueryList[Pane]: server=self, ) ] - return QueryList(panes) - # - # Dunder - # def __eq__(self, other: object) -> bool: - """Equal operator for :class:`Server` object.""" + """Compare two servers by their socket name/path.""" if isinstance(other, Server): return ( self.socket_name == other.socket_name @@ -659,7 +787,7 @@ def __eq__(self, other: object) -> bool: return False def __repr__(self) -> str: - """Representation of :class:`Server` object.""" + """Return a string representation of this :class:`Server`.""" if self.socket_name is not None: return ( f"{self.__class__.__name__}" @@ -671,18 +799,13 @@ def __repr__(self) -> str: f"{self.__class__.__name__}(socket_path=/tmp/tmux-{os.geteuid()}/default)" ) - # - # Legacy: Redundant stuff we want to remove - # + # Deprecated / Legacy Methods + def kill_server(self) -> None: - """Kill tmux server. + """Kill the tmux server (deprecated). - Notes - ----- .. deprecated:: 0.30 - - Deprecated in favor of :meth:`.kill()`. - + Use :meth:`.kill()`. """ warnings.warn( "Server.kill_server() is deprecated in favor of Server.kill()", @@ -692,17 +815,10 @@ def kill_server(self) -> None: self.cmd("kill-server") def _list_panes(self) -> list[PaneDict]: - """Return list of panes in :py:obj:`dict` form. - - Retrieved from ``$ tmux(1) list-panes`` stdout. - - The :py:obj:`list` is derived from ``stdout`` in - :class:`util.tmux_cmd` which wraps :py:class:`subprocess.Popen`. + """Return a list of all panes in dict form (deprecated). .. deprecated:: 0.16 - - Deprecated in favor of :attr:`.panes`. - + Use :attr:`.panes`. """ warnings.warn( "Server._list_panes() is deprecated", @@ -712,15 +828,10 @@ def _list_panes(self) -> list[PaneDict]: return [p.__dict__ for p in self.panes] def _update_panes(self) -> Server: - """Update internal pane data and return ``self`` for chainability. + """Update internal pane data (deprecated). .. deprecated:: 0.16 - - Deprecated in favor of :attr:`.panes` and returning ``self``. - - Returns - ------- - :class:`Server` + Use :attr:`.panes` instead. """ warnings.warn( "Server._update_panes() is deprecated", @@ -731,12 +842,10 @@ def _update_panes(self) -> Server: return self def get_by_id(self, session_id: str) -> Session | None: - """Return session by id. Deprecated in favor of :meth:`.sessions.get()`. + """Return a session by its ID (deprecated). .. deprecated:: 0.16 - - Deprecated by :meth:`.sessions.get()`. - + Use :meth:`.sessions.get()`. """ warnings.warn( "Server.get_by_id() is deprecated", @@ -746,12 +855,10 @@ def get_by_id(self, session_id: str) -> Session | None: return self.sessions.get(session_id=session_id, default=None) def where(self, kwargs: dict[str, t.Any]) -> list[Session]: - """Filter through sessions, return list of :class:`Session`. + """Filter sessions (deprecated). .. deprecated:: 0.16 - - Deprecated by :meth:`.session.filter()`. - + Use :meth:`.sessions.filter()`. """ warnings.warn( "Server.find_where() is deprecated", @@ -764,12 +871,10 @@ def where(self, kwargs: dict[str, t.Any]) -> list[Session]: return [] def find_where(self, kwargs: dict[str, t.Any]) -> Session | None: - """Filter through sessions, return first :class:`Session`. + """Return the first matching session (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :meth:`.sessions.get()`. - + Use :meth:`.sessions.get()`. """ warnings.warn( "Server.find_where() is deprecated", @@ -779,17 +884,10 @@ def find_where(self, kwargs: dict[str, t.Any]) -> Session | None: return self.sessions.get(default=None, **kwargs) def _list_windows(self) -> list[WindowDict]: - """Return list of windows in :py:obj:`dict` form. - - Retrieved from ``$ tmux(1) list-windows`` stdout. - - The :py:obj:`list` is derived from ``stdout`` in - :class:`common.tmux_cmd` which wraps :py:class:`subprocess.Popen`. + """Return a list of all windows in dict form (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :attr:`.windows`. - + Use :attr:`.windows`. """ warnings.warn( "Server._list_windows() is deprecated", @@ -799,12 +897,10 @@ def _list_windows(self) -> list[WindowDict]: return [w.__dict__ for w in self.windows] def _update_windows(self) -> Server: - """Update internal window data and return ``self`` for chainability. + """Update internal window data (deprecated). .. deprecated:: 0.16 - - Deprecated in favor of :attr:`.windows` and returning ``self``. - + Use :attr:`.windows`. """ warnings.warn( "Server._update_windows() is deprecated", @@ -816,12 +912,10 @@ def _update_windows(self) -> Server: @property def _sessions(self) -> list[SessionDict]: - """Property / alias to return :meth:`~._list_sessions`. + """Return session objects in dict form (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :attr:`.sessions`. - + Use :attr:`.sessions`. """ warnings.warn( "Server._sessions is deprecated", @@ -831,11 +925,10 @@ def _sessions(self) -> list[SessionDict]: return self._list_sessions() def _list_sessions(self) -> list[SessionDict]: - """Return list of session object dictionaries. + """Return a list of all sessions in dict form (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :attr:`.sessions`. + Use :attr:`.sessions`. """ warnings.warn( "Server._list_sessions() is deprecated", @@ -845,15 +938,10 @@ def _list_sessions(self) -> list[SessionDict]: return [s.__dict__ for s in self.sessions] def list_sessions(self) -> list[Session]: - """Return list of :class:`Session` from the ``tmux(1)`` session. + """Return a list of all :class:`Session` objects (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :attr:`.sessions`. - - Returns - ------- - list of :class:`Session` + Use :attr:`.sessions`. """ warnings.warn( "Server.list_sessions is deprecated", @@ -864,12 +952,10 @@ def list_sessions(self) -> list[Session]: @property def children(self) -> QueryList[Session]: - """Was used by TmuxRelationalObject (but that's longer used in this class). + """Return child sessions (deprecated). .. deprecated:: 0.16 - - Slated to be removed in favor of :attr:`.sessions`. - + Use :attr:`.sessions`. """ warnings.warn( "Server.children is deprecated", diff --git a/src/libtmux/session.py b/src/libtmux/session.py index d1433e014..2e3e8c861 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -1,8 +1,12 @@ -"""Pythonization of the :term:`tmux(1)` session. +"""Provide a Pythonic representation of the :term:`tmux(1)` session. + +This module implements the :class:`Session` class, representing a tmux session +capable of containing multiple windows and panes. It includes methods for +attaching, killing, renaming, or modifying the session, as well as property +accessors for session attributes. libtmux.session ~~~~~~~~~~~~~~~ - """ from __future__ import annotations @@ -49,13 +53,14 @@ @dataclasses.dataclass() class Session(Obj, EnvironmentMixin): - """:term:`tmux(1)` :term:`Session` [session_manual]_. + """Represent a :term:`tmux(1)` session [session_manual]_. Holds :class:`Window` objects. Parameters ---------- - server : :class:`Server` + server + The :class:`Server` instance that owns this session. Examples -------- @@ -85,8 +90,8 @@ class Session(Obj, EnvironmentMixin): and displays it on screen..." "A session is a single collection of pseudo terminals under the - management of tmux. Each session has one or more windows linked to - it." + management of tmux. Each session has one or more windows linked + to it." https://man.openbsd.org/tmux.1#DESCRIPTION. Accessed April 1st, 2018. """ @@ -134,7 +139,7 @@ def refresh(self) -> None: @classmethod def from_session_id(cls, server: Server, session_id: str) -> Session: - """Create Session from existing session_id.""" + """Create a :class:`Session` from an existing session_id.""" session = fetch_obj( obj_key="session_id", obj_id=session_id, @@ -143,12 +148,9 @@ def from_session_id(cls, server: Server, session_id: str) -> Session: ) return cls(server=server, **session) - # - # Relations - # @property def windows(self) -> QueryList[Window]: - """Windows contained by session. + """Return a :class:`QueryList` of :class:`Window` objects in this session. Can be accessed via :meth:`.windows.get() ` and @@ -163,12 +165,11 @@ def windows(self) -> QueryList[Window]: ) if obj.get("session_id") == self.session_id ] - return QueryList(windows) @property def panes(self) -> QueryList[Pane]: - """Panes contained by session's windows. + """Return a :class:`QueryList` of :class:`Pane` for all windows of this session. Can be accessed via :meth:`.panes.get() ` and @@ -183,111 +184,97 @@ def panes(self) -> QueryList[Pane]: ) if obj.get("session_id") == self.session_id ] - return QueryList(panes) - # - # Command - # def cmd( self, cmd: str, *args: t.Any, target: str | int | None = None, ) -> tmux_cmd: - """Execute tmux subcommand within session context. + """Execute a tmux subcommand within the context of this session. + + Automatically binds ``-t `` to the command unless + overridden by the `target` parameter. - Automatically binds target by adding ``-t`` for object's session ID to the - command. Pass ``target`` to keyword arguments to override. + Parameters + ---------- + cmd + The tmux subcommand to execute. + *args + Additional arguments for the tmux command. + target, optional + Custom target override. By default, the target is this session's ID. Examples -------- >>> session.cmd('new-window', '-P').stdout[0] 'libtmux...:....0' - From raw output to an enriched `Window` object: + From raw output to a `Window` object: - >>> Window.from_window_id(window_id=session.cmd( - ... 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server) + >>> Window.from_window_id( + ... window_id=session.cmd('new-window', '-P', '-F#{window_id}').stdout[0], + ... server=session.server + ... ) Window(@... ...:..., Session($1 libtmux_...)) - Parameters - ---------- - target : str, optional - Optional custom target override. By default, the target is the session ID. - Returns ------- - :meth:`server.cmd` + tmux_cmd + The result of the tmux command execution. Notes ----- .. versionchanged:: 0.34 - - Passing target by ``-t`` is ignored. Use ``target`` keyword argument instead. + Passing target by ``-t`` is ignored. Use the ``target`` parameter instead. .. versionchanged:: 0.8 - - Renamed from ``.tmux`` to ``.cmd``. + Renamed from ``.tmux`` to ``.cmd``. """ if target is None: target = self.session_id return self.server.cmd(cmd, *args, target=target) - """ - Commands (tmux-like) - """ - def set_option( self, option: str, value: str | int, global_: bool = False, ) -> Session: - """Set option ``$ tmux set-option