Skip to content

Bump ruff floor to 0.16.0 and adopt its default rule set - #113

Merged
tony merged 15 commits into
mainfrom
ruff-0.16
Jul 26, 2026
Merged

Bump ruff floor to 0.16.0 and adopt its default rule set#113
tony merged 15 commits into
mainfrom
ruff-0.16

Conversation

@tony

@tony tony commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • Bump the ruff floor to >=0.16.0 in the dev and lint dependency groups so contributors and CI see the same diagnostics instead of splitting on whatever ruff each machine happened to resolve.
  • Adopt ruff's own curated default rule set by moving [tool.ruff.lint] from select to extend-select. An explicit select replaces the default set rather than adding to it, so this project had been silently opting out of every rule it did not name itself. Enabled rules go from 351 to 565.
  • Fix the six real findings that surfaced, one rule per commit, and scope the two intentional ones as per-file ignores with the reason recorded in pyproject.toml.

Why extend-select

ruff 0.16 ships a curated default rule set as its recommended baseline. There is no token that names that set from inside select — the selector grammar has ALL, prefixes, and individual codes, and nothing else — so a config with an explicit select cannot say "the defaults, plus these". Leaving select unset and layering the project's own linters on with extend-select is the only way to get defaults-plus-extras. The file carries a comment saying so, because the natural instinct on reading it is to "tidy" extend-select back into select.

Expanding to whole prefixes instead was considered and rejected: it drags in all of D, PL, S, and friends rather than ruff's curated subset, and is one to two orders of magnitude noisier for no corresponding gain in signal.

Findings fixed

FURB167re.S spelled out as re.DOTALL in the docs-drift test.

FURB188mcp_swap derived the server name with a conditional slice that restated the -mcp suffix twice and depended on -len() arithmetic staying in sync with its own endswith guard. Now str.removesuffix.

FURB192mcp_swap sorted a whole set of registered server names to read the first element, twice, both times to suggest a --server value. Now min.

FLY002 — the server's base instruction blob joined a static tuple of sections. ruff's own fix collapses the sections onto a single over-long line and trades the finding for an E501, so this one is hand-written instead: an f-string with one section per line, which reads the same as the tuple did.

ISC004 — the run_command marker-filtering fixtures build captured pane lines as multi-line implicit concatenations inside a list. Element boundaries are exactly what those fixtures test, and unparenthesized concatenation makes a dropped comma indistinguishable from a deliberate join. Parenthesized, matching the form one fixture in the same literal already used.

EXE001scripts/mcp_swap.py carried a shebang without the executable bit, and the interpreter it named was wrong besides: it is a PEP 723 script whose inline metadata declares tomlkit, so direct execution under bare python3 raised ModuleNotFoundError. The shebang now points at uv run --script, which honours that metadata, and the bit is set in the index. uv run scripts/mcp_swap.py and the just mcp-* recipes are unchanged. Worth flagging for anyone reproducing this locally: ruff does not enforce EXE001 on WSL, so this one surfaced only on CI.

Findings scoped as ignores

S102 in docs/conf.py — the Sphinx idiom of execing the package's __about__.py to read version metadata without importing the package into the docs build. A repo-local file, no user input. Scoped to that one file so a stray exec anywhere else still fails the lint.

BLE001 in six files — every site was read individually, and all of them are deliberate boundaries: the MCP error middleware that renders any tool exception as an is_error result, the per-operation isolation in the tool and send-keys batch runners, the best-effort tmux version probes and lifespan buffer GC, and mcp_swap's rollback to a config's original bytes when a write fails. tmux and the agent CLIs' config files fail in more ways than a narrowed clause can enumerate, so narrowing here would trade a logged degradation for a crash in code whose whole job is not to crash. BLE001 stays enforced everywhere else.

Design decisions

Floor, not pin. >=0.16.0 is what holds the version: it guarantees every contributor is at or past the release that changes formatting behavior, while still letting them take later patch releases. uv.lock pins the exact version CI resolves.

One commit per rule. Each fixed rule and each added ignore is its own commit, so a rule can be reverted or re-argued without unpicking the rest, and each commit body carries the rule's documentation link.

No noqa, no explanatory code comments. Intentional idioms are declared in the ruff config, where the next reader of the config finds them, rather than annotated at the call site.

Markdown formatting is a no-op here. 0.16.0 makes ruff format format Python and pycon code blocks inside Markdown by default, so the README and the docs/ tree are now in scope. Every Markdown file in this repo is already formatted the way ruff wants it, so ruff format --check . passes across the wider file set with no churn.

Test plan

  • uv run ruff check . — all checks passed
  • uv run ruff format --check . — every file already formatted (Python plus Markdown code blocks)
  • uv run mypy — no issues found
  • uv run pytest — full suite green
  • Enabled-rule count confirmed against ruff check --show-settings . before and after the config change

tony added 3 commits July 26, 2026 13:22
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: Contributors reading the changelog need to know the dev toolchain
floor moved and that `ruff format` now reaches Markdown.

what:
- Add a `Minimum `ruff>=0.16.0`` entry under `### Development` (#113)
@codecov-commenter

codecov-commenter commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.08%. Comparing base (5c35acf) to head (64ca1cf).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #113   +/-   ##
=======================================
  Coverage   86.08%   86.08%           
=======================================
  Files          46       46           
  Lines        3593     3593           
  Branches      516      516           
=======================================
  Hits         3093     3093           
  Misses        355      355           
  Partials      145      145           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 10 commits July 26, 2026 14:22
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 afa3e20.
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: ruff's default rule set flags the single-letter `re` flag aliases.
`re.S` is terse to the point of being cryptic at the call site; the long
name says what the flag does without a trip to the `re` docs.

what:
- Use `re.DOTALL` instead of the `re.S` alias

https://docs.astral.sh/ruff/rules/regex-flag-alias/
why: ruff's default rule set flags hand-rolled suffix stripping. The
conditional slice restated the suffix twice and depended on `-len()`
arithmetic staying in sync with the `endswith` guard; `str.removesuffix`
expresses the same intent in one term.

what:
- Replace the conditional slice in `_derive_server_and_entry` with
  `str.removesuffix`

https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/
why: ruff's default rule set flags `sorted(...)[0]`, which sorts a whole
collection to read one element. Both sites want the lowest registered
server name to suggest via `--server`; `min` says that directly and
drops the throwaway sort.

what:
- Use `min` instead of `sorted(...)[0]` in `_naming_hint` and the status
  report's name-mismatch warning

https://docs.astral.sh/ruff/rules/sorted-min-max/
why: ruff's default rule set flags joining a static tuple, since the
separator and the operands are all known at author time. ruff's own fix
collapses the seven sections onto one 187-char line and trades the
finding for an E501; interpolating one section per line keeps the
sections scannable and stays inside the line limit.

what:
- Compose `_BASE_INSTRUCTIONS` from f-string fragments instead of
  `"\n\n".join(...)`

https://docs.astral.sh/ruff/rules/static-join-to-f-string/
why: ruff's default rule set flags unparenthesized implicit string
concatenation inside a collection, because a missing comma reads
identically to a deliberate join and silently merges two elements. These
fixtures model captured pane lines, where element boundaries are the
thing under test.

what:
- Wrap the multi-line marker strings in `FILTER_DROP_FIXTURES` in
  parentheses, matching the form already used by the targeted-marker
  fixture

https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/
why: ruff's default rule set flags `exec`. `docs/conf.py` execs the
package's `__about__.py` to read version and project metadata without
importing `libtmux_mcp` — the standard Sphinx idiom, on a repo-local
file, with no user input reaching it. Scoped to that one file so a
stray `exec` anywhere else still fails the lint.

what:
- Add a `docs/conf.py` per-file ignore for S102 with the reason inline

https://docs.astral.sh/ruff/rules/exec-builtin/
why: ruff's default rule set flags every `except Exception`. All nine
sites are deliberate boundaries — the MCP error middleware, the batch
runners' per-operation isolation, the best-effort tmux probes and buffer
GC, and mcp_swap's config rollback. tmux and the agent CLIs' config
files fail in more ways than a narrowed clause can enumerate, so
narrowing would trade a logged degradation for a crash in code whose
whole job is not to crash.

what:
- Add per-file ignores for BLE001 in the six files that hold those
  boundaries, with the reason for each recorded inline
- Leave BLE001 enforced everywhere else

https://docs.astral.sh/ruff/rules/blind-except/
why: The ruff entry covered the version floor but not the config change
that came with it, which is the part that alters what CI enforces.

what:
- Describe the `select` to `extend-select` move, the enabled-rule count
  change, the fixes it produced, and the two scoped ignores
@tony tony changed the title Bump ruff floor to 0.16.0 Bump ruff floor to 0.16.0 and adopt its default rule set Jul 26, 2026
tony added 2 commits July 26, 2026 15:14
why: ruff's default rule set flags a shebang on a non-executable file.
The bit was never set, and the interpreter named was wrong besides: this
is a PEP 723 script whose inline metadata declares tomlkit, so running
it under bare `python3` raises ModuleNotFoundError. `uv run --script`
honours that metadata. The rule is skipped on WSL, so this only
surfaced on CI.

what:
- Point the shebang at `uv run --script`
- Set the executable bit in the index

`uv run scripts/mcp_swap.py` and the `just mcp-*` recipes are unchanged.
https://docs.astral.sh/ruff/rules/shebang-not-executable/
why: The default rule set entry listed the fixes it produced but not the
one that only surfaced on CI, which is the one with a user-visible
effect: the script can now be run directly.

what:
- Mention the executable bit and the `uv run --script` shebang
@tony
tony merged commit 1191ec1 into main Jul 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants