added ruff test

This commit is contained in:
ykcheungab
2026-09-27 19:20:43 +08:00
parent bcab2790bf
commit fa2703968e
178 changed files with 3277 additions and 1034 deletions
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Merge gate for lint on a change — the narrow rule set a change must pass.
This is deliberately *not* the repo's full `ruff.toml` selection. The policy, decided from the
measured findings rather than from taste:
* ``BLE001`` (blind except) is **allowed**. A broad ``except Exception`` is the right choice
around device I/O, GL teardown and camera probing, and this codebase already records that
choice inline as ``# noqa: BLE001 - <reason>``. Gating it would produce ~100 findings whose
only correct fix is "add a reasoned suppression".
* ``S110``/``S112`` (a handler that is only ``pass``/``continue``) and ``E722`` (bare
``except:``) **are** gated: the complaint is not the breadth of the catch, it is that the
failure becomes invisible. Silence is acceptable only with a reasoned suppression or a
handler that reports/re-raises.
* ``RUF100`` (unused suppression) is reported for triage but is configured ``unfixable``: a
machine must never delete a suppression whose comment records why it exists.
Run over the files a change touches, never the whole tree, so an AI-authored diff is not
drowned in pre-existing style churn.
Usage::
tools/lint_gate.py # files changed vs HEAD (staged + unstaged)
tools/lint_gate.py --base origin/main # files changed vs a ref
tools/lint_gate.py --files a.py b.py # explicit files
tools/lint_gate.py --all # gate rules over the whole repo
tools/lint_gate.py --json # machine-readable result on stdout
Exit codes: 0 clean, 1 findings, 2 a usage or environment problem (e.g. ruff missing).
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
#: The gate. Explicit, so a rule added to `ruff.toml` cannot silently start failing builds.
#: `E4`/`E7` are deliberately absent: `E402` fires on the intentional "set the environment before
#: importing Qt" pattern in scripts, and `E701`/`E702` are style, which `ruff format` handles.
GATE_SELECT = "E9,E722,F,S110,S112,B023,PLW1510"
#: Rules we report but never let a tool auto-fix (RUF100 is `unfixable` in ruff.toml too).
REPORT_ONLY = "RUF100"
REPO = Path(__file__).resolve().parents[1]
def find_ruff() -> str:
"""Locate ruff: $RUFF, then PATH, then the interpreter environment."""
for candidate in (os.environ.get("RUFF"), shutil.which("ruff")):
if candidate and Path(candidate).is_file():
return candidate
beside = Path(sys.executable).parent / ("ruff.exe" if os.name == "nt" else "ruff")
if beside.is_file():
return str(beside)
print(
"lint_gate: ruff not found. Set $RUFF, install it beside the interpreter, or put it on "
"PATH. The pin matters: findings differ between ruff versions, so the gate names its "
"version below when it runs.",
file=sys.stderr,
)
raise SystemExit(2)
def git(*args: str) -> list[str]:
result = subprocess.run(
["git", "-C", str(REPO), *args],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(f"lint_gate: git {' '.join(args)} failed: {result.stderr.strip()}", file=sys.stderr)
raise SystemExit(2)
return [line for line in result.stdout.splitlines() if line.strip()]
def changed_python_files(base: str | None) -> list[str]:
"""The .py files this change touches: working tree, index, and new files."""
paths: set[str] = set()
if base:
paths.update(git("diff", "--name-only", "--diff-filter=ACMR", f"{base}...HEAD"))
paths.update(git("diff", "--name-only", "--diff-filter=ACMR"))
paths.update(git("diff", "--name-only", "--cached", "--diff-filter=ACMR"))
paths.update(git("ls-files", "--others", "--exclude-standard"))
return sorted(p for p in paths if p.endswith(".py") and (REPO / p).is_file())
def run_ruff(ruff: str, args: list[str]) -> tuple[list[dict], str]:
version = subprocess.run(
[ruff, "--version"], capture_output=True, text=True, check=False
).stdout.strip()
result = subprocess.run(
[ruff, "check", "--no-cache", "--output-format", "json", *args],
capture_output=True,
text=True,
check=False,
)
if result.returncode not in (0, 1):
print(f"lint_gate: ruff failed ({result.returncode}): {result.stderr.strip()}", file=sys.stderr)
raise SystemExit(2)
try:
findings = json.loads(result.stdout or "[]")
except json.JSONDecodeError:
print(f"lint_gate: could not parse ruff output: {result.stdout[:400]}", file=sys.stderr)
raise SystemExit(2)
return findings, version
def describe(finding: dict) -> str:
code = finding.get("code") or "?"
location = finding.get("location") or {}
path = str(finding.get("filename", "")).replace(str(REPO) + os.sep, "")
return (
f"{path}:{location.get('row', '?')}:{location.get('column', '?')}: {code}: "
f"{finding.get('message', '')}"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--base", default=None, help="compare against this ref (e.g. origin/main)")
parser.add_argument("--files", nargs="*", default=None, help="explicit files to check")
parser.add_argument("--all", action="store_true", help="run the gate rules over the whole repo")
parser.add_argument("--json", action="store_true", help="machine-readable result on stdout")
args = parser.parse_args(argv)
ruff = find_ruff()
if args.all:
targets = [str(REPO)]
else:
targets = args.files or changed_python_files(args.base)
if not targets:
print("lint_gate: no changed Python files; nothing to check.")
if args.json:
print(json.dumps({"findings": [], "checked": [], "status": "clean"}))
return 0
findings, version = run_ruff(ruff, ["--select", GATE_SELECT, *targets])
report_only, _ = run_ruff(ruff, ["--select", REPORT_ONLY, *targets])
if args.json:
print(
json.dumps(
{
"ruff": version,
"select": GATE_SELECT,
"checked": targets,
"findings": [
{
"file": str(f.get("filename", "")).replace(str(REPO) + os.sep, ""),
"row": (f.get("location") or {}).get("row"),
"column": (f.get("location") or {}).get("column"),
"code": f.get("code"),
"message": f.get("message"),
"fixable": bool((f.get("fix") or {}).get("applicability") == "safe"),
}
for f in findings
],
"unused_suppressions": [describe(f) for f in report_only],
"status": "findings" if findings else "clean",
},
indent=2,
)
)
return 1 if findings else 0
print(f"lint_gate: {version}")
print(f" select {GATE_SELECT} (BLE001 is allowed by policy; see ruff.toml)")
print(f" checked {len(targets)} target(s)")
if findings:
print(f"\n{len(findings)} finding(s):")
for finding in sorted(findings, key=describe):
hint = " [safe fix: ruff check --fix]" if (finding.get("fix") or {}).get("applicability") == "safe" else ""
print(f" {describe(finding)}{hint}")
else:
print(" no findings")
if report_only:
print(f"\n{len(report_only)} unused suppression(s) to triage by hand (never auto-removed):")
for finding in report_only:
print(f" {describe(finding)}")
print(f"\nstatus: {'FINDINGS' if findings else 'clean'}")
return 1 if findings else 0
if __name__ == "__main__":
raise SystemExit(main())
+4 -5
View File
@@ -77,7 +77,6 @@ def _grid_scene(target_vertices: int):
stand-in.
"""
import numpy as np
from orisys_app_shared.scene3d.contracts import (
CameraPreset,
Material,
@@ -88,6 +87,8 @@ def _grid_scene(target_vertices: int):
)
from orisys_app_shared.scene3d.digest import (
field_domain_id as compute_domain_id,
)
from orisys_app_shared.scene3d.digest import (
geometry_id as compute_geometry_id,
)
from orisys_app_shared.scene3d.legacy import vertex_normals
@@ -207,7 +208,6 @@ def measure_cpu(
what is left is the number an LOD decision should be made on.
"""
import numpy as np
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.composer import SceneComposer
from orisys_app_shared.scene3d.contracts import (
@@ -310,12 +310,11 @@ def measure(
) -> tuple[dict, int]:
"""Run the assembled view and report what each stage cost."""
import numpy as np
from PySide6.QtWidgets import QApplication
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.contracts import DisplaySettings
from orisys_studio.scene3d.gl_backend import configure_surface_format
from orisys_studio.scene3d.view import Scene3DView
from PySide6.QtWidgets import QApplication
configure_surface_format(profile) # type: ignore[arg-type]
app = QApplication.instance() or QApplication(sys.argv)
@@ -337,8 +336,8 @@ def measure(
report["reason"] = view.reason
return report, 4
if package is not None:
from orisys_app_shared.scene3d.package import PackagedAssetLoader
from orisys_app_shared.scene3d import protect
from orisys_app_shared.scene3d.package import PackagedAssetLoader
provider = (
protect.load_key_file(Path(key_file), key_id=key_id)
+1 -2
View File
@@ -89,8 +89,8 @@ def _platform_lock_from_args(args):
def _sealing_from_args(args):
"""The key material the command line points at, or ``None`` for a plain package."""
from orisys_app_shared.scene3d.contracts import Sealing
from orisys_app_shared.scene3d import protect
from orisys_app_shared.scene3d.contracts import Sealing
signing_pem = None
if args.sign_key is not None:
@@ -191,7 +191,6 @@ def main(argv: list[str] | None = None) -> int:
_bootstrap_paths()
import numpy as np
from orisys_app_shared.scene3d.contracts import ThreeDError
from orisys_app_shared.scene3d.legacy import LegacyFingerImporter
from orisys_app_shared.scene3d.package import describe_envelope, write_package
+10 -2
View File
@@ -71,7 +71,12 @@ MM_PER_SENSOR_UNIT = 0.001
def derive(design_dir: Path, sensor_dir: Path, *, tolerance: float) -> dict:
from align_housing import apply_similarity, fit_similarity, load_faces, load_vertices
from align_housing import (
apply_similarity,
fit_similarity,
load_faces,
load_vertices,
)
design_mesh = design_dir / "mesh.npz"
sensor_mesh = sensor_dir / "mesh.npz"
@@ -160,7 +165,10 @@ def verify(design_dir: Path, sensor_dir: Path, rigid: np.ndarray, units: float)
render vertices come out in the same order.
"""
from orisys_app_shared.scene3d.contracts import ThreeDError
from orisys_app_shared.scene3d.legacy import LegacyFingerImporter, validate_rigid_transform
from orisys_app_shared.scene3d.legacy import (
LegacyFingerImporter,
validate_rigid_transform,
)
design_map = design_dir / "unroll_map_uv_s0.42.npz"
sensor_map = sensor_dir / "unroll_map_uv_s0.4.npz"
+3 -5
View File
@@ -21,7 +21,6 @@ from __future__ import annotations
import argparse
import json
import os
import platform
import sys
import time
@@ -43,10 +42,6 @@ def _bootstrap_paths() -> None:
def collect(profile: str, *, keep_open_s: float) -> dict:
"""Create a real window, let Qt initialise it, and report what it got."""
from PySide6.QtCore import QTimer
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QApplication
from orisys_studio.scene3d.gl_backend import (
GLDispatch,
GLRenderer,
@@ -54,6 +49,9 @@ def collect(profile: str, *, keep_open_s: float) -> dict:
configure_surface_format,
lut_rgba,
)
from PySide6.QtCore import QTimer
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QApplication
configure_surface_format(profile) # type: ignore[arg-type]
app = QApplication.instance() or QApplication(sys.argv)
+2 -4
View File
@@ -115,10 +115,9 @@ def run(
late_rate_limit_mb_per_min: float,
) -> tuple[dict, int]:
"""Produce frames through the real pipeline and watch what it costs over time."""
import numpy as np
from concurrent.futures import ThreadPoolExecutor
import numpy as np
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.composer import SceneComposer
from orisys_app_shared.scene3d.contracts import (
@@ -141,10 +140,9 @@ def run(
view = None
widget = None
if use_gui:
from PySide6.QtWidgets import QApplication
from orisys_studio.scene3d.gl_backend import configure_surface_format
from orisys_studio.scene3d.view import Scene3DView
from PySide6.QtWidgets import QApplication
configure_surface_format(profile) # type: ignore[arg-type]
app = QApplication.instance() or QApplication(sys.argv)
+5 -2
View File
@@ -36,8 +36,11 @@ def _bootstrap_paths() -> None:
def _legacy_report(args) -> tuple[int, dict]:
import numpy as np
from orisys_app_shared.scene3d.contracts import ExpectedGeometry, ThreeDError, validate_assets
from orisys_app_shared.scene3d.contracts import (
ExpectedGeometry,
ThreeDError,
validate_assets,
)
from orisys_app_shared.scene3d.legacy import (
LegacyFingerImporter,
estimate_meters_per_source_unit,