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
6 changes: 0 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ jobs:
matrix:
python-version: ['3.14']
tmux-version: ['2.6', '2.7', '2.8', '3.0a', '3.1b', '3.2a', '3.3a', '3.4', '3.5', 'master']
# balance ci coverage across supported python/tmux versions with CI speed
include:
- python-version: '3.9'
tmux-version: '2.6'
- python-version: '3.9'
tmux-version: 'master'
steps:
- uses: actions/checkout@v4

Expand Down
8 changes: 8 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force

- _Future release notes will be placed here_

### Breaking changes

- Drop Python 3.9, EOL October 5th, 2025 (#987)

tmuxp 1.55.0 was the last release for Python 3.9.

The minimum Python for tmuxp as of 1.56.0 is Python 3.10

### Development

- Add Python 3.14 to test matrix (#986)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ See donation options at <https://tony.sh/support.html>.
# Project details

- tmux support: 1.8+
- python support: >= 3.9, pypy, pypy3
- python support: >= 3.10, pypy, pypy3
- Source: <https://github.com/tmux-python/tmuxp>
- Docs: <https://tmuxp.git-pull.com>
- API: <https://tmuxp.git-pull.com/api.html>
Expand Down
26 changes: 12 additions & 14 deletions docs/_ext/aafig.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import locale
import logging
import pathlib
import posixpath
import typing as t
from hashlib import sha1 as sha
Expand Down Expand Up @@ -130,9 +131,11 @@ def render_aafig_images(app: Sphinx, doctree: nodes.Node) -> None:
options["format"] = format_map[format_]
else:
logger.warning(
f'unsupported builder format "{format_}", please '
"add a custom entry in aafig_format config "
"option for this builder",
(
'unsupported builder format "%s", please add a custom entry in '
"aafig_format config option for this builder"
),
format_,
)
img.replace_self(nodes.literal_block(text, text))
continue
Expand Down Expand Up @@ -196,11 +199,9 @@ def render_aafigure(
f = None
try:
try:
with open(
metadata_fname,
encoding=locale.getpreferredencoding(False),
) as f:
extra = f.read()
extra = pathlib.Path(metadata_fname).read_text(
encoding=locale.getpreferredencoding(False)
)
except Exception as e:
raise AafigError from e
finally:
Expand All @@ -221,12 +222,9 @@ def render_aafigure(
extra = None
if options["format"].lower() == "svg":
extra = visitor.get_size_attrs()
with open(
metadata_fname,
"w",
encoding=locale.getpreferredencoding(False),
) as f:
f.write(extra)
pathlib.Path(metadata_fname).write_text(
extra, encoding=locale.getpreferredencoding(False)
)

return relfn, outfn, None, extra

Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "tmuxp"
version = "1.55.0"
description = "Session manager for tmux, which allows users to save and load tmux sessions through simple configuration files."
requires-python = ">=3.9,<4.0"
requires-python = ">=3.10,<4.0"
authors = [
{name = "Tony Narlock", email = "[email protected]"}
]
Expand All @@ -15,7 +15,6 @@ classifiers = [
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -156,7 +155,7 @@ exclude_lines = [

[tool.mypy]
strict = true
python_version = "3.9"
python_version = "3.10"
files = [
"src/",
"tests/",
Expand All @@ -175,7 +174,7 @@ module = [
ignore_missing_imports = true

[tool.ruff]
target-version = "py39"
target-version = "py310"

[tool.ruff.lint]
select = [
Expand Down
4 changes: 2 additions & 2 deletions src/tmuxp/_internal/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import yaml

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias

FormatLiteral = t.Literal["json", "yaml"]

Expand Down Expand Up @@ -106,7 +106,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
{'session_name': 'my session'}
"""
assert isinstance(path, pathlib.Path)
content = path.open().read()
content = path.open(encoding="utf-8").read()

if path.suffix in {".yaml", ".yml"}:
fmt: FormatLiteral = "yaml"
Expand Down
3 changes: 1 addition & 2 deletions src/tmuxp/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@

if t.TYPE_CHECKING:
import pathlib

from typing_extensions import TypeAlias
from typing import TypeAlias

CLIVerbosity: TypeAlias = t.Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
CLISubparserName: TypeAlias = t.Literal[
Expand Down
5 changes: 3 additions & 2 deletions src/tmuxp/cli/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def command_convert(
answer_yes = True

if answer_yes:
with open(newfile, "w", encoding=locale.getpreferredencoding(False)) as buf:
buf.write(new_workspace)
pathlib.Path(newfile).write_text(
new_workspace, encoding=locale.getpreferredencoding(False)
)
print(f"New workspace file saved to <{newfile}>.") # NOQA: T201 RUF100
7 changes: 4 additions & 3 deletions src/tmuxp/cli/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .utils import prompt, prompt_choices, prompt_yes_no

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeGuard
from typing import TypeAlias, TypeGuard

CLIOutputFormatLiteral: TypeAlias = t.Literal["yaml", "json"]

Expand Down Expand Up @@ -210,8 +210,9 @@ def extract_workspace_format(
destdir = os.path.dirname(dest)
if not os.path.isdir(destdir):
os.makedirs(destdir)
with open(dest, "w", encoding=locale.getpreferredencoding(False)) as buf:
buf.write(workspace)
pathlib.Path(dest).write_text(
workspace, encoding=locale.getpreferredencoding(False)
)

if not args.quiet:
print(f"Saved to {dest}.") # NOQA: T201 RUF100
5 changes: 3 additions & 2 deletions src/tmuxp/cli/import_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ def import_config(
if prompt_yes_no(f"Save to {dest_path}?"):
dest = dest_path

with open(dest, "w", encoding=locale.getpreferredencoding(False)) as buf:
buf.write(new_config)
pathlib.Path(dest).write_text(
new_config, encoding=locale.getpreferredencoding(False)
)

tmuxp_echo(f"Saved to {dest}.")
else:
Expand Down
4 changes: 3 additions & 1 deletion src/tmuxp/cli/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
from .utils import prompt_choices, prompt_yes_no, style, tmuxp_echo

if t.TYPE_CHECKING:
from typing import TypeAlias

from libtmux.session import Session
from typing_extensions import NotRequired, TypeAlias, TypedDict
from typing_extensions import NotRequired, TypedDict

from tmuxp.types import StrPath

Expand Down
2 changes: 1 addition & 1 deletion src/tmuxp/cli/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tmuxp._compat import PY3, PYMINOR

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias

CLIColorsLiteral: TypeAlias = t.Literal[56, 88]
CLIShellLiteral: TypeAlias = t.Literal[
Expand Down
5 changes: 2 additions & 3 deletions src/tmuxp/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@

if t.TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import TypeAlias

from typing_extensions import TypeAlias

CLIColour: TypeAlias = t.Union[int, tuple[int, int, int], str]
CLIColour: TypeAlias = int | tuple[int, int, int] | str


logger = logging.getLogger(__name__)
Expand Down
4 changes: 3 additions & 1 deletion src/tmuxp/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@


if t.TYPE_CHECKING:
from typing import TypeGuard

from libtmux.session import Session
from libtmux.window import Window
from typing_extensions import TypedDict, TypeGuard, Unpack
from typing_extensions import TypedDict, Unpack

from ._internal.types import PluginConfigSchema

Expand Down
3 changes: 2 additions & 1 deletion src/tmuxp/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
if t.TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
from typing import TypeAlias

from libtmux.pane import Pane
from libtmux.server import Server
from libtmux.session import Session
from libtmux.window import Window
from typing_extensions import NotRequired, TypeAlias, TypedDict, Unpack
from typing_extensions import NotRequired, TypedDict, Unpack

CLIShellLiteral: TypeAlias = t.Literal[
"best",
Expand Down
7 changes: 5 additions & 2 deletions src/tmuxp/workspace/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,11 @@ def iter_create_windows(
else:
target = "windows"
logger.warning(
f"Cannot set environment for new {target}. "
"You need tmux 3.0 or newer for this.",
(
"Cannot set environment for new %s. "
"You need tmux 3.0 or newer for this."
),
target,
)
environment = None

Expand Down
3 changes: 1 addition & 2 deletions src/tmuxp/workspace/finders.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@

if t.TYPE_CHECKING:
import pathlib

from typing_extensions import TypeAlias
from typing import TypeAlias

from tmuxp.types import StrPath

Expand Down
7 changes: 4 additions & 3 deletions tests/cli/test_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,11 @@ def test_load_symlinked_workspace(


if t.TYPE_CHECKING:
from typing import TypeAlias

from pytest_mock import MockerFixture
from typing_extensions import TypeAlias

ExpectedOutput: TypeAlias = t.Optional[t.Union[str, list[str]]]
ExpectedOutput: TypeAlias = str | list[str] | None


class CLILoadFixture(t.NamedTuple):
Expand Down Expand Up @@ -309,7 +310,7 @@ def test_load(
assert server.socket_name is not None

monkeypatch.chdir(tmp_path)
for session_name, config_path in zip(session_names, config_paths):
for session_name, config_path in zip(session_names, config_paths, strict=False):
tmuxp_config = pathlib.Path(
config_path.format(tmp_path=tmp_path, TMUXP_CONFIGDIR=tmuxp_configdir),
)
Expand Down
13 changes: 12 additions & 1 deletion tests/fixtures/pluginsystem/partials/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@

from __future__ import annotations

from typing_extensions import NotRequired, TypedDict
import typing as t

if t.TYPE_CHECKING:
from typing_extensions import NotRequired, TypedDict
else:
# Fallback for runtime, though this module should not be imported at runtime
try:
from typing_extensions import NotRequired, TypedDict
except ImportError:
# Create dummy classes for runtime
NotRequired = t.Any # type: ignore[misc, assignment]
TypedDict = type # type: ignore[assignment, misc]


class PluginTestConfigSchema(TypedDict):
Expand Down
12 changes: 6 additions & 6 deletions tests/workspace/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def test_environment_variables_warns_prior_to_tmux_3_0(
sum(
1
for record in caplog.records
if "Cannot set environment for new windows." in record.msg
if "Cannot set environment for new windows." in record.message
)
# From window_overrides and both_overrides, but not
# both_overrides_in_first_pane.
Expand All @@ -467,7 +467,7 @@ def test_environment_variables_warns_prior_to_tmux_3_0(
sum(
1
for record in caplog.records
if "Cannot set environment for new panes." in record.msg
if "Cannot set environment for new panes." in record.message
)
# From pane_overrides and both_overrides, but not both_overrides_in_first_pane.
== 2
Expand All @@ -476,7 +476,7 @@ def test_environment_variables_warns_prior_to_tmux_3_0(
sum(
1
for record in caplog.records
if "Cannot set environment for new panes and windows." in record.msg
if "Cannot set environment for new panes and windows." in record.message
)
# From both_overrides_in_first_pane.
== 1
Expand Down Expand Up @@ -586,7 +586,7 @@ def test_start_directory(session: Session, tmp_path: pathlib.Path) -> None:
assert session == builder.session
dirs = ["/usr/bin", "/dev", str(test_dir), "/usr", "/usr"]

for path, window in zip(dirs, session.windows):
for path, window in zip(dirs, session.windows, strict=False):
for p in window.panes:

def f(path: str, p: Pane) -> bool:
Expand Down Expand Up @@ -641,7 +641,7 @@ def test_start_directory_relative(session: Session, tmp_path: pathlib.Path) -> N

dirs = ["/usr/bin", "/dev", str(test_dir), str(config_dir), str(config_dir)]

for path, window in zip(dirs, session.windows):
for path, window in zip(dirs, session.windows, strict=False):
for p in window.panes:

def f(path: str, p: Pane) -> bool:
Expand Down Expand Up @@ -1430,7 +1430,7 @@ def test_first_pane_start_directory(session: Session, tmp_path: pathlib.Path) ->

assert session.windows
window = session.windows[0]
for path, p in zip(dirs, window.panes):
for path, p in zip(dirs, window.panes, strict=False):

def f(path: str, p: Pane) -> bool:
pane_path = p.pane_current_path
Expand Down
Loading