Skip to content

fix: bound ruff to >=0.16,<0.17, make the lint gate real, fix the defects it found - #9

Merged
abrichr merged 1 commit into
mainfrom
chore/bound-ruff-explicit-lint
Jul 28, 2026
Merged

fix: bound ruff to >=0.16,<0.17, make the lint gate real, fix the defects it found#9
abrichr merged 1 commit into
mainfrom
chore/bound-ruff-explicit-lint

Conversation

@abrichr

@abrichr abrichr commented Jul 28, 2026

Copy link
Copy Markdown
Member

What this fixes

pyproject.toml declared ruff>=0.1.0 (unbounded) and [tool.ruff] set only line-length = 100 — no target-version, no select. That means the rule set this repo is linted against was whatever the resolved ruff happened to default to. ruff 0.16.0 grew its default set from 59 rules to 413. uv.lock pinned 0.14.13, so nothing had noticed yet.

The CI lint step could not have noticed either. Verbatim, before:

      - name: Run ruff linter (check)
        run: uv run ruff check src/openadapt_viewer/ || true
        continue-on-error: true

      - name: Run ruff formatter (check)
        run: uv run ruff format --check src/openadapt_viewer/ || true
        continue-on-error: true

|| true and continue-on-error: true — belt and braces. This gate has never enforced anything. At the moment it was made real the tree had 261 findings.

The rule-set decision: keep the full default set

Not narrowed. The residual after safe autofix was 102 findings across a 13.8k-line tree — small enough to read one by one, and reading them is what surfaced the defects below. A select = ["E","F","I","W"] fallback would have reported 57 findings and found none of them. Predictability comes from the bound instead: ruff>=0.16,<0.17, with the reasoning inline in pyproject.toml. ruff only grows its default set in a minor release, so the rule set cannot move until a person raises that ceiling and owns the new findings in the same change.

target-version = "py310" is now explicit rather than inferred from requires-python.

The fake gate

ruff check is now a blocking ruff check . over the whole repository (src, tests, scripts, root-level scripts) — that is the scope the residual was cleared against, and it costs no extra CI time.

ruff format --check is deleted, not made real. 90 of 143 files (31 of 41 under src/) do not match ruff format today. Making it blocking here would mean reformatting the tree inside a lint-configuration change and burying every real fix in whitespace. A check that cannot fail is worse than no check — it reads as coverage that does not exist — so it is gone until someone does the reformat as its own reviewable commit. The reasoning is in the workflow file.

Real defects (not cosmetic)

Site Was What a user saw
src/openadapt_viewer/scanner.py:229 bare except: pass (E722+S110) An unparseable processing_timestamp silently substituted the file's mtime as the segmentation date. Also swallowed KeyboardInterrupt.
src/openadapt_viewer/builders/page_builder.py:314
src/openadapt_viewer/viewers/capture/generator.py:411
except Exception: pass (S110+BLE001) An unreadable packaged core.css fell back to a truncated inline stylesheet in total silence — the page rendered with visibly different styling and nothing said why.
scripts/generate_segmentation_screenshots.py:499
scripts/generate_comprehensive_screenshots.py:420
return 0 unconditionally (found via BLE001) A run in which every scenario raised still printed ✓ Screenshot generation completed successfully! and exited 0. cli.run_screenshots_command propagates that status verbatim, so openadapt-viewer screenshots reported success having produced nothing.
scripts/generate_segmentation_screenshots.py:345,368 except Exception: pass (S110) A failed page.click still produced a screenshot named "search focused" showing an unfocused box, or "recording filter open" showing a closed dropdown, and reported it as generated. Sibling handlers in the same file already warned.
src/openadapt_viewer/viewers/benchmark/real_data_loader.py:127 .lstrip("../") (B005) Character-set strip where a prefix strip was meant. Same result under the guarding startswith, but wrong for any other input. Now removeprefix("../").
test_minimal_viewer.py:123,128 exit(1) (PLR1722) The site-injected builtin, absent under python -S and in frozen builds, where this would have died with NameError instead of exiting 1.

Straight answers on the categories you asked about

  • 21 PLW1510 — none hid a defect. 20 are in tests and 1 is cli.py:422; every one of the 21 inspects result.returncode explicitly, so check=False is what the surrounding code wants and raising would break them. Added explicitly with a one-line note per file.
  • 5 S110 + 1 E722 — this is where the bugs were. 5 of the 6 are listed in the table above (scanner bare-except, two CSS fallbacks, two silent playwright clicks). The 6th is the except Exception as e on the scanner's own S110 pair. All narrowed to concrete exception tuples plus a warning; none silenced.
  • 3 F841 — all dead, none a lost result. stats_parser/clean_parser are argparse subparsers that take no arguments of their own; name in run_catalog_command recomputed exactly what scanner._extract_recording_info already derives, and args.name is applied to recording.name two lines later. Assignments dropped.
  • 21 BLE001 — 15 narrowed to real exception tuples (sqlite3.Error, OSError, ValueError, playwright.sync_api.Error, …), each with an inline note on what it covers and why. 6 kept as deliberate # noqa: BLE001: three top-of-process handlers and three per-item loop handlers that must not abandon a batch — those now record the failure so the exit status is honest. No repo-wide ignore = ["BLE001"].

Did any DTZ fix move a timestamp a user sees? No.

Every one of the 40 naive-datetime sites was fixed with a trailing .astimezone(), which attaches the machine's local zone without moving the wall clock. This was the point: a viewer shows the time a recording happened on this machine, and converting to UTC would have silently shifted what the user reads.

Two timestamps are actually rendered to users — created_at_formatted in the segmentation catalog, and the Created: column of openadapt-viewer catalog list. Both go through strftime with no %z, so their output is byte-identical before and after. tests/test_scanner_and_catalog.py::TestTimestampsRenderUnchanged pins that against the literal pre-change expression.

Only .isoformat() output changes, gaining a +HH:MM suffix — the generated_at metadata keys. That makes an ambiguous emitted value unambiguous, not different.

DataLoader.parse_datetime now returns consistently aware values for parsed input (offset-bearing ISO strings are left alone; naive ones are anchored to local without moving). The isinstance(datetime) passthrough is untouched, since its docstring and test_parse_datetime_object define it as identity.

Rules disabled

One entry, narrowly scoped:

[tool.ruff.lint.per-file-ignores]
"src/openadapt_viewer/__init__.py" = ["RUF022"]
"src/openadapt_viewer/components/__init__.py" = ["RUF022"]

Both __all__ lists are grouped by subsystem with # Core / # Components / # Builders / # Viewers / # Catalog headings; an alphabetical sort strands every heading against unrelated names. Justification is inline.

Plus 6 inline # noqa: BLE001 and 2 inline # noqa: DTZ006, each with a reason on the adjacent line. The DTZ ones are in the test that deliberately uses the pre-change naive expression as its expected value.

Tests

tests/test_scanner_and_catalog.py (new, 11 tests) and 2 tests appended to tests/test_segmentation_screenshots.py. Verified against origin/main in a throwaway worktree: 5 of them fail on the pre-change code (the scanner warning, the KeyboardInterrupt passthrough, both CSS-fallback warnings, and the screenshot exit status), and all pass here.

Lockfile

uv lock --upgrade-package ruff moved ruff 0.14.13 -> 0.16.0. revision was already 3, so there is no format churn from this change.

There is other lockfile movement, and it is not from the ruff bump: the committed uv.lock was already stale against pyproject.toml. imageio — a declared runtime dependency — and its numpy transitives were absent from the lock entirely, and the project was recorded at 0.1.0 while pyproject.toml says 0.2.0. Any uv sync regenerates this, including the one CI runs on every job, so CI has been silently relocking. No existing dependency version changed — the only diffs are ruff 0.14.13 -> 0.16.0, openadapt-viewer 0.1.0 -> 0.2.0, and the addition of imageio 2.37.4 + numpy.

Verification (local, ruff 0.16.0)

uv run ruff check src/openadapt_viewer/   ->  All checks passed!
uv run ruff check .                       ->  All checks passed!
uv run pytest tests/ -v                   ->  145 passed, 44 skipped   (was 132 passed, 44 skipped)

ruff format --check src/openadapt_viewer/ still reports 31 files — pre-existing, untouched, and the reason that step is deleted rather than made blocking.

Release

No release fires from this. .github/workflows/release.yml is on: workflow_dispatch only as of #8, so the fix: prefix has no effect here; a release only happens when someone runs the workflow by hand. fix: is used because this PR fixes real defects, not just configuration.

Out of scope, but worth someone's attention

src/openadapt_viewer/viewers/benchmark/real_data_loader.py:126 hardcodes an absolute developer home path (/Users/abrichr/oa/src) in shipped library code, so those screenshot paths resolve on exactly one machine. Not a lint finding and not touched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

`ruff>=0.1.0` was unbounded and `[tool.ruff]` set only `line-length`, so this
repo inherited whatever default rule set the resolved ruff happened to ship.
ruff 0.16.0 grew that set from 59 rules to 413. This bounds the dependency to
`>=0.16,<0.17`, pins `target-version = "py310"`, keeps the FULL default set
deliberately, and clears all 261 findings.

The CI lint step could not fail -- it carried `|| true` AND
`continue-on-error: true` -- so it had never enforced anything. It is now a
blocking `ruff check .` over the whole repository. The companion
`ruff format --check` step is deleted rather than made real: 90 of 143 files
do not match `ruff format`, and reformatting belongs in its own commit.

Real defects the sweep exposed, all fixed with regression tests:

* scanner._extract_segmentation_info swallowed an unparseable
  `processing_timestamp` with a bare `except: pass`, silently substituting the
  file's mtime as the segmentation date -- and swallowing KeyboardInterrupt.
* Both `_get_core_css` implementations fell back to a truncated inline
  stylesheet in total silence when the packaged core.css could not be read, so
  a page rendered with visibly different styling and nothing said why.
* Both screenshot scripts returned 0 unconditionally. A run in which every
  scenario raised still printed "completed successfully" and exited 0, and
  `openadapt-viewer screenshots` propagates that status verbatim.
* Two playwright interactions used `except Exception: pass`, so a screenshot
  named "search focused" / "recording filter open" could show an unfocused box
  or a closed dropdown and be reported as generated.
* real_data_loader used `.lstrip("../")`, a character-set strip, where a prefix
  strip was meant.
* test_minimal_viewer.py called the `site`-injected builtin `exit`, absent
  under `python -S` and in frozen builds.

No timestamp a user reads was moved: every naive datetime was fixed with a
trailing `.astimezone()`, which anchors the local zone without changing the
wall clock. The two rendered timestamps (`created_at_formatted` and the
`Created:` column of `catalog list`) go through `strftime` with no `%z`, and a
test pins their output byte-for-byte against the pre-change expression.

`uv lock --upgrade-package ruff` also refreshed a lockfile that was stale
against pyproject.toml: `imageio` (a declared runtime dependency) and its
numpy/pillow transitives were absent, and the project was recorded at 0.1.0.
No existing dependency version changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
@abrichr
abrichr merged commit 743ffa9 into main Jul 28, 2026
5 checks passed
@abrichr
abrichr deleted the chore/bound-ruff-explicit-lint branch July 28, 2026 02:53
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