InsightSoftwareConsortium / InsightSoftwareConsortium/ITK
Investigate a Ruff EXE gate for shebangs, and Ruff as consolidation for black/pyupgrade
- Dominant language
- C++
- Stars
- 1.7k
- Forks
- 748
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 64
Description
### Proposal
Investigate adopting `ruff` in ITK's pre-commit configuration, for two related reasons:
1. **A shebang gate** — ruff's [`flake8-executable` (`EXE`)](https://docs.astral.sh/ruff/rules/#flake8-executable-exe) rules would end the recurring manual shebang sweeps.
2. **Tool consolidation** — ruff can subsume `black` and `pyupgrade`, ITK's only two Python formatting/lint hooks.
This is a request to investigate and discuss, not a request to merge something. The second half in particular has a real cost and a governance dimension.
### Why now: the shebang sweeps
`check-shebang-scripts-are-executable` verifies that a shebang and the executable bit agree. It does **not** check that the interpreter named actually resolves, and that gap has cost several PRs:
| PR | Branch | What it fixed |
|---|---|---|
| #6840 | release-5.4 | Marked `VCL_ModernizeNaming.py` executable to satisfy the hook |
| — | — | Automated review then flagged that its `#!python` still cannot execute (mode 755, `execve` → ENOENT) |
| #6842 | main | 17 × `env python` → `env python3`; 2 × `/usr/bin/python` → `env python3` |
| #6844 | release-5.4 | The same, plus `#!python` and `#!/bin/env python` (`/bin/env` does not exist) |
Every one of those was found by hand or by an external reviewer. `EXE003` flags a shebang that does not name `python`; `EXE001`/`EXE002` cover the mode pairing on POSIX and Windows. Together they turn a periodic manual audit into a hook.
Current state of Python shebangs after #6842 / #6844
Both branches converge on `#!/usr/bin/env python3`. Before those PRs:
| Shebang | main | release-5.4 |
|---|---|---|
| `#!/usr/bin/env python3` | 22 | 1 |
| `#!/usr/bin/env python` | 17 | 20 |
| `#!/usr/bin/python` | 4 | 4 |
| `#!python` | 0 | 1 |
| `#!/bin/env python` | 0 | 1 |
The drift was one-directional: `main` had been partially modernized while `release-5.4` had not, and nothing detected the difference.
### Why now: consolidation
ITK's Python hooks today are `black` (24.2.0) and `pyupgrade` (v3.21.2), across 180 first-party `.py` files. Ruff implements both: `ruff format` for the former, `UP` rules for the latter.
MONAI has been carrying out exactly this consolidation and has already folded in `pyupgrade` ([Project-MONAI/MONAI#8606](https://github.com/Project-MONAI/MONAI/issues/8606)), `yesqa` (#8692), `flake8` (#8694) and `pycln` (#9061). The remaining step — replacing `black` and `isort` — is under discussion in [Project-MONAI/MONAI#9066](https://github.com/Project-MONAI/MONAI/issues/9066), with parity and CI-consistency concerns in [#8683](https://github.com/Project-MONAI/MONAI/issues/8683).
ITK is a smaller case than MONAI: no `isort`, no `flake8`, 180 files rather than 1353. That makes it a cheaper experiment, and it means the decision here is genuinely optional rather than a maintenance necessity.
### Caveats worth stating up front
- **`ruff format` is not byte-identical to `black`.** It is intentionally close but documents known differences. A migration would produce a one-time reformatting diff that has to be reviewed, or accepted as mechanical.
- **Adding ruff for `EXE` alone still adds a dependency.** If the `EXE` rules are the only motivation, the honest comparison is against a ten-line local hook that greps first lines — cheaper, but one more bespoke script to own.
- **Wrapping-generated Python is not in scope** and would need exclusion, as `black` already handles today.
- **The two goals are separable.** An `EXE`-only ruff configuration is a small, low-risk change; replacing `black` is a larger conversation. They should probably not be bundled into one PR.
### Suggested investigation
1. Add ruff with **only** the `EXE` rules enabled and measure the finding count on both branches — likely zero after #6842/#6844, which would confirm the gate holds the line rather than creating work.
2. Separately, run `ruff format --diff` against a `black`-clean tree and report the size of the delta.
3. Report runtime for `ruff` versus `black` + `pyupgrade` on the 180-file set.
4. Bring the numbers back here before proposing any replacement.
Contributor guide
Research direction
Start with ITK's pre-commit configuration and the 180 first-party Python files. Run the proposed EXE-only check on both branches, then compare ruff format --diff with the black-clean tree and measure runtime against black plus pyupgrade. Done means reporting the finding count, formatting delta, and runtime before any replacement is proposed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100