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 stabilizes RUF036, which requires `None` to be the last member of a type union. It reads as the fallback case and matches how `Optional[X]` and typeshed order their unions. what: - Reorder `keygetter`'s return annotation to end in `| None` https://docs.astral.sh/ruff/rules/none-not-at-end-of-union/
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #722 +/- ##
==========================================
- Coverage 52.44% 52.37% -0.07%
==========================================
Files 26 26
Lines 3726 3729 +3
Branches 747 747
==========================================
- Hits 1954 1953 -1
- Misses 1468 1472 +4
Partials 304 304 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: Document the dev-tooling floor bump alongside the other Development entries for the unreleased version. what: - Record `ruff>=0.16.0` minimum under `### Development` - Note RUF036 stabilization and Markdown code-block formatting
why: ruff 0.16.0 published 2026-07-23T19:10Z and has now cleared uv's 3-day supply-chain cooldown, so the resolver reaches it unaided. The `ruff>=0.16.0` floor is what holds the version; leaving the exemption would permanently opt ruff out of the cooldown guard. what: - Drop `ruff = false` from `[tool.uv.exclude-newer-package]` - Relock: the setting is recorded in `uv.lock`, so removing it forces a re-resolve. ruff stays at 0.16.0 and no other package moves. This reverts commit 44d044b0eb02fbd53fbd05e5a5bfe5b48b04a4de.
why: ruff 0.16 ships a curated 413-rule default set as its recommended baseline. An explicit `select` replaces that set rather than extending it, so this project was running 152 rules and silently opting out of the other 413. `extend-select` layers the project's own linters on top of the default set instead of in place of it. what: - Replace `select` with `extend-select`, same entries, one per line - Note in the file why `select` stays unset Enabled rules go from 152 to 565. https://docs.astral.sh/ruff/linter/#rule-selection
why: ruff's default rule set flags `type[A] | type[B]` unions, which spell out one construct four times where `type[A | B]` says the same thing. what: - Combine the `SubclassFixture.subclass` annotation into `type[...]` - Combine the matching parametrized test argument annotation https://docs.astral.sh/ruff/rules/unnecessary-type-union/
why: Annotating `__enter__` with the concrete class makes a subclass used as a context manager infer as `ControlMode` instead of the subclass, so subclass-only attributes fail type checking inside the `with` block. what: - Annotate `ControlMode.__enter__` as returning `Self` - Import `Self` from `typing_extensions` under `TYPE_CHECKING`, as the rest of the package does for the 3.10 floor https://docs.astral.sh/ruff/rules/non-self-return-type/
why: `os.getenv` returns its default unchanged, so a non-str default is returned verbatim when the variable is unset. The retry constants only survived because `int()` and `float()` accept numbers as well as strings; any consumer treating the result as the documented `str | None` would break. what: - Quote the `RETRY_TIMEOUT_SECONDS` fallback - Quote the `RETRY_INTERVAL_SECONDS` fallback https://docs.astral.sh/ruff/rules/invalid-envvar-default/
why: Three tests take the `session` fixture and then rebind that name as a loop variable. The fixture object becomes unreachable after the first iteration, so a later edit that meant to use the fixture would silently get whichever session the loop last visited. what: - Bind the loop variable as `session_` in `test_find_where`, `test_find_where_multiple_infos`, and `test_where` https://docs.astral.sh/ruff/rules/redefined-argument-from-local/
why: `__init__` must return `None`, so `return super().__init__(...)` relies on the return value being `None` by accident. Python raises `TypeError` the moment a base class returns anything else, and the construct hides which branch is early-exit control flow. what: - Call `super().__init__(...)` as a statement in the exception constructors - Keep the early-exit branches explicit with a bare `return` https://docs.astral.sh/ruff/rules/return-in-init/
why: The fallback exists for a server with no sessions yet, which is an `IndexError`. Catching every exception meant a genuinely broken `Server.sessions` would be papered over by creating a session, and the test would pass while reporting nothing. what: - Catch `IndexError` when indexing into `server.sessions` https://docs.astral.sh/ruff/rules/blind-except/
why: `docs/conf.py` reads the package's version metadata by exec'ing `__about__.py` so the docs build does not import the package it is documenting. The input is a file in this repository, not user data, so the flake8-bandit warning has nothing to warn about here. what: - Per-file-ignore S102 for `docs/conf.py` https://docs.astral.sh/ruff/rules/exec-builtin/
why: `from libtmux.exc import ObjectDoesNotExist as ObjectDoesNotExist` is PEP 484's explicit re-export form. mypy runs with `strict = true`, which turns on `no_implicit_reexport`, so dropping the alias makes `from libtmux._internal.query_list import ObjectDoesNotExist` an `attr-defined` error for every caller. what: - Per-file-ignore PLC0414 for `src/libtmux/_internal/query_list.py` https://docs.astral.sh/ruff/rules/useless-import-alias/
why: `lookup_in` and `lookup_nin` are predicates run against whatever objects the caller put in the `QueryList`, including mappings with custom `__contains__`. Their contract is to answer "does this match", so a comparison that raises is a non-match, not an error worth propagating out of `QueryList.filter`. what: - Per-file-ignore BLE001 for `src/libtmux/_internal/query_list.py` https://docs.astral.sh/ruff/rules/blind-except/
why: The option parsers read whatever tmux prints, across every tmux version this library supports. Each handler logs the failure and keeps the raw value, so an option this version of libtmux cannot parse does not take the rest of the object's options down with it. Narrowing to the exception types today's tmux happens to produce would turn a future tmux's output into a hard failure. what: - Per-file-ignore BLE001 for `src/libtmux/options.py` https://docs.astral.sh/ruff/rules/blind-except/
why: `Server.is_alive` answers a yes/no question about a server that may not be there at all. A missing socket, a dead daemon, a missing tmux binary and a crashed subprocess are all the same answer, and this is the documented lenient half of the pair. `Server.raise_if_dead` is the primitive for callers who need the reason. what: - Per-file-ignore BLE001 for `src/libtmux/server.py` https://docs.astral.sh/ruff/rules/blind-except/
why: The ruff floor entry covered the version bump but not the rule selection change that rides with it. what: - Note that lint runs on ruff's default set plus this project's linters - Name the scoped per-file ignores and why they exist
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
ruff>=0.16.0in thedevandlintdependency groups so contributors and CI see the same diagnostics[tool.ruff.lint] selecttoextend-select, taking the enabled rule count from 351 to 565Changes
Dependencies
pyproject.toml:rufffloor raised to>=0.16.0in thedevandlintgroups.uv.lock: relocked, ruff 0.15.22 -> 0.16.0.Rule selection
An explicit
[tool.ruff.lint] selectreplaces ruff's default rule set rather than extending it, so this repo was opting out of the 413 rules 0.16 enables by default. Ruff has noDEFAULTselector token that could name that set explicitly, so leavingselectunset and listing this project's own linters underextend-selectis the only way to get defaults plus extras. Every entry sits on its own line with a trailing comment naming the linter it selects.Expanding to whole prefixes instead was rejected: that drags in all of
D,PL,Sand friends rather than ruff's curated subset, and is dramatically noisier for no gain in signal.Fixed
RUF036(none-not-at-end-of-union) — stabilized out of preview in 0.16.0;keygetter's return annotation now closes its union withNone.PYI034(non-self-return-type) —ControlMode.__enter__was annotated with the concrete class, so a subclass used as a context manager inferred asControlModeand lost its own attributes inside thewithblock. It returnsSelfnow, imported fromtyping_extensionsunderTYPE_CHECKINGthe way the rest of the package handles the 3.10 floor.PLE0101(return-in-init) — the exception constructors inexc.py,_internal/query_list.py, and the vendoredversion.pywrotereturn super().__init__(...), which relies on the base returningNoneby accident and hides which branch is early-exit control flow. The call is a statement now, with a barereturnwhere the construct was doing control flow.PLW1508(invalid-envvar-default) — the retry constants passed numbers asos.getenvdefaults.os.getenvhands its default back unchanged, so the documentedstr | Noneresult only held becauseint()andfloat()accept numbers too.PLR1704(redefined-argument-from-local) — three tests intest_tmuxobject.pytook thesessionfixture and then rebound that name as a loop variable, leaving the fixture object unreachable. The loop variable issession_now.PYI055(unnecessary-type-union) —type[Server] | type[Session] | type[Window] | type[Pane]collapsed totype[Server | Session | Window | Pane].BLE001(blind-except) —test_dataclasses.pycaught every exception aroundserver.sessions[0], where the fallback exists for a server with no sessions. Narrowed toIndexError, so a genuinely brokenServer.sessionsfails the test instead of being papered over.Scoped ignores
Each lands as its own
per-file-ignoresentry with the reason recorded in the config.S102(exec-builtin) indocs/conf.py— Sphinx reads version metadata by exec'ing the package's__about__.py, which keeps the docs build off an import of the package it documents. The input is a file in this repository.PLC0414(useless-import-alias) insrc/libtmux/_internal/query_list.py—ObjectDoesNotExist as ObjectDoesNotExistis PEP 484's explicit re-export form. mypy runs withstrict = true, which turns onno_implicit_reexport; removing the aliases and re-running mypy turns everyfrom libtmux._internal.query_list import ObjectDoesNotExistinto anattr-definederror.BLE001insrc/libtmux/_internal/query_list.py— the lookup operators are predicates run against whatever the caller put in theQueryList, including mappings with a custom__contains__. A comparison that raises is a non-match, not an error to propagate out ofQueryList.filter.BLE001insrc/libtmux/options.py— the option parsers read whatever tmux prints, across every tmux version this library supports. Each handler logs the failure and keeps the raw value, so an option this version of libtmux cannot parse does not take the rest of the object's options with it. Narrowing to the exception types today's tmux happens to produce would turn a future tmux's output into a hard failure.BLE001insrc/libtmux/server.py—Server.is_aliveanswers a yes/no question about a server that may not be there at all. A missing socket, a dead daemon, a missing tmux binary and a crashed subprocess are the same answer. This is the documented lenient half of the pair;Server.raise_if_deadis the primitive for callers who need the reason.Design decisions
Floor, not pin:
>=0.16.0rather than==. The lockfile already reproduces the exact version for this repo; the floor exists to keep a contributor on an older ruff from producing a passing local run that fails CI.Ignores are per-file, never global: every suppression names the file it applies to and carries a comment stating why the construct there is deliberate, so a new blind except in an unrelated module still gets flagged.
Vendored
version.pyis fixed, not exempted: the repo already lints and autofixessrc/libtmux/_vendor/, and the flagged constructor is a local modification rather than upstreampackagingcode.Test plan
uv run ruff check .— cleanuv run ruff format . --check— clean, including Markdown code blocksuv run mypy src tests— cleanuv run pytest— full suite green, including doctests oversrc/,docs/, andREADME.md