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` - Reorder the `_FILE` subprocess alias to end in `| None`, matching the identical alias in `_internal/run.py` - Reorder `test_create_project`'s `raises_exception` parameter https://docs.astral.sh/ruff/rules/none-not-at-end-of-union/
why: ruff 0.16.0 formats Python and pycon code blocks inside Markdown by default, so `ruff format . --check` now fails on docs that were never run through the formatter. The churn is mechanical - quote style, argument wrapping, comment spacing, trailing whitespace. what: - Reformat the Python blocks in `README.md` - Reformat the Python blocks in `notes/2025-11-26-command-support.md` https://astral.sh/blog/ruff-v0.16.0
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #549 +/- ##
==========================================
+ Coverage 61.31% 61.34% +0.03%
==========================================
Files 40 40
Lines 6566 6556 -10
Branches 1104 1104
==========================================
- Hits 4026 4022 -4
+ Misses 1946 1940 -6
Partials 594 594 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: The ruff floor changes what contributors need installed and widens what `ruff format` rewrites, so it belongs in the unreleased entry. what: - Add a `#### Ruff moves to 0.16 (#549)` 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 7790836.
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. `return super().__init__(...)` reads as if the base initializer produced a value worth propagating; it does not, and the form masks a genuine mistake if one is ever introduced there. what: - Call `super().__init__(...)` as a statement in every exception class https://docs.astral.sh/ruff/rules/return-in-init/
why: `_TXT` was a private type alias with no annotation referencing it, so it carried no meaning for readers or type checkers. what: - Remove the `_TXT` type alias from `libvcs._internal.subprocess` https://docs.astral.sh/ruff/rules/unused-private-type-alias/
why: The `timestamp` handed to a progress callback came from a naive `datetime.now()`, so it carried the host's local wall clock with no offset attached. Two hosts in different zones produced timestamps that compare and serialize as if they were the same instant, and a DST rollover makes them ambiguous or non-monotonic on a single host. what: - Pass `tz=datetime.timezone.utc` to every `datetime.now()` feeding `ProgressCallbackProtocol` https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/
why: `subprocess.run` defaults to `check=False`, so a reader cannot tell whether a call site meant to ignore the exit status or forgot to check it. Both of these deliberately inspect `returncode` themselves to attach stderr to the assertion message. what: - Pass `check=False` at the two call sites that assert on `returncode` https://docs.astral.sh/ruff/rules/subprocess-run-without-check/
why: Each `.gitmodules` lookup passes `check_returncode=False`, which is precisely the switch that stops `run()` raising on a non-zero exit — a missing or unreadable `.gitmodules` already comes back as output text that the following `if` filters out. The wrapping `try`/`except Exception`/`pass` therefore guarded a failure mode the call cannot produce, while silently swallowing any real one. what: - Call `git config` directly for submodule name, url, and branch The same deletion clears the blind-except at these sites. https://docs.astral.sh/ruff/rules/try-except-pass/ https://docs.astral.sh/ruff/rules/blind-except/
why: `docs/conf.py` reads `__title__`, `__version__`, and friends by exec'ing `src/libvcs/__about__.py` into a dict. That is deliberate: it keeps the docs build from importing libvcs, so version metadata resolves even when the package itself is not installed. The exec'd file is this repository's own source, not untrusted input. what: - Per-file-ignore S102 for `docs/conf.py`, with the reason in the config https://docs.astral.sh/ruff/rules/exec-builtin/
why: `run()` reproduces `subprocess.Popen`'s signature so callers can reach any of it, and `preexec_fn` is one of those parameters. It defaults to None, so libvcs never forks with a preexec hook on its own; the thread-safety hazard the rule warns about belongs to a caller that opts in. Removing the parameter would break that caller with nothing to replace it. what: - Per-file-ignore PLW1509 for `src/libvcs/_internal/run.py`, with the reason in the config https://docs.astral.sh/ruff/rules/subprocess-popen-preexec-fn/
why: QueryList filters arbitrary caller-supplied objects. Resolving a `food__istartswith` path means walking `__getattr__`, properties, and `__eq__` that libvcs did not write and cannot enumerate the failure modes of. An unresolvable lookup is a non-match, not an error, so `keygetter` and `parse_lookup` log at debug and return None while the `in`/`nin` predicates return False. Narrowing the handlers would let a foreign property abort an entire filter. what: - Per-file-ignore BLE001 for `src/libvcs/_internal/query_list.py`, with the reason in the config https://docs.astral.sh/ruff/rules/blind-except/
why: `SvnSync.url_rev` falls back to shelling out to `svn info --xml` and scraping the result when neither the `.svn/entries` nor the XML layout applies. What that can raise depends on the installed svn, the working copy layout, and the output format — a `CommandError`, a failed match, a non-integer revision. The method's contract is to answer `(None, 0)` for a working copy it cannot read, so the fallback absorbs all of them. what: - Per-file-ignore BLE001 for `src/libvcs/sync/svn.py`, with the reason in the config https://docs.astral.sh/ruff/rules/blind-except/
why: Adopting ruff's default set changes what `just ruff` enforces for contributors, and the progress-callback timestamp change is visible to anyone who compares or serializes that value. what: - Extend the ruff 0.16 deliverable with the `extend-select` switch - Add a `### Fixes` deliverable for tz-aware callback timestamps
why: `libvcs.sync.svn` is imported, never executed — it has no `__main__` guard and ships without the executable bit — so the `#!/usr/bin/env python` line advertised an entry point that does not exist. The rule is skipped on WSL, so it only surfaced on CI's Linux runners. what: - Remove the shebang from `src/libvcs/sync/svn.py` 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
devandlintdependency groups and relocking.selecttoextend-select, taking the enabled-rule count from 351 to 565.README.mdandnotes/, whichruff formatnow rewrites by default.Changes
Dependencies
pyproject.toml raises
ruffto>=0.16.0in both thedevandlintgroups, and uv.lock moves ruff 0.15.22 to 0.16.0. A floor rather than a pin, so contributors and CI agree on the diagnostics without freezing patch releases out.Rule selection
ruff 0.16 ships a curated 413-rule default set as its recommended baseline. An explicit
selectreplaces that set rather than extending it, so naming linters there silently opted the project out of everything ruff enables by default.RuleSelectorhas noDEFAULTtoken, so there is no way to name the default set from insideselect; leavingselectunset and listing the project's own linters underextend-selectis the only way to get defaults-plus-extras.pyproject.toml makes that switch with the same entries, one per line, each labelled with the linter it selects, and records in the file why
selectstays unset. Enabled rules go from 351 to 565.Lint fixes
src/libvcs/_internal/query_list.py, src/libvcs/_internal/subprocess.py, and tests/test_shortcuts.py move
Noneto the end of their type unions (RUF036). Annotation-only reordering with no runtime or type-checking effect —int | IO[Any] | NoneandNone | int | IO[Any]denote the same type. The_FILEalias insubprocess.pynow matches the identically named alias in_internal/run.py, which already orderedNonelast.Every exception class calls
super().__init__(...)as a statement instead of returning it (PLE0101).__init__must return None; the old form read as if the base initializer produced a value worth propagating.ProgressCallbackProtocolreceives an aware UTCdatetimerather than a naive local one (DTZ005). Naive timestamps taken on hosts in different zones compared and serialized as though they named the same instant, and a DST rollover made them ambiguous on a single host. Callbacks that only format the value for display are unaffected.GitSubmoduleManager._lsreads submodule name, url, and branch from.gitmodulesdirectly (S110,BLE001). Each lookup already passescheck_returncode=False, which is exactly the switch that stopsrun()raising on a non-zero exit, so thetry/except Exception/passaround it guarded a failure mode the call cannot produce while swallowing any real one._TXTis gone fromlibvcs._internal.subprocess(PYI047) — a private type alias with no annotation referencing it. The twosubprocess.runcalls in the test suite that assert onreturncodethemselves now saycheck=False(PLW1510).libvcs.sync.svnloses its#!/usr/bin/env pythonline (EXE001). The module is imported, never executed — no__main__guard, no executable bit — so the shebang advertised an entry point that does not exist. This one is only visible on CI: ruff skips the rule on WSL, where it was authored.Scoped ignores
Four rules fire on constructs that are deliberate here. Each is scoped to the file it applies to, never repo-wide, with the reason recorded next to the entry in
pyproject.toml.S102in docs/conf.py: the Sphinx config reads__title__,__version__, and friends by exec'ingsrc/libvcs/__about__.pyinto a dict, which keeps the docs build from importing libvcs so version metadata resolves even when the package is not installed. The exec'd file is this repository's own source.PLW1509in src/libvcs/_internal/run.py:run()reproducessubprocess.Popen's signature so callers can reach any of it,preexec_fnincluded, and defaults it to None. libvcs never forks with a preexec hook on its own; the thread-safety hazard belongs to a caller that opts in.BLE001in src/libvcs/_internal/query_list.py: QueryList filters arbitrary caller-supplied objects, so resolving afood__istartswithpath walks__getattr__, properties, and__eq__that libvcs did not write. An unresolvable lookup is a non-match, not an error —keygetterandparse_lookuplog at debug and return None while thein/ninpredicates return False. Narrowing the handlers would let a foreign property abort an entire filter.BLE001in src/libvcs/sync/svn.py:SvnSync.url_revfalls back to shelling out tosvn info --xmland scraping the result when neither the.svn/entriesnor the XML layout applies. What that can raise depends on the installed svn, the working copy layout, and the output format. The method's contract is to answer(None, 0)for a working copy it cannot read.Formatting
README.md and notes/2025-11-26-command-support.md get their Python blocks normalized: double quotes, standard argument wrapping, single-space comment gaps, trailing whitespace removed. All of it is mechanical output of
ruff format, which in 0.16.0 descends into Markdown fences by default.ruff format . --checkin CI fails without it.Design decisions
Expanding to whole prefixes instead of the curated default set was rejected: it is one to two orders of magnitude noisier, because it drags in all of
D,PL,S, and friends rather than ruff's own recommended subset.Each rule lands as its own commit — one per fix, one per ignore — so a disagreement over any single disposition can be reverted without unpicking the rest. Ignores carry their justification in
pyproject.tomlrather than as# noqacomments in the code.The
_FILEalias fix was applied by hand because ruff classifies it as an unsafe fix — the alias is a runtime assignment, not an annotation, so ruff cannot rule out code that inspectstyping.get_args()ordering. Nothing in the tree does; the alias is referenced only from annotations and docstrings.The
notes/reformat was accepted rather than excluded from the formatter. It flattens some hand-aligned trailing comments in a design note, but carvingnotes/out ofruff formatis a policy change that belongs in its own PR, not in a version bump.Test plan
uv run ruff check .— cleanuv run ruff format . --check— cleanuv run mypy— cleanuv run py.test— greenThe
Git.helpdoctest callsgit help --all, which intermittently hangs on the author's machine regardless of this branch:run()does not drain the child's pipes when notimeoutis given, so the child blocks on write while the parent spins in its poll loop. It reproduces identically on an unmodifiedmastercheckout, and it is on a code path this branch does not touch. Pre-existing and out of scope here; CI is the authority on that test.