Skip to content

py(deps[dev]): ruff 0.16.0 - #1081

Merged
tony merged 22 commits into
masterfrom
ruff-0.16
Jul 26, 2026
Merged

py(deps[dev]): ruff 0.16.0#1081
tony merged 22 commits into
masterfrom
ruff-0.16

Conversation

@tony

@tony tony commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • Migrate dev tooling to ruff 0.16.0, pinning a floor so contributors and CI resolve the same linter release instead of whatever each machine happened to install.
  • Adopt ruff's curated default rule set by switching select to extend-select, taking the project from 351 enabled rules to 565.
  • Fix every finding the default set surfaced, and scope a per-file ignore — with its reason recorded in the config — around each idiom that is deliberate.

Changes

pyproject.tomlruff moves from unpinned to >=0.16.0 in both the dev and lint dependency groups. [tool.ruff.lint] select becomes extend-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 Colors example 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] select replaces 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 leaving select unset and layering the project's linters on with extend-select is 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

  • PLE0101 return-in-init — the exception and validation-error classes returned the result of super().__init__(...), which the constructor protocol discards. Now called as a statement.
  • PLW0127 self-assigning-variable — leftover p = p / session = session scaffolding in the classic builder, the workspace loader, and the builder, freezer, and helper tests. Removing them left one loop variable unused, renamed to _p so the iteration that creates the panes still runs.
  • PYI034 non-self-return-typePrivatePath.__new__ and Spinner.__enter__ were annotated with the concrete class, so a subclass of either got typed as the base. Both return Self, imported from typing_extensions under t.TYPE_CHECKING because the floor is Python 3.10.
  • ISC004 implicit-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.
  • PLW1508 invalid-envvar-default — the COLUMNS and ROWS lookups passed int defaults, making os.getenv return str | int depending on whether the variable was set. String defaults throughout.
  • PLW1510 subprocess-run-without-check — the help-example test asserts on returncode itself, so check=False is now explicit.
  • T100 debugger — the pdb.set_trace fallback sat behind PY3 and PYMINOR >= 7, which cannot be false on Python 3.10. Dropped, along with its unreachable import pdb.
  • YTT201 sys-version-info0-eq3PY3 compared the major version with ==, which would flip every consumer to the Python 2 path the day the major version increments. Now >=.
  • TC004 runtime-import-in-type-checking-blocktests/test_util.py imported pathlib twice; the copy inside the type-checking block never bound anything.
  • EXE001 shebang-not-executablesrc/tmuxp/log.py carried a #!/usr/bin/env python line 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.

  • S102 exec-builtin in docs/conf.py — Sphinx reads the version metadata by exec-ing __about__.py into a dict, which keeps conf.py from importing the package it documents.
  • S102 exec-builtin in src/tmuxp/cli/shell.pytmuxp shell -c is 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.
  • S102 exec-builtin in src/tmuxp/shell.py — the interactive console sources $PYTHONSTARTUP and ~/.pythonrc.py the same way CPython's own REPL does.
  • BLE001 blind-except in src/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.
  • PLW0127 self-assigning-variable in src/tmuxp/_compat.pybreakpoint = breakpoint at 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 makes from tmuxp._compat import breakpoint work in tmuxp.cli.shell; module attribute lookup has no builtins fallback.

Test plan

  • uv run ruff check . — all checks passed
  • uv run ruff format . --check — all files already formatted
  • uv run mypy — no issues found
  • uv run py.test — full suite green under tmux locally
  • CI green across the tmux version matrix

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: 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

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.55%. Comparing base (96a1e73) to head (ff9334c).

Files with missing lines Patch % Lines
src/tmuxp/exc.py 72.72% 3 Missing ⚠️
src/tmuxp/cli/convert.py 0.00% 1 Missing ⚠️
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.
📢 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 19 commits July 26, 2026 13:27
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/
@tony
tony merged commit 8240ca8 into master Jul 26, 2026
14 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.

1 participant