Skip to content
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 @@ -73,6 +73,20 @@ the format object now carry the tmux manual's description for every field.
They previously reached the rendered API reference as "Alias for field
number 0" or as a bare name carrying only its type.

#### Lint floor moved to ruff 0.16 (#722)

Minimum `ruff>=0.16.0` (was unpinned). 0.16.0 stabilizes
`none-not-at-end-of-union` (`RUF036`) out of preview, and starts formatting
Python code blocks inside Markdown, so `ruff format` now covers the docs tree
alongside `src/` and `tests/`.

Linting runs on ruff's curated default rule set, with this project's own
linters layered on top through `extend-select`. Pylint, flake8-pyi,
flake8-blind-except, and flake8-bandit checks now apply; the tmux output
parsers, {meth}`Server.is_alive() <libtmux.Server.is_alive>`, and the Sphinx
config carry scoped per-file ignores where catching everything is the intended
contract.

## libtmux 0.62.0 (2026-07-12)

libtmux 0.62.0 teaches libtmux objects to locate themselves and to resolve
Expand Down
36 changes: 32 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dev = [
"coverage",
"pytest-cov",
# Lint
"ruff",
"ruff>=0.16.0",
"mypy",
]

Expand All @@ -95,7 +95,7 @@ coverage =[
]
lint = [
"typing-extensions; python_version < '3.11'",
"ruff",
"ruff>=0.16.0",
"mypy",
]

Expand Down Expand Up @@ -179,7 +179,10 @@ exclude_lines = [
target-version = "py310"

[tool.ruff.lint]
select = [
# `select` is deliberately unset: ruff 0.16 enables a curated default rule
# set, and an explicit `select` would replace it rather than extend it.
# `extend-select` layers this project's additional linters on top.
extend-select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
Expand All @@ -196,7 +199,7 @@ select = [
"PERF", # Perflint
"RUF", # Ruff-specific rules
"D", # pydocstyle
"FA100", # future annotations
"FA100", # future annotations
]
ignore = [
"COM812", # missing trailing comma, ruff format conflict
Expand Down Expand Up @@ -228,6 +231,31 @@ convention = "numpy"

[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = ["F401"]
"docs/conf.py" = [
# Sphinx reads version metadata by exec'ing the package's `__about__.py`,
# which keeps the docs build off an import of the package being documented.
"S102",
]
"src/libtmux/_internal/query_list.py" = [
# `X as X` is PEP 484's explicit re-export form, and mypy runs strict here,
# so dropping the alias stops mypy seeing the exception types as exported.
"PLC0414",
# The lookup operators are total predicates over caller-supplied data of any
# type: a comparison that blows up means "no match", not an error to raise.
"BLE001",
]
"src/libtmux/options.py" = [
# The option parsers accept whatever shape tmux prints, across every tmux
# version. An unparseable entry is logged and kept as its raw value so one
# unfamiliar option cannot take down every other option on the object.
"BLE001",
]
"src/libtmux/server.py" = [
# `Server.is_alive` answers a yes/no question about an unreachable server.
# Every way of failing to reach it is a "no"; callers who need the reason
# use `Server.raise_if_dead`.
"BLE001",
]

[tool.pytest.ini_options]
addopts = [
Expand Down
4 changes: 3 additions & 1 deletion src/libtmux/_internal/control_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
if t.TYPE_CHECKING:
import types

from typing_extensions import Self

from libtmux.server import Server
from libtmux.session import Session

Expand Down Expand Up @@ -56,7 +58,7 @@ def __init__(self, server: Server, session: Session) -> None:
self.server = server
self.session = session

def __enter__(self) -> ControlMode:
def __enter__(self) -> Self:
"""Spawn control-mode client and wait for registration."""
read_fd, self._write_fd = os.pipe()

Expand Down
6 changes: 3 additions & 3 deletions src/libtmux/_internal/query_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __call__(
def keygetter(
obj: Mapping[str, t.Any],
path: str,
) -> None | t.Any | str | list[str] | Mapping[str, str]:
) -> t.Any | str | list[str] | Mapping[str, str] | None:
"""Fetch values in objects and keys, supported nested data.

**With dictionaries**:
Expand Down Expand Up @@ -316,12 +316,12 @@ def lookup_iregex(

class PKRequiredException(Exception):
def __init__(self, *args: object) -> None:
return super().__init__("items() require a pk_key exists")
super().__init__("items() require a pk_key exists")


class OpNotFound(ValueError):
def __init__(self, op: str, *args: object) -> None:
return super().__init__(f"{op} not in LOOKUP_NAME_MAP")
super().__init__(f"{op} not in LOOKUP_NAME_MAP")


class QueryList(list[T], t.Generic[T]):
Expand Down
2 changes: 1 addition & 1 deletion src/libtmux/_vendor/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class InvalidVersion(ValueError):
"""

def __init__(self, version: str, *args: object) -> None:
return super().__init__(f"Invalid version: '{version}'")
super().__init__(f"Invalid version: '{version}'")


class _BaseVersion:
Expand Down
27 changes: 15 additions & 12 deletions src/libtmux/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ def __init__(
reason: str = "unset or empty",
) -> None:
if variable is None:
return super().__init__("Not inside a tmux pane", *args)
return super().__init__(
super().__init__("Not inside a tmux pane", *args)
return
super().__init__(
f"Not inside a tmux pane: ${variable} is {reason}",
*args,
)
Expand Down Expand Up @@ -295,11 +296,12 @@ 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")
return
super().__init__("Could not find object")


class VersionTooLow(LibTmuxException):
Expand All @@ -318,7 +320,7 @@ 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):
Expand All @@ -333,7 +335,7 @@ class UnknownColorOption(UnknownOption):
"""Unknown color option."""

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):
Expand All @@ -352,7 +354,7 @@ class VariableUnpackingError(LibTmuxException):
"""Error unpacking variable."""

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):
Expand All @@ -364,8 +366,9 @@ class PaneNotFound(PaneError):

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}")
return
super().__init__("Pane not found")


class WindowError(LibTmuxException):
Expand All @@ -376,21 +379,21 @@ class MultipleActiveWindows(WindowError):
"""Multiple active windows."""

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."""

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."""

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):
Expand Down
4 changes: 2 additions & 2 deletions src/libtmux/test/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
#: Number of seconds to wait before timing out when retrying operations
#: Can be configured via :envvar:`RETRY_TIMEOUT_SECONDS` environment variable
#: Defaults to 8 seconds
RETRY_TIMEOUT_SECONDS = int(os.getenv("RETRY_TIMEOUT_SECONDS", 8))
RETRY_TIMEOUT_SECONDS = int(os.getenv("RETRY_TIMEOUT_SECONDS", "8"))

#: Interval in seconds between retry attempts
#: Can be configured via :envvar:`RETRY_INTERVAL_SECONDS` environment variable
#: Defaults to 0.05 seconds (50ms)
RETRY_INTERVAL_SECONDS = float(os.getenv("RETRY_INTERVAL_SECONDS", 0.05))
RETRY_INTERVAL_SECONDS = float(os.getenv("RETRY_INTERVAL_SECONDS", "0.05"))
2 changes: 1 addition & 1 deletion tests/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_pane(

try:
session_ = server.sessions[0]
except Exception:
except IndexError:
session_ = server.new_session()

assert session_ is not None
Expand Down
4 changes: 2 additions & 2 deletions tests/test_from_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ class SubclassFixture(t.NamedTuple):
"""A ``from_env`` constructor, and the class it is called on."""

test_id: str
subclass: type[Server] | type[Session] | type[Window] | type[Pane]
subclass: type[Server | Session | Window | Pane]


class MyServer(Server):
Expand Down Expand Up @@ -500,7 +500,7 @@ class MyPane(Pane):
def test_from_env_returns_the_class_it_was_called_on(
session: Session,
test_id: str,
subclass: type[Server] | type[Session] | type[Window] | type[Pane],
subclass: type[Server | Session | Window | Pane],
) -> None:
"""``from_env`` honours ``cls``, so subclasses get their own type back.

Expand Down
36 changes: 18 additions & 18 deletions tests/test_tmuxobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@
def test_find_where(server: Server, session: Session) -> None:
"""Test that find_where() retrieves single matching object."""
# server.find_where
for session in server.sessions:
session_id = session.session_id
for session_ in server.sessions:
session_id = session_.session_id
assert session_id is not None

assert server.sessions.get(session_id=session_id) == session
assert server.sessions.get(session_id=session_id) == session_
assert isinstance(server.sessions.filter(session_id=session_id)[0], Session)

# session.find_where
for window in session.windows:
for window in session_.windows:
window_id = window.window_id
assert window_id is not None

assert session.windows.get(window_id=window_id) == window
assert isinstance(session.windows.get(window_id=window_id), Window)
assert session_.windows.get(window_id=window_id) == window
assert isinstance(session_.windows.get(window_id=window_id), Window)

# window.find_where
for pane in window.panes:
Expand All @@ -60,28 +60,28 @@ def test_find_where_None(server: Server, session: Session) -> None:

def test_find_where_multiple_infos(server: Server, session: Session) -> None:
""".find_where returns objects with multiple attributes."""
for session in server.sessions:
session_id = session.session_id
for session_ in server.sessions:
session_id = session_.session_id
assert session_id is not None
session_name = session.session_name
session_name = session_.session_name
assert session_name is not None

find_where = server.sessions.get(
session_id=session_id,
session_name=session_name,
)

assert find_where == session
assert find_where == session_
assert isinstance(find_where, Session)

# session.find_where
for window in session.windows:
for window in session_.windows:
window_id = window.window_id
assert window_id is not None
window_index = window.window_index
assert window_index is not None

find_window_where = session.windows.get(
find_window_where = session_.windows.get(
window_id=window_id,
window_index=window_index,
)
Expand All @@ -107,10 +107,10 @@ def test_where(server: Server, session: Session) -> None:
window = session.active_window
window.split() # create second pane

for session in server.sessions:
session_id = session.session_id
for session_ in server.sessions:
session_id = session_.session_id
assert session_id is not None
session_name = session.session_name
session_name = session_.session_name
assert session_name is not None

server_sessions = server.sessions.filter(
Expand All @@ -120,18 +120,18 @@ def test_where(server: Server, session: Session) -> None:

assert len(server_sessions) == 1
assert isinstance(server_sessions, list)
assert server_sessions[0] == session
assert server_sessions[0] == session_
assert isinstance(server_sessions[0], Session)

# session.where
for window in session.windows:
for window in session_.windows:
window_id = window.window_id
assert window_id is not None

window_index = window.window_index
assert window_index is not None

session_windows = session.windows.filter(
session_windows = session_.windows.filter(
window_id=window_id,
window_index=window_index,
)
Expand Down
Loading
Loading