Skip to content

Commit a91f144

Browse files
committed
ruff: format + lint as a first-class gate
Lands the ruff half of the Python toolchain (plan \xc2\xa7B2). Config: [tool.ruff] in //:pyproject.toml \xe2\x80\x94 line-length 100, target py314, src = [meta, tools], select E/F/I/B/UP/SIM/RUF/S. Per-file ignores carve out S603/S607 for meta/scripts (subprocess calls to first-party tools by name) and S101/S105/S106/S108/S311 for tests. Where ruff runs: - Pre-commit: ruff-check --fix then ruff-format (fix order matters; check re-sorts imports, format finishes). - CI: new `ruff` job mirrors golangci-lint\xe2\x80\x99s shape, uses astral-sh/ruff-action with version pinned alongside the Dockerfile ARG. Added to build-and-test-per-target.needs to gate merge. - Devcontainer: installed via uv tool install during image build. UV_TOOL_DIR is forced to /usr/local/share/uv-tools so the root-owned install is reachable by the vscode user. - VS Code: charliermarsh.ruff in extensions.json + devcontainer.json; settings.json sets ruff as the default Python formatter with source.fixAll.ruff + source.organizeImports.ruff as save-time actions. Renovate: new regex matcher for .github/workflows/*.yml picks up the `version:` input under `# renovate: ...` (mirrors the Dockerfile pattern), and a ruff packageRule groups both pins into one PR. Existing meta/scripts/*.py reformatted by `ruff format` and the four real findings fixed (SIM102 nested-if, two E501 long error messages, SIM115 NamedTemporaryFile-without-with). Test files\xe2\x80\x99 RUF005 fixed inline. README: new ruff row in the CI checks table, ruff-check / ruff-format / uv-lock-fresh rows in the pre-commit table, charliermarsh.ruff in the recommended extensions, and a `ruff (diagnostics + format)` row in the on-save table.
1 parent 750aac2 commit a91f144

19 files changed

Lines changed: 238 additions & 118 deletions

.devcontainer/Dockerfile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ ARG BUILDIFIER_VERSION=v8.5.1
99
# uv: the workspace's Python package manager. Single source of truth for the
1010
# uv.lock + requirements_lock.txt chain consumed by rules_python's pip.parse
1111
# and enforced by the `uv-lock-fresh` pre-commit hook. Installed first so
12-
# later PRs can layer `uv tool install ruff ty pre-commit` on top of it.
12+
# later layers can layer `uv tool install ruff ty pre-commit` on top of it.
1313
# Pinned tag bumped by Renovate's docker manager (matches the pattern used in
1414
# ~/.dotfiles/.devcontainer/Dockerfile).
1515
COPY --from=ghcr.io/astral-sh/uv:0.11.21 /uv /uvx /usr/local/bin/
1616

17+
# ruff: format + lint. Installed via `uv tool install` so the binary lives in
18+
# /usr/local/bin and `ruff` is on PATH for the pre-commit hooks and the VS Code
19+
# extension. UV_TOOL_DIR is forced to a world-readable system location because
20+
# the install runs as root; the default ~/.local/share/uv would land under
21+
# /root (mode 700) and the vscode user could not follow the bin/ symlink.
22+
# Renovate's regex manager tracks RUFF_VERSION via the comment above the ARG.
23+
# renovate: datasource=pypi depName=ruff
24+
ARG RUFF_VERSION=0.15.17
25+
ENV UV_TOOL_BIN_DIR=/usr/local/bin \
26+
UV_TOOL_DIR=/usr/local/share/uv-tools
27+
RUN uv tool install --no-cache "ruff==${RUFF_VERSION}"
28+
1729
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
1830
build-essential \
1931
ca-certificates \

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"vscode": {
5656
"extensions": [
5757
"bazelbuild.vscode-bazel",
58+
"charliermarsh.ruff",
5859
"cnshenj.vscode-task-manager",
5960
"esbenp.prettier-vscode",
6061
"github.vscode-github-actions",

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ jobs:
7979
with:
8080
working-directory: ${{ matrix.module }}
8181

82+
# Python's sibling of golangci-lint. Format + lint in one job; config lives in
83+
# `[tool.ruff]` in //:pyproject.toml. Renovate's regex manager tracks the
84+
# version pin via the comment above (matches the Dockerfile pattern).
85+
ruff:
86+
name: ruff
87+
runs-on: ubuntu-latest
88+
steps:
89+
- uses: actions/checkout@v6
90+
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
91+
with:
92+
# renovate: datasource=pypi depName=ruff
93+
version: "0.15.17"
94+
args: format --check
95+
- run: ruff check
96+
8297
# Build and test runs once per supported target platform, on a runner whose host matches
8398
# the target. (Running tests natively per platform is the only way (without an emulation
8499
# layer we do not have) to actually exercise platform-specific code paths and catch regressions
@@ -97,6 +112,7 @@ jobs:
97112
secrets-check,
98113
no-cgo-check,
99114
golangci-lint,
115+
ruff,
100116
]
101117
strategy:
102118
fail-fast: false

.pre-commit-config.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ repos:
1818
pass_filenames: false
1919
files: ^(pyproject\.toml|uv\.lock|requirements_lock\.txt)$
2020

21+
# ruff check first (with --fix), then ruff format — check's import
22+
# sorting can produce output the formatter wants to retouch. Both fix in
23+
# place; pre-commit re-flags modified files so the user re-stages.
24+
- id: ruff-check
25+
name: ruff check --fix
26+
language: system
27+
entry: ruff check --fix
28+
pass_filenames: false
29+
files: \.py$
30+
31+
- id: ruff-format
32+
name: ruff format
33+
language: system
34+
entry: ruff format
35+
pass_filenames: false
36+
files: \.py$
37+
2138
- id: gazelle
2239
name: gazelle
2340
language: system

.vscode/extensions.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"recommendations": [
33
"bazelbuild.vscode-bazel",
4+
"charliermarsh.ruff",
45
"esbenp.prettier-vscode",
56
"github.vscode-github-actions",
67
"golang.go",

.vscode/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
"prettier.proseWrap": "always",
1919
"prettier.trailingComma": "all",
2020
"python.languageServer": "Default",
21+
"// Ruff format-on-save": "charliermarsh.ruff surfaces diagnostics inline; this block makes it the default Python formatter so `editor.formatOnSave` calls ruff format, and runs ruff check --fix + import-organize as save-time code actions. Config is read from `[tool.ruff]` in //:pyproject.toml — single source of truth shared with CI + pre-commit.",
22+
"[python]": {
23+
"editor.defaultFormatter": "charliermarsh.ruff",
24+
"editor.codeActionsOnSave": {
25+
"source.fixAll.ruff": "explicit",
26+
"source.organizeImports.ruff": "explicit"
27+
}
28+
},
2129
"// Task Manager favorites": "Pins the Bazel build/test tasks to the cnshenj.vscode-task-manager sidebar (recommended in the devcontainer; see devcontainer.json). Names must match the `label` fields in tasks.json. The `UnnaturalDesigns/Workspace/` prefix is Task Manager's own scoping convention.",
2230
"taskManager.favorites": [
2331
"UnnaturalDesigns/Workspace/Bazel Build all",

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Two GitHub Actions workflows run on every push and pull request to `main`.
159159
| Secrets check | Always - verifies the `secrets/` directory contains no committed files |
160160
| No-cgo policy check | Always - rejects `import "C"` and transitive deps that compile C/C++/cgo/SWIG |
161161
| golangci-lint | After module check passes - runs per Go module |
162+
| ruff | Always - `ruff format --check` and `ruff check` over all Python |
162163
| Build and test | After all checks above pass |
163164
| Coverage | After build and test - `bazel coverage //...`, uploads merged lcov to Codecov |
164165
@@ -181,23 +182,29 @@ each commit. To install:
181182
pre-commit install
182183
```
183184
184-
Only hooks that either fix the problem they detect (`bazel-mod-tidy`, `gazelle`) or prevent unsafe
185-
content from entering the repo (`check-secrets-dir`) run here. Verification-only checks live in the
186-
editor instead (see **Editor integration** below) so they can surface findings without blocking a
187-
commit when you want to switch contexts.
185+
Only hooks that either fix the problem they detect (`bazel-mod-tidy`, `gazelle`, `uv-lock-fresh`,
186+
`ruff-check`, `ruff-format`) or prevent unsafe content from entering the repo (`check-secrets-dir`)
187+
run here. Verification-only checks live in the editor instead (see **Editor integration** below) so
188+
they can surface findings without blocking a commit when you want to switch contexts.
188189
189-
| Hook | Triggers on |
190-
| ------------------- | ----------------------------- |
191-
| `bazel-mod-tidy` | `go.mod`, `go.work`, `go.sum` |
192-
| `gazelle` | `*.go` files |
193-
| `check-secrets-dir` | files under `secrets/` |
190+
| Hook | Triggers on |
191+
| ------------------- | -------------------------------------------- |
192+
| `bazel-mod-tidy` | `go.mod`, `go.work`, `go.sum` |
193+
| `uv-lock-fresh` | `pyproject.toml`, `uv.lock`, `requirements_lock.txt` |
194+
| `ruff-check` | `*.py` files |
195+
| `ruff-format` | `*.py` files |
196+
| `gazelle` | `*.go` files |
197+
| `check-secrets-dir` | files under `secrets/` |
194198
195199
**Editor integration** (via `.vscode/`) - runs the non-fixing checks on save. Works in VS Code and
196200
VS Code-derived editors (e.g. Google Antigravity). Recommended extensions
197201
(`.vscode/extensions.json`):
198202
199203
- [`golang.go`](https://marketplace.visualstudio.com/items?itemName=golang.go) - runs
200204
`golangci-lint` on save at package scope, surfacing inline findings that match what CI enforces.
205+
- [`charliermarsh.ruff`](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) -
206+
surfaces `ruff check` diagnostics inline and applies `ruff format` on save, matching what the CI
207+
`ruff` job and the pre-commit hooks enforce.
201208
- [`emeraldwalk.runonsave`](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) -
202209
triggers the repo-health scripts on save.
203210
- [`ryanluker.vscode-coverage-gutters`](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) -
@@ -206,6 +213,7 @@ VS Code-derived editors (e.g. Google Antigravity). Recommended extensions
206213
| On-save check | Triggers on |
207214
| -------------------- | ------------------------------------------ |
208215
| `golangci-lint` | `*.go` files |
216+
| `ruff` (diagnostics + format) | `*.py` files |
209217
| `check-go-modules` | `go.mod`, workflow `.yml`, `.golangci.yml` |
210218
| `check-go-work` | `go.mod`, `go.work` |
211219

meta/scripts/_workspace.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
def workspace_root() -> Path:
1515
result = subprocess.run(
1616
["git", "rev-parse", "--show-toplevel"],
17-
capture_output=True, text=True, check=True,
17+
capture_output=True,
18+
text=True,
19+
check=True,
1820
)
1921
return Path(result.stdout.strip())
2022

@@ -27,8 +29,7 @@ def is_skipped(path: Path) -> bool:
2729
that should never contain repo-managed sources.
2830
"""
2931
return any(
30-
part in _SKIP_DIR_NAMES or part.startswith(_SKIP_DIR_PREFIXES)
31-
for part in path.parts
32+
part in _SKIP_DIR_NAMES or part.startswith(_SKIP_DIR_PREFIXES) for part in path.parts
3233
)
3334

3435

@@ -54,7 +55,7 @@ def col_range(file: Path, lineno: int, needle: str) -> tuple[int, int]:
5455
line = file.read_text().splitlines()[lineno - 1]
5556
start = line.index(needle) + 1
5657
return start, start + len(needle)
57-
except (OSError, IndexError, ValueError):
58+
except OSError, IndexError, ValueError:
5859
# bad path / past-EOF lineno / needle-not-on-line all collapse to the same fallback.
5960
return 1, 2
6061

meta/scripts/check_go_modules.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# where rules_python already makes the import resolvable.
2020
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
2121

22-
from meta.scripts._workspace import col_range, found_modules, workspace_root # noqa: E402
22+
from meta.scripts._workspace import col_range, found_modules, workspace_root
2323

2424

2525
def workflow_module_lists(
@@ -52,7 +52,7 @@ def workflow_module_lists(
5252
text = workflow_file.read_text()
5353
result: list[tuple[str, int, dict[Path, int]]] = []
5454

55-
state = "scanning" # scanning | in_matrix | in_module
55+
state = "scanning" # scanning | in_matrix | in_module
5656
matrix_indent = -1
5757
module_indent = -1
5858
module_key_line = -1
@@ -109,9 +109,8 @@ def workflow_module_lists(
109109
module_key_line = lineno
110110
current = {}
111111

112-
elif state == "in_module":
113-
if stripped.startswith("- "):
114-
current[Path(stripped[2:].strip())] = lineno
112+
elif state == "in_module" and stripped.startswith("- "):
113+
current[Path(stripped[2:].strip())] = lineno
115114

116115
# End of file while still inside a module list.
117116
if state == "in_module" and current is not None:
@@ -146,7 +145,10 @@ def check_workflow_matrices(root: Path, modules: set[Path]) -> int:
146145
for mod in sorted(matrix_set - modules):
147146
line = matrix_entries[mod]
148147
start, end = col_range(wf_file, line, str(mod))
149-
print(f"{rel}:{line}:{start}-{end}: [{job_name}] stale matrix entry ./{mod} (no go.mod)")
148+
print(
149+
f"{rel}:{line}:{start}-{end}: "
150+
f"[{job_name}] stale matrix entry ./{mod} (no go.mod)"
151+
)
150152
errors += 1
151153

152154
return errors
@@ -174,7 +176,10 @@ def check_golangci_configs(root: Path, modules: set[Path]) -> int:
174176
candidate = candidate.parent
175177
if not found:
176178
# Anchor on the module's go.mod — no specific token at fault.
177-
print(f"{mod}/go.mod:1:1-2: no .golangci.yml reachable from ./{mod} (module dir or any parent up to repo root)")
179+
print(
180+
f"{mod}/go.mod:1:1-2: no .golangci.yml reachable from ./{mod} "
181+
f"(module dir or any parent up to repo root)"
182+
)
178183
errors += 1
179184
return errors
180185

meta/scripts/check_go_work.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# where rules_python already makes the import resolvable.
1717
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
1818

19-
from meta.scripts._workspace import ( # noqa: E402
19+
from meta.scripts._workspace import (
2020
col_range,
2121
found_modules,
2222
registered_modules,

0 commit comments

Comments
 (0)