aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-08-21 12:14:27 +0200
committerblasty <peter@haxx.in>2026-08-21 12:14:27 +0200
commit93ca90558aa6016e77625c627a70640e693e3303 (patch)
tree284dfb0d126aefd0bb69f517ec64ca12d2130bcb
parenttests: repair the two seams the ida-nexus port left behind (diff)
downloadida-tui-93ca90558aa6016e77625c627a70640e693e3303.tar.gz
ida-tui-93ca90558aa6016e77625c627a70640e693e3303.tar.xz
ida-tui-93ca90558aa6016e77625c627a70640e693e3303.zip
adopt ruff: pinned formatter + import sorting, opt-in pre-commit hook
PR #1 arrived black-formatted, which forced the question. ruff's formatter is black's output, it is one static binary rather than a dependency chain, and it is fast enough that a hook is free. ruff.toml pins the version (a formatter whose output moves between releases turns 'formatted' into 'formatted by whoever committed last') and selects only import sorting on the lint side -- a hook that refused a commit over an unused variable would be a different feature. .githooks/pre-commit (install: git config core.hooksPath .githooks) formats exactly what is staged, refuses partly-staged files rather than quietly widening a commit built with git add -p, and prefers the pinned .venv ruff. Also drops the blanket '# ruff: noqa' PR #1 left in remote_ops.py -- it would have exempted that file from the only lint rule we enforce.
-rwxr-xr-x.githooks/pre-commit53
-rw-r--r--CONTRIBUTING.md14
-rw-r--r--idatui/remote_ops.py1
-rw-r--r--pyproject.toml2
-rw-r--r--ruff.toml33
-rw-r--r--uv.lock27
6 files changed, 128 insertions, 2 deletions
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..39cd4a5
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,53 @@
+#!/bin/sh
+# Format the Python being committed, so what lands in git is already formatted.
+# Imports sorted, then laid out -- both by ruff (ruff.toml pins the version).
+#
+# Install with `git config core.hooksPath .githooks`. Hooks are not versioned
+# by git, so a hook in a repo has to be a file somebody opts into -- there is
+# no way to ship one that runs on clone, and a repo that could would be a repo
+# that runs code on clone.
+#
+# Skip a commit with `git commit --no-verify` when you mean to.
+set -e
+
+py_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.py$' || true)
+[ -n "$py_files" ] || exit 0
+
+# Where ruff is. The repo-local venv first, before $PATH: ruff.toml pins a
+# version, and a system ruff of the wrong one would refuse the commit while
+# the venv sitting right there has the version it asked for. Off the toplevel,
+# so this works from a subdirectory.
+root=$(git rev-parse --show-toplevel)
+if [ -x "$root/.venv/bin/ruff" ]; then
+ ruff="$root/.venv/bin/ruff"
+else
+ ruff=$(command -v ruff 2>/dev/null || true)
+fi
+if [ -z "$ruff" ]; then
+ echo "pre-commit: ruff is not installed, and this commit touches Python." >&2
+ echo " uv sync --extra dev (a venv in .venv, version pinned)" >&2
+ echo " ...or commit with --no-verify if you know what you are doing." >&2
+ exit 1
+fi
+
+# A file with unstaged changes is the one case where formatting in place is
+# dangerous: the formatter rewrites the *working tree*, and re-staging
+# afterwards would commit work that was deliberately left out of the index.
+# So say so and stop, rather than quietly widening a commit somebody built
+# with `git add -p`.
+partial=""
+for f in $py_files; do
+ if ! git diff --quiet -- "$f"; then partial="$partial $f"; fi
+done
+if [ -n "$partial" ]; then
+ echo "pre-commit: these files are only partly staged, so formatting them" >&2
+ echo " in place would add work you left out of the commit:" >&2
+ for f in $partial; do echo " $f" >&2; done
+ echo " stage them fully, stash the rest, or use --no-verify." >&2
+ exit 1
+fi
+
+cd "$root"
+"$ruff" check --select I --fix --quiet $py_files
+"$ruff" format --quiet $py_files
+git add $py_files
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b66df1c..5cf7765 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -51,6 +51,20 @@ The IDA-backed suites need an interpreter that has `textual`, `idapro` and
import instead of doing it at module top. Please don't break that — it's what
keeps the fast gate fast and lets people without IDA contribute at all.
+## Formatting
+
+Python is formatted by ruff (`ruff.toml` pins the exact version; the config is
+deliberately default: black-style layout plus import sorting, nothing else).
+Install the pre-commit hook once and forget about it:
+
+```bash
+uv sync --extra dev # puts the pinned ruff in .venv
+git config core.hooksPath .githooks # formats what you stage
+```
+
+Mechanical reformat commits are listed in `.git-blame-ignore-revs`;
+`git config blame.ignoreRevsFile .git-blame-ignore-revs` keeps blame useful.
+
## Sending a change
- Run at least `python3 tests/run.py --fast` before you push. If your change
diff --git a/idatui/remote_ops.py b/idatui/remote_ops.py
index cbd1d49..b07bb71 100644
--- a/idatui/remote_ops.py
+++ b/idatui/remote_ops.py
@@ -1,7 +1,6 @@
"""Typed remote operations executed through ida-nexus."""
from __future__ import annotations
-# ruff: noqa
import threading
from collections.abc import Callable
diff --git a/pyproject.toml b/pyproject.toml
index dc68d12..c16d406 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ dependencies = [
]
[project.optional-dependencies]
-dev = ["pytest>=8"]
+dev = ["pytest>=8", "ruff==0.16.3"] # ruff.toml pins the same version
# The graph view's SESE layout engine (`e` in graph mode) is optional: without
# it, layout falls back to the pure-python engine. It is deliberately NOT listed
# as a dependency -- PyPI's pytriskel has no wheel for current Pythons, no sdist,
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..6d0f877
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,33 @@
+# How the Python in this tree is formatted, and how its imports are ordered.
+# `.githooks/pre-commit` applies both to what you staged (install it with
+# `git config core.hooksPath .githooks`); `ruff format --check .` asks.
+#
+# Adopted when PR #1 arrived black-formatted: ruff's formatter IS black's
+# output, it is one static binary instead of a Python dependency chain, and it
+# is fast enough that a pre-commit hook is free. Configuration is deliberately
+# left at the defaults -- every knob turned here is an argument somebody has to
+# have again later.
+
+# The version this tree was formatted with, enforced by ruff itself: a
+# formatter whose output moves between releases turns "formatted" into
+# "formatted by whoever committed last", and the diff lands on the next
+# person. A mismatch is refused with both versions named rather than quietly
+# reformatting everything. Bumping it is one line here, one in the hook's
+# install hint, and a reformat commit (added to .git-blame-ignore-revs) --
+# the honest cost of a new version.
+required-version = "==0.16.3"
+
+target-version = "py311"
+line-length = 88
+
+[lint]
+# Import sorting, and nothing else. `ruff check` can enforce a great deal more
+# and one day it might, but this hook's job is formatting: one that also
+# refused a commit over an unused variable would be a different feature, and
+# not one anybody asked for.
+#
+# Sorting is safe here because ruff sorts import BLOCKS and never hoists across
+# a statement: every test file's `sys.path.insert(...)` sits between the
+# standard library and the `idatui` imports, and that line is exactly what
+# makes them importable.
+select = ["I"]
diff --git a/uv.lock b/uv.lock
index 42e544a..2808382 100644
--- a/uv.lock
+++ b/uv.lock
@@ -61,6 +61,7 @@ dependencies = [
[package.optional-dependencies]
dev = [
{ name = "pytest" },
+ { name = "ruff" },
]
[package.metadata]
@@ -68,6 +69,7 @@ requires-dist = [
{ name = "ida-nexus", specifier = ">=0.7.0" },
{ name = "pygments", specifier = ">=2" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
+ { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.3" },
{ name = "textual", specifier = ">=8" },
]
provides-extras = ["dev"]
@@ -197,6 +199,31 @@ wheels = [
]
[[package]]
+name = "ruff"
+version = "0.16.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" },
+ { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" },
+ { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" },
+ { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" },
+ { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" },
+ { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" },
+ { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" },
+ { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" },
+]
+
+[[package]]
name = "textual"
version = "8.2.8"
source = { registry = "https://pypi.org/simple" }