From 44d044b0b45fe08bbf7dc5b9214a57c261ff4c6b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:16:19 -0500 Subject: [PATCH 01/18] chore(uv[cooldown]): Exempt ruff from release cooldown why: uv's global `exclude-newer = "3 days"` supply-chain cooldown hides ruff 0.16.0 (released 2026-07-23) from the resolver, so the version floor in the next commit cannot resolve. what: - Add `ruff = false` to `[tool.uv.exclude-newer-package]` Temporary. Revert before merge - the cooldown clears on its own and the `ruff>=0.16` floor is what actually holds the version. --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fc0bff9cb..a19076aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,10 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = false +# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is +# still inside the 3-day cooldown, so the resolver cannot see it. Drop this +# line once the cooldown clears; the version floor is what holds ruff 0.16. +ruff = false [tool.mypy] strict = true From 3a2d6b9d392866f8bcca491dc0b0bed97a9201e9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:16:53 -0500 Subject: [PATCH 02/18] py(deps[dev]): Require ruff>=0.16.0 why: ruff 0.16.0 stabilizes rules inside prefixes this project already selects and starts formatting Python code blocks in Markdown. Pinning a floor keeps contributors and CI on the same diagnostics instead of splitting on whatever ruff each machine resolved. what: - Raise `ruff` to `>=0.16.0` in the `dev` and `lint` dependency groups - Relock `uv.lock`: ruff 0.15.22 -> 0.16.0 https://astral.sh/blog/ruff-v0.16.0 --- pyproject.toml | 4 ++-- uv.lock | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a19076aef..5eb9859ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ dev = [ "coverage", "pytest-cov", # Lint - "ruff", + "ruff>=0.16.0", "mypy", ] @@ -95,7 +95,7 @@ coverage =[ ] lint = [ "typing-extensions; python_version < '3.11'", - "ruff", + "ruff>=0.16.0", "mypy", ] diff --git a/uv.lock b/uv.lock index 4910994e0..e115b271c 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,7 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false +ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false @@ -658,7 +659,7 @@ dev = [ { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, { name = "pytest-xdist" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a36" }, { name = "sphinx-autodoc-pytest-fixtures", specifier = "==0.0.1a36" }, @@ -673,7 +674,7 @@ docs = [ ] lint = [ { name = "mypy" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] testing = [ @@ -1166,27 +1167,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, - { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] From 2443276229807c6720e0b0bdabeadd0bf133dbc3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:18:43 -0500 Subject: [PATCH 03/18] _internal(style[query_list]): Sort None last in keygetter union why: ruff 0.16.0 stabilizes RUF036, which requires `None` to be the last member of a type union. It reads as the fallback case and matches how `Optional[X]` and typeshed order their unions. what: - Reorder `keygetter`'s return annotation to end in `| None` https://docs.astral.sh/ruff/rules/none-not-at-end-of-union/ --- src/libtmux/_internal/query_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libtmux/_internal/query_list.py b/src/libtmux/_internal/query_list.py index e28ea42e0..994161ca2 100644 --- a/src/libtmux/_internal/query_list.py +++ b/src/libtmux/_internal/query_list.py @@ -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**: From 14c2df0586d8987a5a5703ebae7a9623b318f53a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:22:42 -0500 Subject: [PATCH 04/18] docs(CHANGES) Lint floor moved to ruff 0.16 why: Document the dev-tooling floor bump alongside the other Development entries for the unreleased version. what: - Record `ruff>=0.16.0` minimum under `### Development` - Note RUF036 stabilization and Markdown code-block formatting --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 09fa208fd..b28614b67 100644 --- a/CHANGES +++ b/CHANGES @@ -73,6 +73,13 @@ 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/`. + ## libtmux 0.62.0 (2026-07-12) libtmux 0.62.0 teaches libtmux objects to locate themselves and to resolve From 39bc31773f857a04a7b7960006273282142eb53b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 14:20:56 -0500 Subject: [PATCH 05/18] Revert "chore(uv[cooldown]): Exempt ruff from release cooldown" why: ruff 0.16.0 published 2026-07-23T19:10Z and has now cleared uv's 3-day supply-chain cooldown, so the resolver reaches it unaided. The `ruff>=0.16.0` floor is what holds the version; leaving the exemption would permanently opt ruff out of the cooldown guard. what: - Drop `ruff = false` from `[tool.uv.exclude-newer-package]` - Relock: the setting is recorded in `uv.lock`, so removing it forces a re-resolve. ruff stays at 0.16.0 and no other package moves. This reverts commit 44d044b0eb02fbd53fbd05e5a5bfe5b48b04a4de. --- pyproject.toml | 4 ---- uv.lock | 1 - 2 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5eb9859ac..12072a20d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,10 +130,6 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = false -# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is -# still inside the 3-day cooldown, so the resolver cannot see it. Drop this -# line once the cooldown clears; the version floor is what holds ruff 0.16. -ruff = false [tool.mypy] strict = true diff --git a/uv.lock b/uv.lock index e115b271c..32e4c4948 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,6 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false -ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false From b83b031d162477fbd992791b5a89b3008ac4f4e2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:00:56 -0500 Subject: [PATCH 06/18] chore(ruff[lint]): Adopt ruff's default rule set why: ruff 0.16 ships a curated 413-rule default set as its recommended baseline. An explicit `select` replaces that set rather than extending it, so this project was running 152 rules and silently opting out of the other 413. `extend-select` layers the project's own linters on top of the default set instead of in place of it. what: - Replace `select` with `extend-select`, same entries, one per line - Note in the file why `select` stays unset Enabled rules go from 152 to 565. https://docs.astral.sh/ruff/linter/#rule-selection --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12072a20d..8e49b0d3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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 From aef0c115350c47a944267f32ce63c2a4afc995dc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:26 -0500 Subject: [PATCH 07/18] tests(style[from_env]): Collapse type union into one type why: ruff's default rule set flags `type[A] | type[B]` unions, which spell out one construct four times where `type[A | B]` says the same thing. what: - Combine the `SubclassFixture.subclass` annotation into `type[...]` - Combine the matching parametrized test argument annotation https://docs.astral.sh/ruff/rules/unnecessary-type-union/ --- tests/test_from_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_from_env.py b/tests/test_from_env.py index 2f8975e80..1d0c699a6 100644 --- a/tests/test_from_env.py +++ b/tests/test_from_env.py @@ -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): @@ -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. From 58dd09a6326efd03660c9418e837d896a2ff5c30 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:58 -0500 Subject: [PATCH 08/18] ControlMode(fix[__enter__]): Return Self, not the class why: Annotating `__enter__` with the concrete class makes a subclass used as a context manager infer as `ControlMode` instead of the subclass, so subclass-only attributes fail type checking inside the `with` block. what: - Annotate `ControlMode.__enter__` as returning `Self` - Import `Self` from `typing_extensions` under `TYPE_CHECKING`, as the rest of the package does for the 3.10 floor https://docs.astral.sh/ruff/rules/non-self-return-type/ --- src/libtmux/_internal/control_mode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libtmux/_internal/control_mode.py b/src/libtmux/_internal/control_mode.py index 6c1d2301e..05945451e 100644 --- a/src/libtmux/_internal/control_mode.py +++ b/src/libtmux/_internal/control_mode.py @@ -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 @@ -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() From 4a0f6201ecb5b9de3cc5af039984aea35115eefa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:25 -0500 Subject: [PATCH 09/18] test(fix[constants]): Pass str defaults to os.getenv why: `os.getenv` returns its default unchanged, so a non-str default is returned verbatim when the variable is unset. The retry constants only survived because `int()` and `float()` accept numbers as well as strings; any consumer treating the result as the documented `str | None` would break. what: - Quote the `RETRY_TIMEOUT_SECONDS` fallback - Quote the `RETRY_INTERVAL_SECONDS` fallback https://docs.astral.sh/ruff/rules/invalid-envvar-default/ --- src/libtmux/test/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libtmux/test/constants.py b/src/libtmux/test/constants.py index 7923d00ea..e8c19e5ba 100644 --- a/src/libtmux/test/constants.py +++ b/src/libtmux/test/constants.py @@ -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")) From de87e0c866f9fcacc2e9e9fdbdb8bab1c57616ab Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:59 -0500 Subject: [PATCH 10/18] tests(fix[tmuxobject]): Stop shadowing the session fixture why: Three tests take the `session` fixture and then rebind that name as a loop variable. The fixture object becomes unreachable after the first iteration, so a later edit that meant to use the fixture would silently get whichever session the loop last visited. what: - Bind the loop variable as `session_` in `test_find_where`, `test_find_where_multiple_infos`, and `test_where` https://docs.astral.sh/ruff/rules/redefined-argument-from-local/ --- tests/test_tmuxobject.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_tmuxobject.py b/tests/test_tmuxobject.py index e8c8be9ec..8a2621a0e 100644 --- a/tests/test_tmuxobject.py +++ b/tests/test_tmuxobject.py @@ -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: @@ -60,10 +60,10 @@ 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( @@ -71,17 +71,17 @@ def test_find_where_multiple_infos(server: Server, session: Session) -> None: 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, ) @@ -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( @@ -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, ) From 7b3c13fb7abdecce27df9a6737503c6c0f6ca529 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:30 -0500 Subject: [PATCH 11/18] exc(fix): Drop explicit returns from __init__ why: `__init__` must return `None`, so `return super().__init__(...)` relies on the return value being `None` by accident. Python raises `TypeError` the moment a base class returns anything else, and the construct hides which branch is early-exit control flow. what: - Call `super().__init__(...)` as a statement in the exception constructors - Keep the early-exit branches explicit with a bare `return` https://docs.astral.sh/ruff/rules/return-in-init/ --- src/libtmux/_internal/query_list.py | 4 ++-- src/libtmux/_vendor/version.py | 2 +- src/libtmux/exc.py | 27 +++++++++++++++------------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/libtmux/_internal/query_list.py b/src/libtmux/_internal/query_list.py index 994161ca2..20aeb407f 100644 --- a/src/libtmux/_internal/query_list.py +++ b/src/libtmux/_internal/query_list.py @@ -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]): diff --git a/src/libtmux/_vendor/version.py b/src/libtmux/_vendor/version.py index b49dab12b..01dfc9048 100644 --- a/src/libtmux/_vendor/version.py +++ b/src/libtmux/_vendor/version.py @@ -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: diff --git a/src/libtmux/exc.py b/src/libtmux/exc.py index 4d34a2d35..57bb06102 100644 --- a/src/libtmux/exc.py +++ b/src/libtmux/exc.py @@ -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, ) @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): From 11cd0e477254eb0ae293e49a7a1f3bff6a2fe7ba Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:45 -0500 Subject: [PATCH 12/18] tests(fix[dataclasses]): Narrow the session lookup except why: The fallback exists for a server with no sessions yet, which is an `IndexError`. Catching every exception meant a genuinely broken `Server.sessions` would be papered over by creating a session, and the test would pass while reporting nothing. what: - Catch `IndexError` when indexing into `server.sessions` https://docs.astral.sh/ruff/rules/blind-except/ --- tests/test_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index 4fcc05207..c70443ed5 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -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 From dc8e522ef9cf0d7e773b08dfb313ceea0069b6de Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:13:12 -0500 Subject: [PATCH 13/18] chore(ruff[lint]): Allow exec in the Sphinx config why: `docs/conf.py` reads the package's version metadata by exec'ing `__about__.py` so the docs build does not import the package it is documenting. The input is a file in this repository, not user data, so the flake8-bandit warning has nothing to warn about here. what: - Per-file-ignore S102 for `docs/conf.py` https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8e49b0d3b..55eb6e983 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,6 +231,11 @@ 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", +] [tool.pytest.ini_options] addopts = [ From 166968e71e0576500eb16c99a529e5b7fe7eee81 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:13:51 -0500 Subject: [PATCH 14/18] chore(ruff[lint]): Keep query_list's explicit re-exports why: `from libtmux.exc import ObjectDoesNotExist as ObjectDoesNotExist` is PEP 484's explicit re-export form. mypy runs with `strict = true`, which turns on `no_implicit_reexport`, so dropping the alias makes `from libtmux._internal.query_list import ObjectDoesNotExist` an `attr-defined` error for every caller. what: - Per-file-ignore PLC0414 for `src/libtmux/_internal/query_list.py` https://docs.astral.sh/ruff/rules/useless-import-alias/ --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 55eb6e983..e0942c0b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,6 +236,11 @@ convention = "numpy" # 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", +] [tool.pytest.ini_options] addopts = [ From 0b1442f79924da19d6702c2fe469211d38dfe1a2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:14:13 -0500 Subject: [PATCH 15/18] chore(ruff[lint]): Allow catch-all in query_list lookups why: `lookup_in` and `lookup_nin` are predicates run against whatever objects the caller put in the `QueryList`, including mappings with custom `__contains__`. Their contract is to answer "does this match", so a comparison that raises is a non-match, not an error worth propagating out of `QueryList.filter`. what: - Per-file-ignore BLE001 for `src/libtmux/_internal/query_list.py` https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e0942c0b0..12a451e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,6 +240,9 @@ convention = "numpy" # `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", ] [tool.pytest.ini_options] From 19319082ae1f1cc0cbca5a4603b7273a3221a862 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:14:29 -0500 Subject: [PATCH 16/18] chore(ruff[lint]): Allow catch-all in option parsing why: The option parsers read whatever tmux prints, across every tmux version this library supports. Each handler logs the failure and keeps the raw value, so an option this version of libtmux cannot parse does not take the rest of the object's options down with it. Narrowing to the exception types today's tmux happens to produce would turn a future tmux's output into a hard failure. what: - Per-file-ignore BLE001 for `src/libtmux/options.py` https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 12a451e8a..8d5d5cb9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -244,6 +244,12 @@ convention = "numpy" # 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", +] [tool.pytest.ini_options] addopts = [ From f8fc359ab757538125393f2adee0451f3eed78f3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:14:41 -0500 Subject: [PATCH 17/18] chore(ruff[lint]): Allow catch-all in Server.is_alive why: `Server.is_alive` answers a yes/no question about a server that may not be there at all. A missing socket, a dead daemon, a missing tmux binary and a crashed subprocess are all the same answer, and this is the documented lenient half of the pair. `Server.raise_if_dead` is the primitive for callers who need the reason. what: - Per-file-ignore BLE001 for `src/libtmux/server.py` https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8d5d5cb9f..16f8ebc3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -250,6 +250,12 @@ convention = "numpy" # 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 = [ From 152615546cc87e18b49644cd406a1088cf7c8aa6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:17:56 -0500 Subject: [PATCH 18/18] docs(CHANGES) Default ruff rule set why: The ruff floor entry covered the version bump but not the rule selection change that rides with it. what: - Note that lint runs on ruff's default set plus this project's linters - Name the scoped per-file ignores and why they exist --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index b28614b67..7a691b0cc 100644 --- a/CHANGES +++ b/CHANGES @@ -80,6 +80,13 @@ Minimum `ruff>=0.16.0` (was unpinned). 0.16.0 stabilizes 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() `, 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