added ruff for code review
This commit is contained in:
@@ -41,7 +41,12 @@ def clip_halfspace(points, distance):
|
|||||||
|
|
||||||
def clip_rect(points, x0, x1, y0, y1):
|
def clip_rect(points, x0, x1, y0, y1):
|
||||||
for axis, bound, sign in ((0, x0, 1), (0, x1, -1), (1, y0, 1), (1, y1, -1)):
|
for axis, bound, sign in ((0, x0, 1), (0, x1, -1), (1, y0, 1), (1, y1, -1)):
|
||||||
points = clip_halfspace(points, lambda p: sign * (p[axis] - bound))
|
# Bind the loop variables as defaults: the callback must clip against the
|
||||||
|
# plane of *this* iteration, not whatever the loop left behind.
|
||||||
|
points = clip_halfspace(
|
||||||
|
points,
|
||||||
|
lambda p, axis=axis, bound=bound, sign=sign: sign * (p[axis] - bound),
|
||||||
|
)
|
||||||
return points
|
return points
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +61,12 @@ def project_triangle(clip_vertices):
|
|||||||
for i, v in enumerate(clip_vertices)]
|
for i, v in enumerate(clip_vertices)]
|
||||||
for axis in range(3):
|
for axis in range(3):
|
||||||
for sign in (-1, 1):
|
for sign in (-1, 1):
|
||||||
points = clip_halfspace(points, lambda p: p[3] + sign * p[axis])
|
# Same late-binding hazard as clip_rect: bind axis/sign per iteration so
|
||||||
|
# the clip plane cannot be read from a later loop state.
|
||||||
|
points = clip_halfspace(
|
||||||
|
points,
|
||||||
|
lambda p, axis=axis, sign=sign: p[3] + sign * p[axis],
|
||||||
|
)
|
||||||
return [((p[0] / p[3] + 1) / 2, (p[1] / p[3] + 1) / 2,
|
return [((p[0] / p[3] + 1) / 2, (p[1] / p[3] + 1) / 2,
|
||||||
1 / p[3], *(b / p[3] for b in p[4:]))
|
1 / p[3], *(b / p[3] for b in p[4:]))
|
||||||
for p in points if p[3] > 1e-15]
|
for p in points if p[3] > 1e-15]
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_samp
|
|||||||
output[name]['mesh'].append(uvs[name])
|
output[name]['mesh'].append(uvs[name])
|
||||||
cache = {}
|
cache = {}
|
||||||
|
|
||||||
def visible(weights):
|
# ``cache`` and ``world`` are bound as defaults: ``cache`` is rebound
|
||||||
|
# for every loop triangle, and a late-bound closure would read the
|
||||||
|
# next triangle's cache (or the final triangle's, if a probe were
|
||||||
|
# deferred), yielding a wrong visibility answer per triangle.
|
||||||
|
def visible(weights, cache=cache, world=world):
|
||||||
nonlocal ray_count
|
nonlocal ray_count
|
||||||
key = tuple(round(w, 12) for w in weights)
|
key = tuple(round(w, 12) for w in weights)
|
||||||
if key in cache:
|
if key in cache:
|
||||||
@@ -64,10 +68,10 @@ def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_samp
|
|||||||
output[name]['patches'].append((i, j, polygon))
|
output[name]['patches'].append((i, j, polygon))
|
||||||
print(f'Evaluated {len(mesh.loop_triangles)} loop triangles; {ray_count} visibility rays; '
|
print(f'Evaluated {len(mesh.loop_triangles)} loop triangles; {ray_count} visibility rays; '
|
||||||
f'{len(output[uv_names[0]]["patches"])} local fragments per UV target')
|
f'{len(output[uv_names[0]]["patches"])} local fragments per UV target')
|
||||||
print(f'Visibility sampling: seven probes per fragment; uniformly probed '
|
print('Visibility sampling: seven probes per fragment; uniformly probed '
|
||||||
f'fragments are accepted or dropped whole, mixed boundary fragments '
|
'fragments are accepted or dropped whole, mixed boundary fragments '
|
||||||
f'refine to 1/4 cell or depth 6 and unresolved leaves are rejected. '
|
'refine to 1/4 cell or depth 6 and unresolved leaves are rejected. '
|
||||||
f'Sub-sample occluders may be missed; hit tolerance max(1e-7, distance*2e-5).')
|
'Sub-sample occluders may be missed; hit tolerance max(1e-7, distance*2e-5).')
|
||||||
return output
|
return output
|
||||||
finally:
|
finally:
|
||||||
evaluated.to_mesh_clear()
|
evaluated.to_mesh_clear()
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ bpy (the same pattern as ``blender/scripts/patch_geometry.py``).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|||||||
from patch_geometry import clip_rect
|
from patch_geometry import clip_rect
|
||||||
from patch_projection import build_patch_geometry
|
from patch_projection import build_patch_geometry
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Common Utilities
|
# Common Utilities
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
"""Run with python -m unittest discover -s blender/scripts -p test_patch_geometry.py."""
|
"""Run with python -m unittest discover -s blender/scripts -p test_patch_geometry.py."""
|
||||||
import unittest
|
import unittest
|
||||||
from patch_geometry import (area, barycentric, cell_fragments, clip_rect,
|
|
||||||
interpolate_uv, project_triangle, visible_triangles)
|
from patch_geometry import (
|
||||||
|
area,
|
||||||
|
barycentric,
|
||||||
|
cell_fragments,
|
||||||
|
clip_rect,
|
||||||
|
interpolate_uv,
|
||||||
|
project_triangle,
|
||||||
|
visible_triangles,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GeometryTests(unittest.TestCase):
|
class GeometryTests(unittest.TestCase):
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import base64
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from probe_falloff import (
|
from probe_falloff import (
|
||||||
FALLOFF_MODES,
|
FALLOFF_MODES,
|
||||||
affected_bands,
|
affected_bands,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Code review: the automated part
|
||||||
|
|
||||||
|
Four gates existed before this document (the test suites, `apps/studio/tools/ui_check.py`, the
|
||||||
|
real-GL smoke tests, the binding-purity test) and none of them is a linter. This adds the missing
|
||||||
|
*static* layer, deliberately small.
|
||||||
|
|
||||||
|
## Why it exists, in one measured fact
|
||||||
|
|
||||||
|
Until now the three repos had **no Ruff configuration at all**. `ruff check .` therefore ran
|
||||||
|
whatever the installed version enables by default — Ruff 0.16 enables a very broad set
|
||||||
|
(`UP`/`RUF`/`PLR`/`S`/`TRY`/`PERF`/`FURB`/`YTT`/`ASYNC`…) — so the same tree produced **1038 /
|
||||||
|
438 / 117** findings in `orisys-app` / `orisys-sdk` / `orisys-sensor-design`, and a different
|
||||||
|
Ruff version produced different ones. A gate built on that is not a gate. `ruff.toml` now pins it.
|
||||||
|
|
||||||
|
## What is gated, and what is deliberately not
|
||||||
|
|
||||||
|
| rule | meaning | gated? | why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `BLE001` | broad `except Exception` | **no** | A broad catch is the *right* choice around device I/O, GL teardown and camera probing. 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` | handler is only `pass` / `continue` | **yes** | 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. |
|
||||||
|
| `E722` | bare `except:` | **yes** | Strictly worse than `except Exception:` — it also swallows `KeyboardInterrupt`/`SystemExit`. |
|
||||||
|
| `F` | unused import, redefinition, undefined name, unused variable | **yes** | Real defects: `F821`/`F822` are latent `NameError`s, `F811` can be a shadowed definition. |
|
||||||
|
| `B023` | closure over a loop variable | **yes** | Late binding silently produces wrong geometry in the headless Blender scripts. |
|
||||||
|
| `PLW1510` | `subprocess.run` without `check` | **yes** | A swallowed tool failure. |
|
||||||
|
| `RUF100` | unused suppression | reported, **never auto-fixed** | The `- reason` in a `noqa` comment is documentation. A machine must not delete it. Triage by hand. |
|
||||||
|
| `UP`, `I`, `PIE`, `RET501`, `PLR1711`, `SIM114`, `RUF022`, `RUF023` | mechanical modernisation | in the lint config, **not** in the gate | Applied once by `--fix`; gating them would only churn AI-authored diffs. |
|
||||||
|
|
||||||
|
`E402` (module import not at top of file) is **selected but not gated**. `import` after
|
||||||
|
`os.environ[...]` is deliberate in this codebase — the environment must be set before Qt/cv2/the SDK
|
||||||
|
load — so gating it would be pure noise. But it is the only rule that would catch an *accidental*
|
||||||
|
import moved above that setup, so it stays visible. When you touch one of those files, record the
|
||||||
|
intent as `# noqa: E402 - <reason>`; `E701`/`E702` are style, which `ruff format` owns. A gate that
|
||||||
|
cries wolf gets ignored, which is worse than no gate.
|
||||||
|
|
||||||
|
## The three commands
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. The agent loop, after every edit — mechanical, no review needed.
|
||||||
|
ruff check <changed files> --fix # never --unsafe-fixes
|
||||||
|
ruff check <changed files> --output-format json # what remains, machine-readable
|
||||||
|
|
||||||
|
# 2. The gate before a change lands. Changed files only, machine-readable, non-zero on findings.
|
||||||
|
python tools/lint_gate.py # vs HEAD
|
||||||
|
python tools/lint_gate.py --base origin/main
|
||||||
|
python tools/lint_gate.py --all # gate rules over the whole repo
|
||||||
|
python tools/lint_gate.py --json
|
||||||
|
|
||||||
|
# 3. Formatting, changed files only — never a whole-tree rewrite mid-diff.
|
||||||
|
ruff format <changed files>
|
||||||
|
```
|
||||||
|
|
||||||
|
`$RUFF` may point at a specific ruff binary; otherwise `tools/lint_gate.py` uses `PATH` or the one
|
||||||
|
beside the interpreter. **Pin the version**: the gate prints the version it ran, and findings differ
|
||||||
|
between versions, so a runner must install the same one the developers use (currently 0.16.9).
|
||||||
|
|
||||||
|
## What this found when it was introduced
|
||||||
|
|
||||||
|
Filtering to rules that can indicate a defect produced ~18 candidates rather than style noise —
|
||||||
|
which is the argument for the narrow set:
|
||||||
|
|
||||||
|
- six latent `NameError`s (`np`, `sys`, `Callable`, `FrameIdentity`, `tessellate_polygon`, and an
|
||||||
|
`__all__` entry naming a function that does not exist) — none caught by any test;
|
||||||
|
- a **duplicated** function definition that silently shadowed the first;
|
||||||
|
- `lower == lower` / `upper == upper` in a range check, and the same pattern inside a **test
|
||||||
|
assertion that could never fail**;
|
||||||
|
- 13 closures over loop variables in the headless Blender/patch scripts;
|
||||||
|
- two `subprocess.run` calls whose failure was silently ignored.
|
||||||
|
|
||||||
|
## Follow-up deliberately not done yet
|
||||||
|
|
||||||
|
`PLR0124` ("name compared with itself") is not in `select`. Eleven of its sites are the deliberate
|
||||||
|
`value != value` NaN idiom and must become `math.isnan(...)` (or carry a reasoned suppression)
|
||||||
|
before the rule can be gated — otherwise it is noise. Once those are converted, add `PLR0124` to
|
||||||
|
`select` and to `GATE_SELECT` in `tools/lint_gate.py`.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Ruff configuration — pinned deliberately.
|
||||||
|
#
|
||||||
|
# Before this file existed the repo had no Ruff config, so `ruff check .` ran whatever the
|
||||||
|
# installed Ruff version enables by default (Ruff 0.16 turns on a very broad set). Findings
|
||||||
|
# were therefore not reproducible between machines. Never rely on tool defaults.
|
||||||
|
#
|
||||||
|
# Policy, decided from the measured findings:
|
||||||
|
#
|
||||||
|
# * `select` is explicit: high-signal correctness rules first, then the mechanical
|
||||||
|
# modernisation rules only.
|
||||||
|
# * BLE001 is selected so existing reasoned suppressions stay *used* and RUF100 cannot
|
||||||
|
# delete their rationale. Broad catches around Blender/environment probing are legitimate.
|
||||||
|
# BLE001 is NOT part of the gate.
|
||||||
|
# * S110 / S112 / E722 ARE part of the gate. The Blender scripts are the riskiest place for
|
||||||
|
# a silent swallow: they run headless, so a swallowed error becomes a silently wrong or
|
||||||
|
# missing artefact rather than a visible crash.
|
||||||
|
# * B023 (loop-variable closure) matters here more than anywhere else in the workspace —
|
||||||
|
# the geometry/patch scripts define callbacks inside loops over axes and signs.
|
||||||
|
# * RUF100 is `unfixable`: a machine must not remove a suppression whose comment records
|
||||||
|
# why it exists.
|
||||||
|
# * The mechanical sweep never uses `--unsafe-fixes`.
|
||||||
|
#
|
||||||
|
# The gate selects the narrow set explicitly:
|
||||||
|
#
|
||||||
|
# tools/lint_gate.py # the gate: changed files, machine-readable
|
||||||
|
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
select = [
|
||||||
|
"E9", "E722", # syntax/IO errors, and bare `except:`
|
||||||
|
"E402", # module import not at top of file — SELECTED for visibility, NOT gated.
|
||||||
|
# `import` after `os.environ[...]` is deliberate where the environment or
|
||||||
|
# the SDK path must be set first. E402 is the only rule that would catch an
|
||||||
|
# accidental import moved above that setup, so it stays reported without
|
||||||
|
# failing a build. An intentional site belongs in `# noqa: E402 - <reason>`.
|
||||||
|
"F", # pyflakes: unused import, redefinition, undefined name
|
||||||
|
"S110", "S112", # try-except-pass / try-except-continue — an invisible failure
|
||||||
|
"B023", # function definition does not bind loop variable
|
||||||
|
"PLW1510", # subprocess.run without an explicit check
|
||||||
|
"RUF100", # unused noqa — reported for triage, never auto-removed
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"I", # import sorting
|
||||||
|
"RUF022", "RUF023", # __all__ / __slots__ ordering
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"RET501", "PLR1711",
|
||||||
|
"SIM114",
|
||||||
|
"BLE001", # selected for suppression hygiene only — excluded from the gate
|
||||||
|
]
|
||||||
|
unfixable = ["RUF100"]
|
||||||
|
|
||||||
|
[format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
Binary file not shown.
@@ -16,8 +16,8 @@ from orisys_sensor_design.manifest import (
|
|||||||
build_export_job,
|
build_export_job,
|
||||||
build_manifest,
|
build_manifest,
|
||||||
default_manifest_path,
|
default_manifest_path,
|
||||||
export_revision_dir,
|
|
||||||
export_output_dir,
|
export_output_dir,
|
||||||
|
export_revision_dir,
|
||||||
load_profile,
|
load_profile,
|
||||||
repo_root,
|
repo_root,
|
||||||
validate_revision_metadata,
|
validate_revision_metadata,
|
||||||
@@ -662,6 +662,7 @@ def cmd_optics(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
from orisys_sensor_design.optics import REPORT_FILENAME as OPTICS_REPORT_FILENAME
|
||||||
from orisys_sensor_design.optics import (
|
from orisys_sensor_design.optics import (
|
||||||
OpticsConfigError,
|
OpticsConfigError,
|
||||||
build_report,
|
build_report,
|
||||||
@@ -672,7 +673,6 @@ def cmd_optics(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
summarize,
|
summarize,
|
||||||
write_report,
|
write_report,
|
||||||
)
|
)
|
||||||
from orisys_sensor_design.optics import REPORT_FILENAME as OPTICS_REPORT_FILENAME
|
|
||||||
|
|
||||||
want_plot = not args.no_plot
|
want_plot = not args.no_plot
|
||||||
if want_plot:
|
if want_plot:
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ supplies one backed by the real stage helpers.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
SCHEMA = "orisys-sensor-design/settings-comparison-v1"
|
SCHEMA = "orisys-sensor-design/settings-comparison-v1"
|
||||||
FILENAME = "settings_comparison.json"
|
FILENAME = "settings_comparison.json"
|
||||||
@@ -410,7 +411,10 @@ def _probe_for(
|
|||||||
changes["pattern"] = dataclasses_module.replace(
|
changes["pattern"] = dataclasses_module.replace(
|
||||||
pattern, nx=int(entry.nx), ny=int(entry.ny), seed=int(entry.seed)
|
pattern, nx=int(entry.nx), ny=int(entry.ny), seed=int(entry.seed)
|
||||||
)
|
)
|
||||||
except Exception: # a non-dataclass pattern must not break the sweep
|
except Exception: # noqa: S110 - a non-dataclass pattern must not break the sweep
|
||||||
|
# Deliberate: ``dataclasses.replace`` only applies to dataclass
|
||||||
|
# patterns. A duck-typed/plain pattern keeps its defaults and the
|
||||||
|
# sweep continues; the run is still recorded under its own settings.
|
||||||
pass
|
pass
|
||||||
return dataclasses_module.replace(settings, **changes)
|
return dataclasses_module.replace(settings, **changes)
|
||||||
|
|
||||||
@@ -651,7 +655,6 @@ def build_figures(
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"COMPARISON_DIRNAME",
|
"COMPARISON_DIRNAME",
|
||||||
"CompareError",
|
|
||||||
"DEFAULT_DIS_MODES",
|
"DEFAULT_DIS_MODES",
|
||||||
"DEFAULT_GRID",
|
"DEFAULT_GRID",
|
||||||
"DEFAULT_MAX_RUNS",
|
"DEFAULT_MAX_RUNS",
|
||||||
@@ -659,6 +662,7 @@ __all__ = [
|
|||||||
"DEFAULT_SEEDS",
|
"DEFAULT_SEEDS",
|
||||||
"FILENAME",
|
"FILENAME",
|
||||||
"SCHEMA",
|
"SCHEMA",
|
||||||
|
"CompareError",
|
||||||
"SweepEntry",
|
"SweepEntry",
|
||||||
"SweepOutcome",
|
"SweepOutcome",
|
||||||
"build_figures",
|
"build_figures",
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ Contract: ``docs/probe-pipeline.md``.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Sequence
|
from typing import Any
|
||||||
|
|
||||||
from orisys_sensor_design.manifest import RevisionPaths, export_object_names, load_yaml
|
from orisys_sensor_design.manifest import RevisionPaths, export_object_names, load_yaml
|
||||||
from orisys_sensor_design.texture import (
|
from orisys_sensor_design.texture import (
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ a 500x500 render. That ratio is the number the bench should be calibrated agains
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -342,7 +343,9 @@ class FixedReferenceFlow:
|
|||||||
if callable(getter):
|
if callable(getter):
|
||||||
try:
|
try:
|
||||||
return int(getter())
|
return int(getter())
|
||||||
except Exception: # pragma: no cover - defensive
|
except Exception: # noqa: S110 - defensive fallback to the documented default
|
||||||
|
# A binding whose getPatchSize() is missing or returns a non-numeric
|
||||||
|
# value reports the measured default instead of failing introspection.
|
||||||
pass
|
pass
|
||||||
return DEFAULT_DIS_PATCH_PX
|
return DEFAULT_DIS_PATCH_PX
|
||||||
|
|
||||||
@@ -1706,8 +1709,11 @@ def run_track_job(
|
|||||||
scale_output_dir = out / FLOW_DIRNAME / name
|
scale_output_dir = out / FLOW_DIRNAME / name
|
||||||
scale_output_dir.mkdir(parents=True, exist_ok=True)
|
scale_output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def _save(flow_array: np.ndarray, frame_name: str) -> None:
|
# Bind the per-scale directory: it is reassigned for every ``name`` in
|
||||||
np.save(scale_output_dir / f"{frame_name}.npy", flow_array.astype(np.float32))
|
# the loop, so a late-bound closure would write every frame into the
|
||||||
|
# last scale's directory if these calls were ever deferred.
|
||||||
|
def _save(flow_array: np.ndarray, frame_name: str, out_dir: Path = scale_output_dir) -> None:
|
||||||
|
np.save(out_dir / f"{frame_name}.npy", flow_array.astype(np.float32))
|
||||||
|
|
||||||
_save(estimator.compute(reference), str(reference_entry.get("name", "reference")))
|
_save(estimator.compute(reference), str(reference_entry.get("name", "reference")))
|
||||||
if null_path is not None:
|
if null_path is not None:
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ this module imports neither Blender nor cv2 and is testable in isolation.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
from orisys_sensor_design.manifest import RevisionPaths, repo_root
|
from orisys_sensor_design.manifest import RevisionPaths, repo_root
|
||||||
from orisys_sensor_design.sensitivity import (
|
from orisys_sensor_design.sensitivity import (
|
||||||
@@ -88,19 +90,19 @@ ROI_CONFOUND_NOTE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"OPTIMIZER_REPORT_SCHEMA",
|
|
||||||
"OPTIMIZER_REPORT_FILENAME",
|
|
||||||
"OPTIMIZER_OBJECTIVE",
|
|
||||||
"DEFAULT_GRID",
|
"DEFAULT_GRID",
|
||||||
"DEFAULT_SEEDS",
|
|
||||||
"DEFAULT_SEARCH_RESOLUTION",
|
"DEFAULT_SEARCH_RESOLUTION",
|
||||||
"NOT_GLOBAL_OPTIMUM",
|
"DEFAULT_SEEDS",
|
||||||
"GRID_RATIONALE",
|
"GRID_RATIONALE",
|
||||||
"SCALE_DEPENDENT_NOTE",
|
"NOT_GLOBAL_OPTIMUM",
|
||||||
|
"OPTIMIZER_OBJECTIVE",
|
||||||
|
"OPTIMIZER_REPORT_FILENAME",
|
||||||
|
"OPTIMIZER_REPORT_SCHEMA",
|
||||||
"ROI_CONFOUND_NOTE",
|
"ROI_CONFOUND_NOTE",
|
||||||
"OptimiseError",
|
"SCALE_DEPENDENT_NOTE",
|
||||||
"Candidate",
|
"Candidate",
|
||||||
"CandidateRun",
|
"CandidateRun",
|
||||||
|
"OptimiseError",
|
||||||
"enumerate_candidates",
|
"enumerate_candidates",
|
||||||
"rank_entries",
|
"rank_entries",
|
||||||
"run_optimise",
|
"run_optimise",
|
||||||
@@ -231,7 +233,7 @@ def _as_float(value: Any) -> float | None:
|
|||||||
number = float(value)
|
number = float(value)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if number != number: # NaN
|
if math.isnan(number):
|
||||||
return None
|
return None
|
||||||
return number
|
return number
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ The viewport widget is kept separate from the chrome so it can be embedded later
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -132,7 +133,10 @@ def run_preview(
|
|||||||
"""
|
"""
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from orisys_sensor_design.preview.gl_view import PreviewGLWidget, configure_surface_format
|
from orisys_sensor_design.preview.gl_view import (
|
||||||
|
PreviewGLWidget,
|
||||||
|
configure_surface_format,
|
||||||
|
)
|
||||||
|
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
created = app is None
|
created = app is None
|
||||||
|
|||||||
@@ -626,9 +626,7 @@ def analyse_map(
|
|||||||
"passed": bool(max_ratio is not None and max_ratio <= settings.max_sliver_ratio),
|
"passed": bool(max_ratio is not None and max_ratio <= settings.max_sliver_ratio),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if not uniform:
|
if not uniform or not material:
|
||||||
verdict = "inconclusive"
|
|
||||||
elif not material:
|
|
||||||
verdict = "inconclusive"
|
verdict = "inconclusive"
|
||||||
else:
|
else:
|
||||||
verdict = "pass" if all(check["passed"] for check in checks) else "fail"
|
verdict = "pass" if all(check["passed"] for check in checks) else "fail"
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ comparison figure that quietly omits a failed setting is worse than no figure.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|||||||
@@ -462,10 +462,10 @@ __all__ = [
|
|||||||
"RENDER_JOB_SCHEMA",
|
"RENDER_JOB_SCHEMA",
|
||||||
"RENDER_MANIFEST_SCHEMA",
|
"RENDER_MANIFEST_SCHEMA",
|
||||||
"REQUIRED_VIEW_TRANSFORM",
|
"REQUIRED_VIEW_TRANSFORM",
|
||||||
|
"ProbeConfigError",
|
||||||
"RenderArtifactError",
|
"RenderArtifactError",
|
||||||
"RenderConfigError",
|
"RenderConfigError",
|
||||||
"RenderPaths",
|
"RenderPaths",
|
||||||
"ProbeConfigError",
|
|
||||||
"build_render_job",
|
"build_render_job",
|
||||||
"default_render_paths",
|
"default_render_paths",
|
||||||
"frame_name",
|
"frame_name",
|
||||||
|
|||||||
@@ -20,14 +20,20 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Mapping, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from orisys_sensor_design.manifest import RevisionPaths, load_yaml, repo_root, sha256_file
|
from orisys_sensor_design.manifest import (
|
||||||
|
RevisionPaths,
|
||||||
|
load_yaml,
|
||||||
|
repo_root,
|
||||||
|
sha256_file,
|
||||||
|
)
|
||||||
from orisys_sensor_design.texture import texture_revision_rel
|
from orisys_sensor_design.texture import texture_revision_rel
|
||||||
|
|
||||||
SENSITIVITY_REPORT_SCHEMA = "orisys-sensor-design/sensitivity-report-v2"
|
SENSITIVITY_REPORT_SCHEMA = "orisys-sensor-design/sensitivity-report-v2"
|
||||||
@@ -68,30 +74,29 @@ VERDICT_SEVERITY = {"ambiguous": -1, "unresolvable": 0, "marginal": 1, "resolvab
|
|||||||
RESOLVABLE_SNR = 3.0
|
RESOLVABLE_SNR = 3.0
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SENSITIVITY_REPORT_SCHEMA",
|
|
||||||
"SENSITIVITY_REPORT_FILENAME",
|
|
||||||
"DIS_PATCH_PX",
|
"DIS_PATCH_PX",
|
||||||
"min_local_scale_px_per_mm",
|
|
||||||
"MIN_LOCAL_SCALE_PX_PER_MM",
|
"MIN_LOCAL_SCALE_PX_PER_MM",
|
||||||
"SensitivityError",
|
"SENSITIVITY_REPORT_FILENAME",
|
||||||
"SensitivityConfigError",
|
"SENSITIVITY_REPORT_SCHEMA",
|
||||||
"SensitivityDataError",
|
"AxisScore",
|
||||||
|
"FrameFlow",
|
||||||
"MissingArtifactError",
|
"MissingArtifactError",
|
||||||
"Roi",
|
"Roi",
|
||||||
"FrameFlow",
|
|
||||||
"AxisScore",
|
|
||||||
"ScaleScore",
|
|
||||||
"ScaleInput",
|
"ScaleInput",
|
||||||
|
"ScaleScore",
|
||||||
|
"SensitivityConfigError",
|
||||||
|
"SensitivityDataError",
|
||||||
|
"SensitivityError",
|
||||||
|
"build_report",
|
||||||
|
"default_probe_output_dir",
|
||||||
|
"load_scale_inputs",
|
||||||
|
"min_local_scale_px_per_mm",
|
||||||
|
"resolution_pair",
|
||||||
|
"resolve_probe_output_dir",
|
||||||
|
"run_sensitivity",
|
||||||
"score_axis",
|
"score_axis",
|
||||||
"score_scale",
|
"score_scale",
|
||||||
"build_report",
|
|
||||||
"collect_report",
|
|
||||||
"run_sensitivity",
|
|
||||||
"write_sensitivity_report",
|
"write_sensitivity_report",
|
||||||
"load_scale_inputs",
|
|
||||||
"default_probe_output_dir",
|
|
||||||
"resolve_probe_output_dir",
|
|
||||||
"resolution_pair",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ from orisys_sensor_design.deform import (
|
|||||||
DEFAULT_PROBE,
|
DEFAULT_PROBE,
|
||||||
DEFORM_JOB_FILENAME,
|
DEFORM_JOB_FILENAME,
|
||||||
DEFORM_JOB_SCHEMA,
|
DEFORM_JOB_SCHEMA,
|
||||||
FRAMES_NPZ_FILENAME,
|
|
||||||
DEFORM_REPORT_FILENAME,
|
DEFORM_REPORT_FILENAME,
|
||||||
|
FRAMES_NPZ_FILENAME,
|
||||||
|
PatternInfo,
|
||||||
ProbeConfigError,
|
ProbeConfigError,
|
||||||
ProbeFrame,
|
ProbeFrame,
|
||||||
PatternInfo,
|
|
||||||
build_deform_job,
|
build_deform_job,
|
||||||
deform_job_path,
|
deform_job_path,
|
||||||
frame_plan,
|
frame_plan,
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import pytest
|
|||||||
|
|
||||||
np = pytest.importorskip("numpy")
|
np = pytest.importorskip("numpy")
|
||||||
|
|
||||||
|
from orisys_sensor_design.manifest import RevisionPaths, repo_root
|
||||||
from orisys_sensor_design.optics import (
|
from orisys_sensor_design.optics import (
|
||||||
AIR_INDEX,
|
|
||||||
Interface,
|
Interface,
|
||||||
|
OpticalStack,
|
||||||
OpticsConfigError,
|
OpticsConfigError,
|
||||||
OpticsLayer,
|
OpticsLayer,
|
||||||
OpticalStack,
|
|
||||||
intersect_mesh,
|
intersect_mesh,
|
||||||
load_optics_block,
|
load_optics_block,
|
||||||
quantify,
|
quantify,
|
||||||
@@ -24,7 +24,6 @@ from orisys_sensor_design.optics import (
|
|||||||
trace_ray,
|
trace_ray,
|
||||||
validate_optics_block,
|
validate_optics_block,
|
||||||
)
|
)
|
||||||
from orisys_sensor_design.manifest import RevisionPaths, repo_root
|
|
||||||
|
|
||||||
REPO = repo_root()
|
REPO = repo_root()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -11,14 +10,13 @@ import pytest
|
|||||||
np = pytest.importorskip("numpy")
|
np = pytest.importorskip("numpy")
|
||||||
|
|
||||||
from orisys_sensor_design.optics import (
|
from orisys_sensor_design.optics import (
|
||||||
AIR_INDEX,
|
|
||||||
Interface,
|
Interface,
|
||||||
OpticsLayer,
|
|
||||||
OpticalStack,
|
OpticalStack,
|
||||||
|
OpticsLayer,
|
||||||
camera_rays,
|
camera_rays,
|
||||||
|
quantify,
|
||||||
sample_stack,
|
sample_stack,
|
||||||
summarize,
|
summarize,
|
||||||
quantify,
|
|
||||||
)
|
)
|
||||||
from orisys_sensor_design.optics_plot import (
|
from orisys_sensor_design.optics_plot import (
|
||||||
DEFAULT_SANITY_MM,
|
DEFAULT_SANITY_MM,
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ def test_reference_cross_printable_flag(tmp_path: Path) -> None:
|
|||||||
_polygon("patch-1", "0,0 50,0 50,50 0,50")
|
_polygon("patch-1", "0,0 50,0 50,50 0,50")
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ ' <g id="reference-cross">\n'
|
+ ' <g id="reference-cross">\n'
|
||||||
+ f' <rect id="cross-arm-0" x="0" y="0" width="20" '
|
+ ' <rect id="cross-arm-0" x="0" y="0" width="20" '
|
||||||
+ f'height="{height_px}" fill="#ff0000"/>\n'
|
+ f'height="{height_px}" fill="#ff0000"/>\n'
|
||||||
+ " </g>"
|
+ " </g>"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -76,11 +76,14 @@ requires_blender = pytest.mark.skipif(
|
|||||||
|
|
||||||
def _run_blender(script: Path, job: Path, timeout: int = 900) -> subprocess.CompletedProcess:
|
def _run_blender(script: Path, job: Path, timeout: int = 900) -> subprocess.CompletedProcess:
|
||||||
assert BLENDER is not None
|
assert BLENDER is not None
|
||||||
|
# check=False is deliberate: several tests assert on non-zero Blender exit
|
||||||
|
# codes (2/3), so a non-zero status is data, not an error to raise on.
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
[str(BLENDER), "-b", str(SOURCE_BLEND), "-P", str(script), "--", "--job", str(job)],
|
[str(BLENDER), "-b", str(SOURCE_BLEND), "-P", str(script), "--", "--job", str(job)],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -248,7 +251,7 @@ def test_job_carries_the_hard_render_settings(paths, settings, tmp_path):
|
|||||||
assert job["objects"] == {"mesh": "sensing_surface", "camera": "cam_sensor"}
|
assert job["objects"] == {"mesh": "sensing_surface", "camera": "cam_sensor"}
|
||||||
assert job["null_frame"] == {"name": REFERENCE_NULL_NAME, "seed": 1}
|
assert job["null_frame"] == {"name": REFERENCE_NULL_NAME, "seed": 1}
|
||||||
assert job["artifacts"]["report"] == str(
|
assert job["artifacts"]["report"] == str(
|
||||||
(Path(tmp_path).resolve() / render.MANIFEST_FILENAME)
|
Path(tmp_path).resolve() / render.MANIFEST_FILENAME
|
||||||
)
|
)
|
||||||
assert job["artifacts"]["frames_npz"] == str(
|
assert job["artifacts"]["frames_npz"] == str(
|
||||||
deform.probe_artifact_paths(Path(tmp_path))["frames"]
|
deform.probe_artifact_paths(Path(tmp_path))["frames"]
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ def _stub_sdk(monkeypatch: pytest.MonkeyPatch, calls: dict, mask_mode: str = "va
|
|||||||
arrays["valid_mask"] = np.ones((4, 4), bool)
|
arrays["valid_mask"] = np.ones((4, 4), bool)
|
||||||
arrays["world_xyz"] = np.zeros((4, 4, 3), np.float32)
|
arrays["world_xyz"] = np.zeros((4, 4, 3), np.float32)
|
||||||
np.savez(map_path, **arrays)
|
np.savez(map_path, **arrays)
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(geometry, "scale_camera_geometry", fake_scale_camera_geometry)
|
monkeypatch.setattr(geometry, "scale_camera_geometry", fake_scale_camera_geometry)
|
||||||
monkeypatch.setattr(precompute, "build_or_load_domain", fake_build_or_load_domain)
|
monkeypatch.setattr(precompute, "build_or_load_domain", fake_build_or_load_domain)
|
||||||
@@ -306,7 +305,6 @@ def test_a_map_without_a_valid_mask_is_fatal(
|
|||||||
calls["build_kwargs"] = kwargs
|
calls["build_kwargs"] = kwargs
|
||||||
height, width = int(kwargs["out_height"]), int(kwargs["out_width"])
|
height, width = int(kwargs["out_height"]), int(kwargs["out_width"])
|
||||||
np.savez(map_path, map_x=np.zeros((height, width), np.float32))
|
np.savez(map_path, map_x=np.zeros((height, width), np.float32))
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(precompute, "build_or_load_domain", build_without_mask)
|
monkeypatch.setattr(precompute, "build_or_load_domain", build_without_mask)
|
||||||
with pytest.raises(uv.UvMapError, match="valid_mask"):
|
with pytest.raises(uv.UvMapError, match="valid_mask"):
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -36,6 +36,7 @@ import bpy
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy_extras.object_utils import world_to_camera_view
|
from bpy_extras.object_utils import world_to_camera_view
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
from mathutils.geometry import tessellate_polygon
|
||||||
|
|
||||||
|
|
||||||
def sha256(path):
|
def sha256(path):
|
||||||
|
|||||||
Reference in New Issue
Block a user