Skip to content

py(deps) ruff 0.15.22 -> 0.16.0 - #568

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

py(deps) ruff 0.15.22 -> 0.16.0#568
tony merged 18 commits into
masterfrom
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.
  • Adopt ruff 0.16.0's Markdown formatting: ruff format now formats Python code blocks inside .md files by default.
  • Adopt ruff's default rule set by replacing [tool.ruff.lint] select with extend-select, taking the enabled rule count from 351 to 565.
  • Fix every finding that surfaced, one commit per rule, and scope the two deliberate idioms as per-file ignores that carry their reason in the config.

Adopting the default rule set

ruff 0.16 ships a curated default rule set as its recommended baseline. An explicit select replaces that set rather than extending it, so this project was running only the linters it named and silently opting out of the rest. Renaming select to extend-select layers the project's own linters on top of the default set instead of in place of it, and every entry now carries a trailing comment naming the linter it selects.

select stays unset deliberately, and the config says so. RuleSelector has no DEFAULT token, so an explicit select cannot name the default set — leaving it unset is the only way to get defaults-plus-extras. Expanding to whole prefixes instead was rejected: it drags in all of D, PL, and S rather than the curated subset.

Bumping the floor rather than pinning an exact version keeps the lockfile the single source of the resolved version while still refusing anything older than the release whose formatter output and default rule set this repo is now checked against.

Rules fixed

Each landed as its own commit.

  • PYI034PrivatePath.__new__ and two __enter__ methods were annotated with their own concrete class, so a subclass instance was inferred as the base. They return Self now. typing.Self needs 3.11 and the package floor is 3.10, so Self is imported from typing_extensions under TYPE_CHECKING; the runtime dependency smoke test still passes with only the declared runtime deps installed.
  • PYI036__exit__ tail parameters typed as types.TracebackType | None and object rather than t.Any.
  • PLE0101CouldNotGuessVCSFromURL.__init__ returned the result of super().__init__(...). A constructor may only return None.
  • DTZ005 — the per-invocation debug log file name was stamped from a naive datetime.now(), whose meaning depends on the ambient system zone. .astimezone() attaches the local zone without changing the digits in the name.
  • ISC004 — two multi-part strings sitting unparenthesized inside a list and a tuple. Wrapping them keeps a stray comma from silently splitting one value into two.
  • PLW1510 — two test probes deliberately let git fail so they can assert on returncode; they now say check=False.
  • TC005 — an if t.TYPE_CHECKING: block holding nothing but pass, plus the typing import that existed only to guard it.
  • PIE790 — a pass trailing a method whose body was already a docstring.
  • EXE001src/vcspull/__init__.py carried #!/usr/bin/env python despite being a package initializer that is never run as a script and is not executable. Note that ruff suppresses this rule under WSL, where the executable bit is unreliable, so it surfaces on CI but not on a WSL checkout.

Ignores added

Each landed as its own commit, with the reason written into pyproject.toml next to the entry.

  • S102 in docs/conf.py — the Sphinx idiom of execing src/vcspull/__about__.py to read version, title, and repo URLs without importing (and therefore installing) the package. The exec'd input is a file in this repository, not user data.
  • BLE001 in four files, each a boundary where the broad catch is the point rather than an oversight. scripts/runtime_dep_smoketest.py exists to report arbitrary import and CLI failures. src/vcspull/cli/_progress.py restores the terminal cursor from an atexit handler, whose raise the interpreter would swallow silently. src/vcspull/cli/sync.py catches inside a worker thread to hand the failure back to the main thread, which decides what to do with it. src/vcspull/log.py keeps a caller's %-format failure from taking the application down with the log formatter. Every one of these sites already carried a comment saying so.

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 passes
  • uvx --isolated --no-cache --from . python scripts/runtime_dep_smoketest.py — clean, confirming the typing_extensions import stays type-checking-only

tests/cli/test_sync_progress.py::test_stop_repo_panel_with_no_final_line_still_collapses is intermittently flaky — it races the running spinner thread against the test's direct writes to the indicator's internals. It reproduces at the same rate with this branch's change to that file reverted, so it predates this work and is not addressed here.

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 code blocks inside Markdown by default,
so `ruff format . --check` now fails CI on the hand-aligned inline
comments in this doc.

what:
- Collapse comment padding to the two-space gap ruff format emits

https://astral.sh/blog/ruff-v0.16.0
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 83.72%. Comparing base (40109c5) to head (b042904).

Files with missing lines Patch % Lines
src/vcspull/cli/sync.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #568   +/-   ##
=======================================
  Coverage   83.72%   83.72%           
=======================================
  Files          32       32           
  Lines        4596     4596           
  Branches      928      928           
=======================================
  Hits         3848     3848           
  Misses        497      497           
  Partials      251      251           

☔ 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 15 commits July 26, 2026 13:26
why: The unreleased entry tracks dev-tooling floors so contributors know
which ruff their formatter output is checked against.

what:
- Add a `### Development` deliverable for the `ruff>=0.16.0` floor and
  the new 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 4660181.
why: ruff 0.16 ships a curated default rule 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
rest. `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: `MockHTTPResponse.__exit__` already has a docstring as its body, so
the trailing `pass` is dead weight that ruff's default rule set now
flags.

what:
- Remove the `pass` placeholder from `MockHTTPResponse.__exit__`

https://docs.astral.sh/ruff/rules/unnecessary-placeholder/
why: The `if t.TYPE_CHECKING:` block held nothing but `pass`, so the
module carried a `typing` import solely to guard an empty branch.

what:
- Remove the empty type-checking block
- Drop the now-unused `import typing as t`

https://docs.astral.sh/ruff/rules/empty-type-checking-block/
why: A multi-part string sitting unparenthesized inside a list or tuple
reads the same as a missing comma between two separate elements. Wrapping
each one makes the joins explicit and keeps a stray comma from silently
splitting the value into two.

what:
- Parenthesize the label-matching regex in the output lexer's token table
- Parenthesize the sync summary line built inside the `parts` list

https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/
why: Both call sites deliberately let git fail so the test can inspect
`returncode` itself -- one asserts a clean-environment commit succeeds
with git's stderr in the message, the other probes whether an `origin`
remote exists. Spelling `check` out separates that intent from an
oversight.

what:
- Pass `check=False` to the `git commit` probe in the fixture test
- Pass `check=False` to the `git remote get-url origin` probe

https://docs.astral.sh/ruff/rules/subprocess-run-without-check/
why: `CouldNotGuessVCSFromURL.__init__` returned the result of
`super().__init__(...)`. A constructor may only return `None`, so the
`return` conveyed nothing and would raise `TypeError` if the base ever
returned a value.

what:
- Call `super().__init__(...)` as a statement instead of returning it

https://docs.astral.sh/ruff/rules/return-in-init/
why: The per-invocation debug log file name is stamped from
`datetime.now()`, which returns a naive value whose meaning depends on
the ambient system zone. Attaching the local zone keeps the file name
digits the reader expects while making the instant unambiguous.

what:
- Chain `.astimezone()` onto `datetime.now()` in `default_debug_log_path`
- Teach the timestamp test double to answer `astimezone`

https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/
why: Both `__exit__` implementations typed their tail parameters as
`t.Any`, which accepts anything and tells a reader nothing about the
protocol. Naming the real shapes lets a type checker catch a caller
that passes the wrong triple.

what:
- Type `_RepoContext.__exit__`'s traceback as `types.TracebackType | None`
- Type `MockHTTPResponse.__exit__`'s star-args as `object`

https://docs.astral.sh/ruff/rules/bad-exit-annotation/
why: `PrivatePath.__new__` and the two `__enter__` methods were
annotated with their own concrete class, so a subclass instance was
inferred as the base -- `with SubContext(...) as ctx` lost every
attribute the subclass added. `Self` binds the return to the actual
class.

what:
- Return `Self` from `PrivatePath.__new__`
- Return `Self` from `_RepoContext.__enter__` and
  `MockHTTPResponse.__enter__`
- Import `Self` from `typing_extensions` under `TYPE_CHECKING`, since
  `typing.Self` needs 3.11 and the package floor is 3.10

https://docs.astral.sh/ruff/rules/non-self-return-type/
why: docs/conf.py reads the version, title, and repo URLs by exec'ing
src/vcspull/__about__.py -- the standard Sphinx way to pull metadata
without importing (and therefore installing) the package. The exec'd
input is a file in this repository, not user data.

what:
- Per-file-ignore S102 for docs/conf.py, with the reason in the config

https://docs.astral.sh/ruff/rules/exec-builtin/
why: Every blind `except` left in the tree guards a boundary where the
alternative is worse than a broad catch: an atexit handler whose raise
the interpreter swallows, a sync worker thread that must hand its
failure back to the main thread, a log formatter that would take the
application down, and a smoke test whose entire purpose is reporting
arbitrary import failures. Each already carries a comment saying so.

what:
- Per-file-ignore BLE001 for the four files, each with its reason in
  the config

https://docs.astral.sh/ruff/rules/blind-except/
why: The ruff 0.16 entry claimed lint diagnostics were unchanged, which
stopped being true once the project stopped replacing ruff's default
rule set.

what:
- Describe the default rule set adoption and the two scoped ignores
- Drop the "lint diagnostics are unchanged" claim
why: src/vcspull/__init__.py carried `#!/usr/bin/env python` but is a
package initializer, never run as a script, and is not executable. The
shebang promises something the file cannot do.

what:
- Remove the shebang line from the package initializer

https://docs.astral.sh/ruff/rules/shebang-not-executable/
@tony
tony merged commit 5b68459 into master Jul 26, 2026
8 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