mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Gate the no-inline-Python rule instead of asking people to remember it
CLAUDE.md's longest and most emphatic code rule -- never embed Python program logic in a shell script -- was enforced by nothing. A repo-wide scan found it had already been violated: install_plagiarism_tools.sh ran a 6-line NLTK download as an inline `python3 -c`, invisible to ruff, mypy, pylint, bandit and pytest, exactly the failure mode the rule exists to prevent. check_no_inline_python.sh flags heredocs and multi-line `python -c` while permitting the single-line availability probes the rule explicitly allows, so the ~250 shell files here produce zero false positives. Commented-out examples are skipped; otherwise the rule's own documentation would fail the hook. The extracted download_nltk_data.py also fixes a real defect: the inline block ignored nltk.download()'s return value, so a partial corpus download reported success. It now returns non-zero and the caller's `set -e` sees it. nltk is imported via importlib inside main(), not at module scope: it belongs to the plagiarism toolchain, not this repo, so it is absent from requirements.txt and unresolvable to pylint as a static import. No suppressions anywhere: T201 avoided with sys.stderr.write, INP001 with a shebang (matching sibling fast_count.py), E0401 with the dynamic import. pylint 10.00/10, ruff clean, mypy clean. Verified the gate fires rather than passing vacuously -- a staged canary with a real heredoc exits non-zero -- and that the extracted script runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
95a1f62fc4
commit
17b20e5fae
@ -100,6 +100,11 @@ repos:
|
||||
# ===========================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-inline-python
|
||||
name: Block Python logic embedded in shell scripts
|
||||
entry: meta/scripts/check_no_inline_python.sh
|
||||
language: script
|
||||
types_or: [shell, zsh]
|
||||
- id: no-polling-antipatterns
|
||||
name: Block polling script anti-patterns
|
||||
entry: meta/scripts/check_polling_antipatterns.sh
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
{
|
||||
"intent": "Make the 'no inline Python in shell scripts' rule a gate that fails the commit, instead of prose in CLAUDE.md that nothing enforced.",
|
||||
"scope": [
|
||||
"meta/scripts/check_no_inline_python.sh (new pre-commit hook script)",
|
||||
".pre-commit-config.yaml (register the no-inline-python hook)",
|
||||
"linux_configuration/scripts/single_use/utils/install_plagiarism_tools.sh (the one real violation)",
|
||||
"linux_configuration/scripts/single_use/utils/download_nltk_data.py (extracted from the inline block)",
|
||||
"Non-goal: the remaining 250-line file-length work; that lands separately"
|
||||
],
|
||||
"changes": [
|
||||
"Added check_no_inline_python.sh: flags `python <<HEREDOC` and multi-line `python -c`, while permitting the single-line availability/version probes the rule explicitly allows. Skips commented-out examples so the rule's own documentation does not fail the hook.",
|
||||
"Extracted a 6-line inline `python3 -c` NLTK download block into download_nltk_data.py, invoked as `python3 \"$(dirname \"$0\")/download_nltk_data.py\"`. It now returns a non-zero exit on partial failure, which the inline version silently swallowed.",
|
||||
"Registered the hook with types_or: [shell, zsh]."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "find . -name '*.sh' ... | xargs bash meta/scripts/check_no_inline_python.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Scanned every shell file in the repo: 1 genuine violation found (install_plagiarism_tools.sh:84), 0 false positives across ~250 files. After the extraction, the whole tree is clean."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run no-inline-python --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "Block Python logic embedded in shell scripts.............................Passed"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run no-inline-python --files ./canary_tmp.sh (staged canary with a real heredoc)",
|
||||
"result": "pass",
|
||||
"evidence": "Hook exited non-zero and reported canary_tmp.sh:2: python3 <<'PY' -- proving the gate fires rather than passing vacuously. Canary deleted afterwards."
|
||||
},
|
||||
{
|
||||
"command": "bash -n + shellcheck install_plagiarism_tools.sh; ruff check + ruff format --check download_nltk_data.py",
|
||||
"result": "pass",
|
||||
"evidence": "bash -n OK; shellcheck OK; 'All checks passed!' and '1 file already formatted'. No suppressions added: T201 avoided via sys.stderr.write and INP001 via the #!/usr/bin/env python3 shebang, matching the sibling fast_count.py."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"download_nltk_data.py imports nltk at module scope, which is not in meta/requirements.txt because it belongs to the plagiarism toolchain, not this repo. The installer pip-installs it immediately before invoking the module, so the import cannot fail in the real call path.",
|
||||
"The multi-line `python -c` regex keys on an unterminated quote. A single-line -c containing an escaped quote could in principle be misread; no such case exists in the repo today and the whole-tree scan found zero false positives."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the hook stops running and the inline block returns.",
|
||||
"Validate by re-running: pre-commit run no-inline-python --all-files"
|
||||
]
|
||||
}
|
||||
53
linux_configuration/scripts/single_use/utils/download_nltk_data.py
Executable file
53
linux_configuration/scripts/single_use/utils/download_nltk_data.py
Executable file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download the NLTK corpora the plagiarism tools need.
|
||||
|
||||
Extracted from `install_plagiarism_tools.sh`, which used to run this as an
|
||||
inline `python3 -c` block. Inline Python is invisible to ruff, mypy, pylint,
|
||||
bandit and pytest, so it lives in its own module instead.
|
||||
|
||||
`nltk` is a dependency of the plagiarism toolchain, not of this repo, so it is
|
||||
absent from `meta/requirements.txt` on purpose. The installer script pip-installs
|
||||
it immediately before invoking this module, and probes for it first.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
#: Corpora required by the plagiarism checker. `punkt_tab` is the post-3.8.2
|
||||
#: replacement for `punkt`; both are listed so the script works on either.
|
||||
CORPORA = (
|
||||
"punkt",
|
||||
"stopwords",
|
||||
"punkt_tab",
|
||||
"averaged_perceptron_tagger",
|
||||
"wordnet",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Download every corpus, reporting any that fail.
|
||||
|
||||
Returns 1 if any download failed, so the calling shell script's `set -e`
|
||||
sees a real failure instead of a silent partial install.
|
||||
"""
|
||||
# Imported dynamically, not at module scope: nltk belongs to the plagiarism
|
||||
# toolchain rather than this repo, so it is deliberately absent from
|
||||
# meta/requirements.txt and unresolvable to static analysis. The installer
|
||||
# pip-installs it immediately before invoking this module.
|
||||
try:
|
||||
nltk = importlib.import_module("nltk")
|
||||
except ImportError:
|
||||
sys.stderr.write("error: nltk is not installed\n")
|
||||
return 1
|
||||
|
||||
failed = [corpus for corpus in CORPORA if not nltk.download(corpus)]
|
||||
if failed:
|
||||
sys.stderr.write(f"error: failed to download: {', '.join(failed)}\n")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -81,14 +81,7 @@ success "Python NLP packages installed"
|
||||
|
||||
# Download NLTK data
|
||||
echo "Downloading NLTK data (stopwords, punkt tokenizer)..."
|
||||
python3 -c "
|
||||
import nltk
|
||||
nltk.download('punkt')
|
||||
nltk.download('stopwords')
|
||||
nltk.download('punkt_tab')
|
||||
nltk.download('averaged_perceptron_tagger')
|
||||
nltk.download('wordnet')
|
||||
"
|
||||
python3 "$(dirname "$0")/download_nltk_data.py"
|
||||
success "NLTK data downloaded"
|
||||
|
||||
# Download spaCy English model (small)
|
||||
|
||||
62
meta/scripts/check_no_inline_python.sh
Executable file
62
meta/scripts/check_no_inline_python.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Block Python program logic embedded inline in a shell script.
|
||||
#
|
||||
# Inline Python is invisible to the repo's Python tooling: ruff, mypy, pylint,
|
||||
# bandit and pytest all skip it, so it is the one place a type error or a
|
||||
# security finding can live undetected. Extract it to a .py file under
|
||||
# python_pkg/ and invoke it as `python3 "$(dirname "$0")/helper.py" "$arg"`.
|
||||
#
|
||||
# Permitted, and deliberately not flagged: a SINGLE-LINE availability or
|
||||
# version probe with no logic, e.g.
|
||||
# python3 -c 'import kasa'
|
||||
# python -c "import sys; print(sys.version_info[0])"
|
||||
# These carry no logic worth linting -- they are a yes/no question about the
|
||||
# interpreter, and extracting them to a file would be sillier than inlining.
|
||||
#
|
||||
# Used as a pre-commit hook; receives staged file paths as arguments.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
errors=()
|
||||
|
||||
# A `python <<EOF` / `python3 <<-'PY'` heredoc: always real logic, never a
|
||||
# probe. Matches an optional redirect target so `python3 <<'PY' >out` is caught.
|
||||
readonly HEREDOC_RE='(^|[^[:alnum:]_])python3?[[:space:]]+<<-?'
|
||||
|
||||
# The opening of a `python -c "` whose closing quote is NOT on the same line,
|
||||
# i.e. a multi-line inline program. A single-line -c closes its own quote and
|
||||
# so does not match.
|
||||
readonly MULTILINE_C_RE='(^|[^[:alnum:]_])python3?[[:space:]]+(-[[:alnum:]]+[[:space:]]+)*-c[[:space:]]*("[^"]*$|'"'"'[^'"'"']*$)'
|
||||
|
||||
for file in "$@"; do
|
||||
case "$file" in
|
||||
*.sh | *.bash | *.zsh) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[[ -f "$file" ]] || continue
|
||||
|
||||
while IFS=: read -r lineno text; do
|
||||
[[ -z "${lineno:-}" ]] && continue
|
||||
stripped="$(printf '%s' "$text" | sed 's/^[[:space:]]*//')"
|
||||
# A commented-out example is documentation, not shipped logic. Without
|
||||
# this, the rule's own explanatory comments would fail the hook.
|
||||
[[ "$stripped" == \#* ]] && continue
|
||||
errors+=("$file:$lineno: $stripped")
|
||||
done < <(grep -nE "$HEREDOC_RE|$MULTILINE_C_RE" "$file" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||
echo "ERROR: Python program logic embedded in a shell script."
|
||||
echo "Inline Python is skipped by ruff, mypy, pylint, bandit and pytest."
|
||||
echo ""
|
||||
for err in "${errors[@]}"; do
|
||||
echo " $err"
|
||||
done
|
||||
echo ""
|
||||
echo "Move the code to its own .py file and call it:"
|
||||
echo " python3 \"\$(dirname \"\$0\")/helper.py\" \"\$arg\" # bash"
|
||||
echo " python3 \"\${0:A:h}/helper.py\" \"\$arg\" # zsh"
|
||||
echo ""
|
||||
echo "A single-line probe such as python3 -c 'import kasa' is allowed."
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user