Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion agents/conductors/hygiene/_hygiene_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@
import json
import re
import sys
import tomllib

try: # Python >= 3.11
import tomllib
except ModuleNotFoundError: # older interpreter: tomli is the same parser, if installed
try:
import tomli as tomllib # type: ignore[no-redef]
except ModuleNotFoundError:
tomllib = None # main() reports the scan as skipped rather than crashing
from pathlib import Path

import sys
Expand Down Expand Up @@ -286,6 +293,23 @@ def main() -> int:
output.add_argument("--summary", action="store_true")
args = parser.parse_args()

if tomllib is None:
reason = (
f"skipped: needs Python >= 3.11 (tomllib) or tomli to read pyproject.toml; "
f"ran on Python {sys.version_info.major}.{sys.version_info.minor}"
)
if args.json_row:
# Same envelope as the normal row; "error" + a null count is what
# hygiene.sh's own helper-failed row uses, so no reader sees "clean".
print(json.dumps({"count": None, "delegate": "/bug", "findings": [],
"kind": "finding", "mode": "extras",
"status": "error", "summary": reason}, sort_keys=True))
elif args.summary:
print(f"-1|{reason}") # count -1 renders as "advisory", never "clean"
else:
print(reason)
return 0

findings, skipped = missing(args.root)

if args.summary:
Expand Down
18 changes: 14 additions & 4 deletions agents/conductors/hygiene/hygiene.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@
set -uo pipefail

HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"

# _hygiene_extras.py parses pyproject.toml with `tomllib` (Python >= 3.11), but a
# bare `python3` can be older (3.10 on the WSL laptop), where the extras mode
# crashed instead of scanning (PyAutoBrain#581). Run it on the newest interpreter
# present; the helper itself still degrades to a "skipped" row if none has it.
TOML_PY="python3"
for _py in python3.13 python3.12 python3.11; do
if command -v "$_py" >/dev/null 2>&1; then TOML_PY="$_py"; break; fi
done
unset _py
source "$HERE/../../_common.sh"

# PYAUTO_ROOT is exported/defaulted by _common.sh. Scan the canonical checkouts
Expand Down Expand Up @@ -312,7 +322,7 @@ prescan_optdeps() {
# hand-added and silently drift. The symptom is a script red in smoke and green
# in release; the fix is always the install set, never the script.
prescan_extras() {
python3 "$HERE/_hygiene_extras.py" --root "$ROOT" --summary
"$TOML_PY" "$HERE/_hygiene_extras.py" --root "$ROOT" --summary
}

# refs: file/folder references in user-facing *_workspace and HowTo* prose
Expand Down Expand Up @@ -670,7 +680,7 @@ emit_json_row() { # mode
# this output, and one bad row would blank the whole hygiene section.
helper_row() { # helper-module mode
local out
if out="$(python3 "$HERE/$1" --root "$ROOT" --json-row 2>/dev/null)" \
if out="$("${3:-python3}" "$HERE/$1" --root "$ROOT" --json-row 2>/dev/null)" \
&& [[ -n "$out" ]] \
&& python3 -c 'import json,sys; json.loads(sys.argv[1])' "$out" 2>/dev/null; then
printf '%s' "$out"
Expand All @@ -683,7 +693,7 @@ emit_json_row() { # mode
if [[ "$m" == "escapes" ]]; then helper_row _hygiene_escapes.py escapes; return; fi
if [[ "$m" == "refs" ]]; then helper_row _hygiene_refs.py refs; return; fi
if [[ "$m" == "optdeps" ]]; then helper_row _hygiene_optdeps.py optdeps; return; fi
if [[ "$m" == "extras" ]]; then helper_row _hygiene_extras.py extras; return; fi
if [[ "$m" == "extras" ]]; then helper_row _hygiene_extras.py extras "$TOML_PY"; return; fi
local res count summary kind status
res="$(prescan "$m")"; count="${res%%|*}"; summary="${res#*|}"; kind="${MODE_KIND[$m]}"
if [[ "$kind" == "advisory" || "$count" == "-1" ]]; then status="advisory"
Expand Down Expand Up @@ -793,7 +803,7 @@ elif [[ "$mode" == "optdeps" ]]; then
python3 "$HERE/_hygiene_optdeps.py" --root "$ROOT"
elif [[ "$mode" == "extras" ]]; then
echo "Optional dependencies the workspace-validation smoke leg never installs (read-only scan):"
python3 "$HERE/_hygiene_extras.py" --root "$ROOT"
"$TOML_PY" "$HERE/_hygiene_extras.py" --root "$ROOT"
elif [[ "$mode" == "config" ]]; then
echo "Library config keys absent downstream + orphan config files (read-only scan):"
python3 "$HERE/_hygiene_config.py" --root "$ROOT" --detail \
Expand Down
63 changes: 63 additions & 0 deletions tests/test_hygiene_extras_toml_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""_hygiene_extras.py degrades to a "skipped" row without tomllib (PyAutoBrain#581).

`tomllib` is Python >= 3.11, and a bare `python3` can be older (3.10 on the WSL
laptop), where the extras mode used to die with ModuleNotFoundError. CI runs
3.12+, so the old-interpreter path is simulated by blocking both `tomllib` and
its `tomli` fallback before the helper is imported.
"""

import json
import subprocess
import sys
from pathlib import Path

HELPER = (
Path(__file__).resolve().parents[1]
/ "agents" / "conductors" / "hygiene" / "_hygiene_extras.py"
)

_BLOCK = """
import importlib.abc, runpy, sys
class _Block(importlib.abc.MetaPathFinder):
def find_spec(self, name, path=None, target=None):
if name in ("tomllib", "tomli"):
raise ModuleNotFoundError(name)
sys.modules.pop("tomllib", None)
sys.meta_path.insert(0, _Block())
sys.argv = [sys.argv[1]] + sys.argv[2:]
runpy.run_path(sys.argv[0], run_name="__main__")
"""


def _run(tmp_path, *flags):
return subprocess.run(
[sys.executable, "-c", _BLOCK, str(HELPER), "--root", str(tmp_path), *flags],
capture_output=True,
text=True,
)


def test_json_row_without_tomllib_is_an_error_row_not_a_traceback(tmp_path):
result = _run(tmp_path, "--json-row")
# runpy re-raises SystemExit(0) from main(); a traceback would mean the import crashed.
assert "Traceback" not in result.stderr, result.stderr
row = json.loads(result.stdout)
assert row["mode"] == "extras"
assert row["status"] == "error"
assert row["count"] is None
assert "Python >= 3.11" in row["summary"]


def test_summary_without_tomllib_renders_advisory_not_clean(tmp_path):
result = _run(tmp_path, "--summary")
assert "Traceback" not in result.stderr, result.stderr
count, _, text = result.stdout.strip().partition("|")
assert count == "-1" # hygiene.sh renders -1 as "advisory"; 0 would read "clean"
assert text.startswith("skipped:")


def test_hygiene_sh_runs_extras_on_a_tomllib_interpreter():
script = HELPER.parent / "hygiene.sh"
text = script.read_text()
assert 'helper_row _hygiene_extras.py extras "$TOML_PY"' in text
assert 'python3 "$HERE/_hygiene_extras.py"' not in text
Loading