Conversation
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.
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
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
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1081 +/- ##
==========================================
- Coverage 82.56% 82.55% -0.02%
==========================================
Files 31 31
Lines 2770 2768 -2
Branches 518 518
==========================================
- Hits 2287 2285 -2
Misses 346 346
Partials 137 137 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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`
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 4ee32f5.
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
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/
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/
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/
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/
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/
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/
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/
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/
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 `<svg>` open tag in parentheses https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/
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/
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/
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/
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/
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/
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`
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
selecttoextend-select, taking the project from 351 enabled rules to 565.Changes
pyproject.toml —
ruffmoves from unpinned to>=0.16.0in both thedevandlintdependency groups.[tool.ruff.lint] selectbecomesextend-select, with every entry on its own line and a trailing comment naming the linter it selects.uv.lock — relocked, ruff 0.15.22 to 0.16.0.
AGENTS.md, docs/topics/plugins.md — ruff 0.16.0 formats Python code blocks in Markdown by default. The plugin authoring example picks up double quotes; the
Colorsexample loses its hand-aligned trailing comments. Mechanical, no prose or semantics touched.CHANGES — entries for the floor bump and for the default rule set adoption.
Design decisions
ruff 0.16 ships a 413-rule default set as its recommended baseline. An explicit
[tool.ruff.lint] selectreplaces that set rather than extending it, so this project was running its own prefix list and silently opting out of everything else. No selector token names the default set, so leavingselectunset and layering the project's linters on withextend-selectis the only way to get defaults-plus-extras.Expanding to whole prefixes instead was considered and rejected: it drags in all of
D,PL,S, and friends rather than the curated subset, which is one to two orders of magnitude noisier for no corresponding signal.Rules fixed
PLE0101return-in-init — the exception and validation-error classes returned the result ofsuper().__init__(...), which the constructor protocol discards. Now called as a statement.PLW0127self-assigning-variable — leftoverp = p/session = sessionscaffolding in the classic builder, the workspace loader, and the builder, freezer, and helper tests. Removing them left one loop variable unused, renamed to_pso the iteration that creates the panes still runs.PYI034non-self-return-type —PrivatePath.__new__andSpinner.__enter__were annotated with the concrete class, so a subclass of either got typed as the base. Both returnSelf, imported fromtyping_extensionsundert.TYPE_CHECKINGbecause the floor is Python 3.10.ISC004implicit-string-concatenation-in-collection-literal — the<svg>open tag in the layout extension is now parenthesized, so a stray comma cannot silently split one list element into three.PLW1508invalid-envvar-default — theCOLUMNSandROWSlookups passedintdefaults, makingos.getenvreturnstr | intdepending on whether the variable was set. String defaults throughout.PLW1510subprocess-run-without-check — the help-example test asserts onreturncodeitself, socheck=Falseis now explicit.T100debugger — thepdb.set_tracefallback sat behindPY3 and PYMINOR >= 7, which cannot be false on Python 3.10. Dropped, along with its unreachableimport pdb.YTT201sys-version-info0-eq3 —PY3compared the major version with==, which would flip every consumer to the Python 2 path the day the major version increments. Now>=.TC004runtime-import-in-type-checking-block —tests/test_util.pyimportedpathlibtwice; the copy inside the type-checking block never bound anything.EXE001shebang-not-executable —src/tmuxp/log.pycarried a#!/usr/bin/env pythonline but is imported rather than run, has no__main__block, and is tracked non-executable. Shebang removed. Note that ruff does not enforce this rule under WSL, so it surfaces only on a Linux runner.Ignores added
Each is scoped to a single file and carries its reason in
pyproject.toml.S102exec-builtin indocs/conf.py— Sphinx reads the version metadata byexec-ing__about__.pyinto a dict, which keepsconf.pyfrom importing the package it documents.S102exec-builtin insrc/tmuxp/cli/shell.py—tmuxp shell -cis documented 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.S102exec-builtin insrc/tmuxp/shell.py— the interactive console sources$PYTHONSTARTUPand~/.pythonrc.pythe same way CPython's own REPL does.BLE001blind-except insrc/tmuxp/log.py— a log formatter must never raise.record.getMessage()interpolates caller-supplied args, so whatever their__str__throws has to be rendered into the line rather than propagating into the operation being logged.PLW0127self-assigning-variable insrc/tmuxp/_compat.py—breakpoint = breakpointat module scope is not a no-op. The right-hand side resolves to the builtin and the assignment binds it as a module attribute, which is what makesfrom tmuxp._compat import breakpointwork intmuxp.cli.shell; module attribute lookup has no builtins fallback.Test plan
uv run ruff check .— all checks passeduv run ruff format . --check— all files already formatteduv run mypy— no issues founduv run py.test— full suite green under tmux locally