From 4ee32f5ce77bab6210b28fe08d8eb7911d11efeb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:22:55 -0500 Subject: [PATCH 01/22] chore(uv[cooldown]): Exempt ruff from 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 24dd740b2c..78470ff1bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,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.coverage.run] branch = true From 075120374b58fa51b2997c594414ea2b5b535f45 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:23:10 -0500 Subject: [PATCH 02/22] 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 78470ff1bd..ff25ef3b61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "coverage", "pytest-cov", # Lint - "ruff", + "ruff>=0.16.0", "mypy", "types-docutils", "types-Pygments", @@ -109,7 +109,7 @@ coverage =[ "pytest-cov", ] lint = [ - "ruff", + "ruff>=0.16.0", "mypy", "types-docutils", "types-Pygments", diff --git a/uv.lock b/uv.lock index 37e38b4904..185255fcd5 100644 --- a/uv.lock +++ b/uv.lock @@ -27,6 +27,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 @@ -1160,27 +1161,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]] @@ -1709,7 +1710,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a36" }, { name = "sphinx-autodoc-argparse", specifier = "==0.0.1a36" }, @@ -1732,7 +1733,7 @@ docs = [ ] lint = [ { name = "mypy" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "types-docutils" }, { name = "types-pygments" }, { name = "types-pyyaml" }, From 1b44e95b7f2dffcac5056ac2a8ab0efb72f17954 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:25:15 -0500 Subject: [PATCH 03/22] style(md[format]): Format Python blocks in Markdown why: ruff 0.16.0 formats Python and pycon code blocks inside Markdown by default, so `ruff format . --check` fails in CI on blocks that were never reachable by the formatter before. The churn is mechanical - no prose, no code semantics change. what: - Normalize quote style in the plugin authoring example - Collapse the aligned trailing comments in the Colors example to the formatter's single-comment spacing https://astral.sh/blog/ruff-v0.16.0 --- AGENTS.md | 14 +++++++------- docs/topics/plugins.md | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index add4ce1637..ac75e4525f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -518,13 +518,13 @@ Global workspace directories: ← heading() ```python colors = Colors() -colors.heading("Section:") # Cyan + bold (section headers) -colors.highlight("item") # Magenta + bold (primary content) -colors.info("/path/to/file") # Cyan (paths, supplementary info) -colors.muted("label:") # Blue (metadata, labels) -colors.success("ok") # Green (success states) -colors.warning("caution") # Yellow (warnings) -colors.error("failed") # Red (errors) +colors.heading("Section:") # Cyan + bold (section headers) +colors.highlight("item") # Magenta + bold (primary content) +colors.info("/path/to/file") # Cyan (paths, supplementary info) +colors.muted("label:") # Blue (metadata, labels) +colors.success("ok") # Green (success states) +colors.warning("caution") # Yellow (warnings) +colors.error("failed") # Red (errors) ``` ### Key Rules diff --git a/docs/topics/plugins.md b/docs/topics/plugins.md index d664cb633f..5d40d08a87 100644 --- a/docs/topics/plugins.md +++ b/docs/topics/plugins.md @@ -116,21 +116,21 @@ class MyTmuxpPlugin(TmuxpPlugin): # Optional version-dependency configuration. See the Plugin API # docs for every supported parameter. config = { - 'tmuxp_min_version': '1.6.2', + "tmuxp_min_version": "1.6.2", } TmuxpPlugin.__init__( self, - plugin_name='tmuxp-plugin-my-tmuxp-plugin', + plugin_name="tmuxp-plugin-my-tmuxp-plugin", **config, ) def before_workspace_builder(self, session): - session.rename_session('my-new-session-name') + session.rename_session("my-new-session-name") def reattach(self, session): - now = datetime.datetime.now().strftime('%Y-%m-%d') - session.rename_session('session_{}'.format(now)) + now = datetime.datetime.now().strftime("%Y-%m-%d") + session.rename_session("session_{}".format(now)) ``` Once it's installed in the same environment, name it in a workspace file: From 92dcf3ee8af6f7dc70c4619fb249c99a918a7b01 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:27:52 -0500 Subject: [PATCH 04/22] docs(CHANGES): Note the ruff floor bump why: The changelog's Development section tracks contributor tooling, and the ruff floor changes what every contributor's `uv sync` installs. what: - Add a `Minimum ruff>=0.16.0` deliverable under `### Development` --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index fb52f90975..6af8c35e98 100644 --- a/CHANGES +++ b/CHANGES @@ -75,6 +75,13 @@ The workspace, plugin, and CLI types now say what each field holds. They previously reached the rendered API reference as "Alias for field number 0" or as a bare name carrying only its type. +#### Minimum `ruff>=0.16.0` (was unpinned) (#1081) + +Contributors and CI now resolve the same linter release instead of whatever +each machine happened to install. ruff 0.16.0 also formats Python code blocks +inside Markdown, so documentation examples are held to the same style as +source. + ## tmuxp 1.74.0 (2026-07-04) tmuxp 1.74.0 pairs a libtmux upgrade with a documentation overhaul. It bumps libtmux to 0.61.0 — hardening tmux 3.7 patch-line support — and teaches `tmuxp debug-info` to report the exact tmux patch release (`3.7a`/`3.7b`) instead of the numeric-normalized version. The docs also gain theme-aware inline diagrams and a concept-first rewrite that leads with what each feature is before its configuration. From 37e320556edd33e8a694b095abd181f25e912714 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 14:22:43 -0500 Subject: [PATCH 05/22] Revert "chore(uv[cooldown]): Exempt ruff from 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 4ee32f5ce77bab6210b28fe08d8eb7911d11efeb. --- pyproject.toml | 4 ---- uv.lock | 1 - 2 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ff25ef3b61..bc1cfd71b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,10 +147,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.coverage.run] branch = true diff --git a/uv.lock b/uv.lock index 185255fcd5..8a3a6fabf3 100644 --- a/uv.lock +++ b/uv.lock @@ -27,7 +27,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 483384c015dbb0203e4ef7a6e10325e0351a372a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:04:12 -0500 Subject: [PATCH 06/22] 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 351 rules and silently opting out of the default set. `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 351 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 bc1cfd71b1..bb973246ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,7 +198,10 @@ ignore_missing_imports = true 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 @@ -215,7 +218,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 e9a1269c1a31865cc72f0ea66be490037dafebb6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:05:20 -0500 Subject: [PATCH 07/22] fix(exc[__init__]): Drop return from super() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `__init__` must return `None`, so returning the result of `super().__init__(...)` is misleading — it reads as if the value matters when the constructor protocol discards it. what: - Call `super().__init__(...)` as a statement in every exception and error class that returned it https://docs.astral.sh/ruff/rules/return-in-init/ --- docs/_ext/aafig.py | 2 +- src/tmuxp/_internal/colors.py | 2 +- src/tmuxp/cli/convert.py | 2 +- src/tmuxp/exc.py | 22 +++++++++++----------- src/tmuxp/workspace/validation.py | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/_ext/aafig.py b/docs/_ext/aafig.py index 6f90d51378..0ef515f04d 100644 --- a/docs/_ext/aafig.py +++ b/docs/_ext/aafig.py @@ -160,7 +160,7 @@ def render_aafig_images(app: Sphinx, doctree: nodes.Node) -> None: class AafigureNotInstalled(AafigError): def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__("aafigure module not installed", *args, **kwargs) + super().__init__("aafigure module not installed", *args, **kwargs) def render_aafigure( diff --git a/src/tmuxp/_internal/colors.py b/src/tmuxp/_internal/colors.py index a9b350016f..8826481cc6 100644 --- a/src/tmuxp/_internal/colors.py +++ b/src/tmuxp/_internal/colors.py @@ -729,7 +729,7 @@ class UnknownStyleColor(Exception): """ def __init__(self, color: CLIColour, *args: object, **kwargs: object) -> None: - return super().__init__(f"Unknown color {color!r}", *args, **kwargs) + super().__init__(f"Unknown color {color!r}", *args, **kwargs) def style( diff --git a/src/tmuxp/cli/convert.py b/src/tmuxp/cli/convert.py index a92cfebbfa..fb62df063a 100644 --- a/src/tmuxp/cli/convert.py +++ b/src/tmuxp/cli/convert.py @@ -72,7 +72,7 @@ class ConvertUnknownFileType(exc.TmuxpException): """Raise if tmuxp convert encounters an unknown filetype.""" def __init__(self, ext: str, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])", ) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index 5168aa10d0..4219feecc6 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -31,7 +31,7 @@ def __init__( msg = "Session not found" if session_target is not None: msg += f": {session_target}" - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class WindowNotFound(TmuxpException): @@ -46,7 +46,7 @@ def __init__( msg = "Window not found" if window_target is not None: msg += f": {window_target}" - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class PaneNotFound(TmuxpException): @@ -61,7 +61,7 @@ def __init__( msg = "Pane not found" if pane_target is not None: msg += f": {pane_target}" - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class EmptyWorkspaceException(WorkspaceError): @@ -72,14 +72,14 @@ def __init__( *args: object, **kwargs: object, ) -> None: - return super().__init__("Session configuration is empty.", *args, **kwargs) + super().__init__("Session configuration is empty.", *args, **kwargs) class SessionMissingWorkspaceException(WorkspaceError, ObjectDoesNotExist): """Session missing while loading tmuxp workspace.""" def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( "No session object exists for WorkspaceBuilder. " "Tip: Add session_name in constructor or run WorkspaceBuilder.build()", *args, @@ -91,7 +91,7 @@ class ActiveSessionMissingWorkspaceException(WorkspaceError): """Active session cannot be found while loading tmuxp workspace.""" def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__("No session active.", *args, **kwargs) + super().__init__("No session active.", *args, **kwargs) class WorkspaceBuilderError(WorkspaceError): @@ -121,7 +121,7 @@ def __init__( " Provide a Python dotted path (e.g. 'package.module:Builder') " "or register an entry point in the 'tmuxp.workspace_builders' group." ) - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class WorkspaceBuilderImportError(WorkspaceBuilderError): @@ -148,7 +148,7 @@ def __init__( " Confirm the builder is importable, or add its directory to " "'workspace_builder_paths'." ) - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class InvalidWorkspaceBuilder(WorkspaceBuilderError): @@ -167,7 +167,7 @@ def __init__( ) -> None: msg = f"{target!r} is not a valid workspace builder" msg += f": {reason}" if reason else "." - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class WorkspaceBuilderPathError(WorkspaceBuilderError): @@ -186,7 +186,7 @@ def __init__( ) -> None: msg = f"workspace_builder_paths entry is invalid: {path}." msg += f" {reason}" if reason else " Each entry must be an existing directory." - return super().__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class InvalidWorkspaceBuilderOption(WorkspaceBuilderError): @@ -197,7 +197,7 @@ class InvalidWorkspaceBuilderOption(WorkspaceBuilderError): """ def __init__(self, reason: str, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( f"Invalid workspace_builder_options: {reason}", *args, **kwargs, diff --git a/src/tmuxp/workspace/validation.py b/src/tmuxp/workspace/validation.py index e7fe4da743..e9de3b4a37 100644 --- a/src/tmuxp/workspace/validation.py +++ b/src/tmuxp/workspace/validation.py @@ -18,7 +18,7 @@ class SessionNameMissingValidationError(SchemaValidationError): """Tmuxp configuration error for session name missing.""" def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( 'workspace requires "session_name"', *args, **kwargs, @@ -29,7 +29,7 @@ class WindowListMissingValidationError(SchemaValidationError): """Tmuxp configuration error for window list missing.""" def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( 'workspace requires list of "windows"', *args, **kwargs, @@ -40,7 +40,7 @@ class WindowNameMissingValidationError(SchemaValidationError): """Tmuxp configuration error for missing window_name.""" def __init__(self, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( 'workspace window is missing "window_name"', *args, **kwargs, @@ -51,7 +51,7 @@ class InvalidPluginsValidationError(SchemaValidationError): """Tmuxp configuration error for invalid plugins.""" def __init__(self, plugins: t.Any, *args: object, **kwargs: object) -> None: - return super().__init__( + super().__init__( '"plugins" only supports list type. ' f" Received {type(plugins)}, " f"value: {plugins}", From d0012d4be9cb5929bcb4e7f2e4e5ddb2501447c9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:07:44 -0500 Subject: [PATCH 08/22] refactor(workspace[loader]): Drop no-op self-assignments why: A statement like `p = p` rebinds a name to itself and does nothing. These read as leftover scaffolding and obscure which assignments actually carry state. what: - Remove the self-assignments in the classic builder, the workspace loader, and the builder, freezer, and helper tests - Rename the now-unused pane loop variable in the builder tests to `_p`, keeping the iteration that creates the panes https://docs.astral.sh/ruff/rules/self-assigning-variable/ --- src/tmuxp/workspace/builder/classic.py | 1 - src/tmuxp/workspace/loader.py | 1 - tests/tests/test_helpers.py | 2 -- tests/workspace/test_builder.py | 9 +++------ tests/workspace/test_freezer.py | 1 - 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/tmuxp/workspace/builder/classic.py b/src/tmuxp/workspace/builder/classic.py index c465eb29e4..7fd78b2734 100644 --- a/src/tmuxp/workspace/builder/classic.py +++ b/src/tmuxp/workspace/builder/classic.py @@ -586,7 +586,6 @@ def build(self, session: Session | None = None, append: bool = False) -> None: focus_pane = None for pane, pane_config in self.iter_create_panes(window, window_config): assert isinstance(pane, Pane) - pane = pane if pane_config.get("focus"): focus_pane = pane diff --git a/src/tmuxp/workspace/loader.py b/src/tmuxp/workspace/loader.py index b022a4da21..fa6aa654d9 100644 --- a/src/tmuxp/workspace/loader.py +++ b/src/tmuxp/workspace/loader.py @@ -228,7 +228,6 @@ def trickle(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: for window_dict in workspace_dict["windows"]: # Prepend start_directory to relative window commands if session_start_directory: - session_start_directory = session_start_directory if "start_directory" not in window_dict: window_dict["start_directory"] = session_start_directory elif not any( diff --git a/tests/tests/test_helpers.py b/tests/tests/test_helpers.py index aa04cee8c0..8137ea87ac 100644 --- a/tests/tests/test_helpers.py +++ b/tests/tests/test_helpers.py @@ -14,7 +14,6 @@ def test_temp_session_kills_session_on_exit(server: Server) -> None: """Test temp_session() context manager kills session on exit.""" - server = server session_name = get_test_session_name(server=server) with temp_session(server=server, session_name=session_name): @@ -27,7 +26,6 @@ def test_temp_session_kills_session_on_exit(server: Server) -> None: @pytest.mark.flaky(reruns=5) def test_temp_session_if_session_killed_before_exit(server: Server) -> None: """Handles situation where session already closed within context.""" - server = server session_name = get_test_session_name(server=server) with temp_session(server=server, session_name=session_name): diff --git a/tests/workspace/test_builder.py b/tests/workspace/test_builder.py index 32cbdffcf4..baf588a483 100644 --- a/tests/workspace/test_builder.py +++ b/tests/workspace/test_builder.py @@ -51,9 +51,8 @@ def test_split_windows(session: Session) -> None: window_count = len(session.windows) # current window count assert len(session.windows) == window_count for w, wconf in builder.iter_create_windows(session): - for p in builder.iter_create_panes(w, wconf): + for _p in builder.iter_create_panes(w, wconf): w.select_layout("tiled") # fix glitch with pane size - p = p assert len(session.windows) == window_count assert isinstance(w, Window) @@ -72,9 +71,8 @@ def test_split_windows_three_pane(session: Session) -> None: window_count = len(session.windows) # current window count assert len(session.windows) == window_count for w, wconf in builder.iter_create_windows(session): - for p in builder.iter_create_panes(w, wconf): + for _p in builder.iter_create_panes(w, wconf): w.select_layout("tiled") # fix glitch with pane size - p = p assert len(session.windows) == window_count assert isinstance(w, Window) @@ -304,9 +302,8 @@ def test_window_options( window_count = len(session.windows) # current window count assert len(session.windows) == window_count for w, wconf in builder.iter_create_windows(session): - for p in builder.iter_create_panes(w, wconf): + for _p in builder.iter_create_panes(w, wconf): w.select_layout("tiled") # fix glitch with pane size - p = p assert len(session.windows) == window_count assert isinstance(w, Window) assert w.show_option("main-pane-height") == 5 diff --git a/tests/workspace/test_freezer.py b/tests/workspace/test_freezer.py index d42386ecef..653db9a08c 100644 --- a/tests/workspace/test_freezer.py +++ b/tests/workspace/test_freezer.py @@ -33,7 +33,6 @@ def test_freeze_config(session: Session) -> None: time.sleep(0.50) - session = session new_config = freezer.freeze(session) validation.validate_schema(new_config) From a3a36633e360fd172a0085043d9bf75882b7450d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:00 -0500 Subject: [PATCH 09/22] chore(ruff[lint]): Ignore PLW0127 in _compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `breakpoint = breakpoint` at module scope is not a self-assignment no-op. The right-hand side resolves to the builtin and the assignment binds it as a module attribute, which is what makes `from tmuxp._compat import breakpoint` work in `tmuxp.cli.shell` — module attribute lookup has no builtins fallback. what: - Scope a `PLW0127` per-file-ignore to `src/tmuxp/_compat.py` with the reason inline https://docs.astral.sh/ruff/rules/self-assigning-variable/ --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bb973246ec..5abb1880c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -251,6 +251,12 @@ convention = "numpy" "src/tmuxp/workspace/finders.py" = ["PTH"] "src/tmuxp/cli/*.py" = ["PTH"] "docs/_ext/aafig.py" = ["PTH"] +# `breakpoint = breakpoint` is not a no-op at module scope: the right-hand +# side resolves to the builtin, and the assignment binds it as a module +# attribute so `from tmuxp._compat import breakpoint` resolves. Module +# attribute lookup does not fall back to builtins, so dropping the line +# breaks the import in `tmuxp.cli.shell`. +"src/tmuxp/_compat.py" = ["PLW0127"] [tool.pytest.ini_options] addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules" From b749913b8a31ff0f59090eb89e75c07861653056 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:37 -0500 Subject: [PATCH 10/22] chore(ruff[lint]): Ignore S102 in docs/conf.py why: Sphinx reads tmuxp's version metadata by `exec`-ing `__about__.py` into a dict, which keeps `conf.py` from importing the package it documents. The input is a file in this repository, not untrusted data. what: - Scope an `S102` per-file-ignore to `docs/conf.py` with the reason inline https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5abb1880c2..f6d0b70e3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -257,6 +257,9 @@ convention = "numpy" # attribute lookup does not fall back to builtins, so dropping the line # breaks the import in `tmuxp.cli.shell`. "src/tmuxp/_compat.py" = ["PLW0127"] +# Sphinx reads the package's version metadata by `exec`-ing `__about__.py` +# into a dict, so `conf.py` never imports the package it documents. +"docs/conf.py" = ["S102"] [tool.pytest.ini_options] addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules" From af008a4757fe93de26e1a4acdcee4bd4ef0051bb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:56 -0500 Subject: [PATCH 11/22] chore(ruff[lint]): Ignore S102 in cli/shell why: `tmuxp shell -c` is documented as "execute python code in libtmux and exit". Running the operator's own code inside a namespace pre-populated with the server, session, window, and pane is the command's entire purpose, so `exec` here is the feature rather than an injection sink. what: - Scope an `S102` per-file-ignore to `src/tmuxp/cli/shell.py` with the reason inline https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f6d0b70e3f..37108c9503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,6 +260,10 @@ convention = "numpy" # Sphinx reads the package's version metadata by `exec`-ing `__about__.py` # into a dict, so `conf.py` never imports the package it documents. "docs/conf.py" = ["S102"] +# `tmuxp shell -c` documents itself as "execute python code in libtmux and +# exit"; running the operator's own code in a tmux-aware namespace is the +# command's purpose, not an injection sink. +"src/tmuxp/cli/shell.py" = ["S102"] [tool.pytest.ini_options] addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules" From 6273216e152f02615eb5cb16d4ecf77ca71f3989 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:06 -0500 Subject: [PATCH 12/22] chore(ruff[lint]): Ignore S102 in shell why: The interactive console honors `$PYTHONSTARTUP` and `~/.pythonrc.py` by `exec`-ing them, mirroring how CPython's own REPL sources them. Both are the user's own startup files; refusing to run them would break parity with the stock shell. what: - Scope an `S102` per-file-ignore to `src/tmuxp/shell.py` with the reason inline https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 37108c9503..f02d58b871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -264,6 +264,9 @@ convention = "numpy" # exit"; running the operator's own code in a tmux-aware namespace is the # command's purpose, not an injection sink. "src/tmuxp/cli/shell.py" = ["S102"] +# The interactive console honors `$PYTHONSTARTUP` and `~/.pythonrc.py` by +# `exec`-ing them, mirroring how CPython's own REPL sources them. +"src/tmuxp/shell.py" = ["S102"] [tool.pytest.ini_options] addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules" From 8b5b36b5bd2eab4e184aee68dc2633716850d285 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:10:43 -0500 Subject: [PATCH 13/22] fix(typing[Self]): Return Self from __new__ and __enter__ why: Annotating `PrivatePath.__new__` and `Spinner.__enter__` with the concrete class throws away the subclass in the inferred type. A subclass of either gets typed as the base, so `with SubSpinner(...) as s` loses the subclass API. what: - Annotate both with `Self`, imported from `typing_extensions` under `t.TYPE_CHECKING` since the floor is Python 3.10 https://docs.astral.sh/ruff/rules/non-self-return-type/ --- src/tmuxp/_internal/private_path.py | 4 +++- src/tmuxp/cli/_progress.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tmuxp/_internal/private_path.py b/src/tmuxp/_internal/private_path.py index 8fa0cec972..c3e735ca2a 100644 --- a/src/tmuxp/_internal/private_path.py +++ b/src/tmuxp/_internal/private_path.py @@ -14,6 +14,8 @@ logger = logging.getLogger(__name__) if t.TYPE_CHECKING: + from typing_extensions import Self + PrivatePathBase = pathlib.Path else: PrivatePathBase = type(pathlib.Path()) @@ -45,7 +47,7 @@ class PrivatePath(PrivatePathBase): 'config: ~/.tmuxp/config.yaml' """ - def __new__(cls, *args: t.Any, **kwargs: t.Any) -> PrivatePath: + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> Self: """Create a new PrivatePath instance.""" return super().__new__(cls, *args, **kwargs) diff --git a/src/tmuxp/cli/_progress.py b/src/tmuxp/cli/_progress.py index b2783f3ff7..b1cfb1cad6 100644 --- a/src/tmuxp/cli/_progress.py +++ b/src/tmuxp/cli/_progress.py @@ -25,6 +25,8 @@ if t.TYPE_CHECKING: import types + from typing_extensions import Self + # ANSI Escape Sequences HIDE_CURSOR = "\033[?25l" @@ -1117,7 +1119,7 @@ def success(self, text: str | None = None) -> None: self.stream.write(f"{msg}\n") self.stream.flush() - def __enter__(self) -> Spinner: + def __enter__(self) -> Self: self.start() return self From 06bf208c9bbec01f0ebdc3d6fe5e96684232a054 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:11:08 -0500 Subject: [PATCH 14/22] chore(ruff[lint]): Ignore BLE001 in log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: A log formatter must never raise. `record.getMessage()` interpolates caller-supplied args, so whatever their `__str__` throws has to be caught and rendered into the line — narrowing the handler would let a bad log argument take down the operation being logged. what: - Scope a `BLE001` per-file-ignore to `src/tmuxp/log.py` with the reason inline https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f02d58b871..c19d2c5f9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -267,6 +267,10 @@ convention = "numpy" # The interactive console honors `$PYTHONSTARTUP` and `~/.pythonrc.py` by # `exec`-ing them, mirroring how CPython's own REPL sources them. "src/tmuxp/shell.py" = ["S102"] +# A log formatter must never raise: `record.getMessage()` interpolates +# caller-supplied args, so anything their `__str__` throws has to be +# rendered into the line instead of propagating into the caller. +"src/tmuxp/log.py" = ["BLE001"] [tool.pytest.ini_options] addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules" From 80d55ea2dde5edbc4fdbcaf150822a57bb551a28 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:17 -0500 Subject: [PATCH 15/22] style(docs[_ext]): Parenthesize the SVG open tag why: Inside a list literal, adjacent string literals on separate lines read as separate elements. A single missing comma silently turns one element into several, so the concatenation is worth making explicit. what: - Wrap the implicitly concatenated `` open tag in parentheses https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/ --- docs/_ext/tmux_layout.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/_ext/tmux_layout.py b/docs/_ext/tmux_layout.py index 1f74fc181a..5f4b9935c6 100644 --- a/docs/_ext/tmux_layout.py +++ b/docs/_ext/tmux_layout.py @@ -246,9 +246,11 @@ def render_layout(panes: list[list[str]], layout: str, size: tuple[int, int]) -> vb_w, vb_h = w * _CW, h * _CH svg_id = f"tmux-layout-{next(_SVG_IDS)}" parts = [ - f'', + ( + f'' + ), ] parts.append("") for index, rect in enumerate(rects): From e9f50ceafc0d21689ecac60d15439a236fdffaae Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:13:53 -0500 Subject: [PATCH 16/22] fix(builder[classic]): Pass str defaults to os.getenv why: `os.getenv` returns its default unchanged, so an `int` default makes the call return `str | int` depending on whether the variable is set. The `int()` wrapper happens to accept both, which hides the inconsistency from the reader and from anything that later treats the result as text. what: - Use string defaults for the `COLUMNS` and `ROWS` lookups so every `os.getenv` here yields a `str` https://docs.astral.sh/ruff/rules/invalid-envvar-default/ --- src/tmuxp/workspace/builder/classic.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tmuxp/workspace/builder/classic.py b/src/tmuxp/workspace/builder/classic.py index 7fd78b2734..12fef7e09c 100644 --- a/src/tmuxp/workspace/builder/classic.py +++ b/src/tmuxp/workspace/builder/classic.py @@ -95,16 +95,21 @@ def _wait_for_pane_ready( def get_default_columns() -> int: """Return default session column size use when building new tmux sessions.""" return int( - os.getenv("TMUXP_DEFAULT_COLUMNS", os.getenv("COLUMNS", COLUMNS_FALLBACK)), + os.getenv( + "TMUXP_DEFAULT_COLUMNS", + os.getenv("COLUMNS", str(COLUMNS_FALLBACK)), + ), ) -ROWS_FALLBACK = int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", 24))) +ROWS_FALLBACK = int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", "24"))) def get_default_rows() -> int: """Return default session row size use when building new tmux sessions.""" - return int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", ROWS_FALLBACK))) + return int( + os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", str(ROWS_FALLBACK))), + ) class ClassicWorkspaceBuilder: From 5de089b5a92d82df631d61b3665a2aaab174aba5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:14:23 -0500 Subject: [PATCH 17/22] test(cli[help]): State check=False on subprocess.run why: The test asserts on `result.returncode` itself, so the default `check=False` is deliberate. Spelling it out separates "we inspect the exit code" from "we forgot to check it", which is the ambiguity the default hides. what: - Pass `check=False` explicitly when running `tmuxp search` https://docs.astral.sh/ruff/rules/subprocess-run-without-check/ --- tests/cli/test_help_examples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cli/test_help_examples.py b/tests/cli/test_help_examples.py index 9cbe365db2..5649fd858a 100644 --- a/tests/cli/test_help_examples.py +++ b/tests/cli/test_help_examples.py @@ -245,6 +245,7 @@ def test_search_no_args_shows_help() -> None: ["tmuxp", "search"], capture_output=True, text=True, + check=False, ) # Should show help (usage line present) assert "usage: tmuxp search" in result.stdout From 7f32b761da94eb3c3ed746eb543c7e67592cb065 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:15:47 -0500 Subject: [PATCH 18/22] refactor(_compat): Drop the pre-3.7 breakpoint fallback why: The `pdb.set_trace` fallback sat behind `PY3 and PYMINOR >= 7`, which cannot be false on the supported floor of Python 3.10. It left an unreachable `import pdb` in shipped code for no benefit. what: - Bind the builtin `breakpoint` unconditionally and drop the dead branch that imported `pdb` https://docs.astral.sh/ruff/rules/debugger/ --- src/tmuxp/_compat.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tmuxp/_compat.py b/src/tmuxp/_compat.py index 51a055f7f5..29be882133 100644 --- a/src/tmuxp/_compat.py +++ b/src/tmuxp/_compat.py @@ -30,12 +30,7 @@ def _identity(x: object) -> object: return x -if PY3 and PYMINOR >= 7: - breakpoint = breakpoint # noqa: A001 -else: - import pdb - - breakpoint = pdb.set_trace # noqa: A001 +breakpoint = breakpoint # noqa: A001 implements_to_string = _identity From b0319973eacda01ec240621288bcd2e92053b50c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:16:46 -0500 Subject: [PATCH 19/22] fix(_compat[PY3]): Test the major version with >= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `sys.version_info[0] == 3` reads as "running Python 3" but goes false the day the major version is incremented, silently flipping every consumer of `PY3` — including the `tmuxp shell` breakpoint branch — to the Python 2 path. what: - Compare the major version with `>=` so `PY3` stays true on any successor major release https://docs.astral.sh/ruff/rules/sys-version-info0-eq3/ --- src/tmuxp/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tmuxp/_compat.py b/src/tmuxp/_compat.py index 29be882133..6efe60139a 100644 --- a/src/tmuxp/_compat.py +++ b/src/tmuxp/_compat.py @@ -5,7 +5,7 @@ logger = logging.getLogger(__name__) -PY3 = sys.version_info[0] == 3 +PY3 = sys.version_info[0] >= 3 PYMINOR = sys.version_info[1] PYPATCH = sys.version_info[2] From d9f6f6b824c3291d6e863ed75e693e62aac0d4a9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:17:57 -0500 Subject: [PATCH 20/22] test(util): Drop the duplicate pathlib import why: `pathlib` is imported at module scope and used at runtime, so the copy inside the `t.TYPE_CHECKING` block never binds anything. It reads as if the module could work without a runtime `pathlib`, which it cannot. what: - Remove the redundant `import pathlib` from the type-checking block https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/ --- tests/test_util.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index 098c8c212b..caa8cac513 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -17,8 +17,6 @@ from .constants import FIXTURE_PATH if t.TYPE_CHECKING: - import pathlib - from libtmux.server import Server From 9203d8b9700850b6563eef7693aadf10df1f8660 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:18:29 -0500 Subject: [PATCH 21/22] docs(CHANGES): Note the default rule set adoption why: The ruff floor entry covers what version contributors install, not what it now checks. Adopting ruff's default rule set changes what CI rejects, which is the part a contributor needs to know before opening a pull request. what: - Add a deliverable under `### Development` for the switch from `select` to `extend-select` --- CHANGES | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES b/CHANGES index 6af8c35e98..802134c18e 100644 --- a/CHANGES +++ b/CHANGES @@ -82,6 +82,15 @@ each machine happened to install. ruff 0.16.0 also formats Python code blocks inside Markdown, so documentation examples are held to the same style as source. +#### ruff's default rule set is enabled (#1081) + +The lint configuration now extends ruff's curated default rule set instead of +replacing it. An explicit `select` overrides those defaults, so the project had +been opting out of every rule outside its own prefix list. The findings this +surfaced are fixed in place, and the idioms it flagged as intentional — `exec` +in the Sphinx config and in `tmuxp shell`, the catch-all in the log formatter — +carry per-file ignores that record why. + ## tmuxp 1.74.0 (2026-07-04) tmuxp 1.74.0 pairs a libtmux upgrade with a documentation overhaul. It bumps libtmux to 0.61.0 — hardening tmux 3.7 patch-line support — and teaches `tmuxp debug-info` to report the exact tmux patch release (`3.7a`/`3.7b`) instead of the numeric-normalized version. The docs also gain theme-aware inline diagrams and a concept-first rewrite that leads with what each feature is before its configuration. From ff9334c253e8ebdbe6bc896d85661f876edab45f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:22:45 -0500 Subject: [PATCH 22/22] style(log): Drop the shebang from a library module why: `tmuxp/log.py` is imported, never run. It has no `__main__` block and is not a console script, and the file is tracked non-executable, so the shebang promises an entry point that does not exist. what: - Remove the `#!/usr/bin/env python` line https://docs.astral.sh/ruff/rules/shebang-not-executable/ --- src/tmuxp/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tmuxp/log.py b/src/tmuxp/log.py index d4867f052a..19fb9a67aa 100644 --- a/src/tmuxp/log.py +++ b/src/tmuxp/log.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Log utilities for tmuxp.""" from __future__ import annotations