build(m1): pin toolchain and guard v1.3.0 in pyproject
Plan Step 2. Verify green: `bash scripts/bootstrap.sh && .venv/bin/python -m
pytest tests/test_toolchain.py` -> 3 passed, exit 0.
Test first, and the RED was demonstrated rather than assumed: run against a
venv carrying pytest and the guard but not pytest-socket, the third test fails
with ModuleNotFoundError while the other two pass. The test discriminates.
pyproject.toml declares the dependencies as a PEP 735 group rather than an
installable distribution -- nothing here needs a build backend, since the tests
import from scripts/ via pytest's pythonpath. Measured before choosing it: pip
26.2.1 on this machine supports `install --group`, and bootstrap upgrades pip
first, so a pip too old fails loudly instead of silently skipping the guard.
The guard is pinned to the exact tag v1.3.0 and is not vendored. Verified the
tag resolves and the package exposes __version__ == "1.3.0"; that equality is
what the test asserts, not the pip metadata.
addopts carries --disable-socket and --strict-markers, which is why the
`network` marker is declared: with strict markers an undeclared marker would
make `-m 'not network'` an error rather than a filter, and the offline
guarantee would quietly stop being tested.
bootstrap.sh builds under ${CLAUDE_PLUGIN_DATA} when set (survives a plugin
update) and falls back to the repo-root .venv for development -- risk H2. It
refuses an interpreter below 3.10 instead of building on it, because python3
here can resolve to 3.9.6 under a GUI-spawned process.
Exercised, not just written: re-running is idempotent (exit 0); --med-xlsx
installs the extra (openpyxl 3.1.5 importable); an unknown argument exits 2
with usage; a missing JOBBSOK_PYTHON interpreter refuses loudly. bash 3.2-clean
(system /bin/bash 3.2.57 -n passes) and ASCII-only.
.gitignore needed no change: `.venv/` was already on line 12 from the initial
commit, and `git check-ignore -v .venv` confirms it matches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0f92b2a1c3
commit
24768ef66b
3 changed files with 175 additions and 0 deletions
37
pyproject.toml
Normal file
37
pyproject.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Toolchain contract for the jobbsok plugin (plan Step 2).
|
||||||
|
#
|
||||||
|
# This is deliberately NOT an installable distribution. Nothing here needs a
|
||||||
|
# build backend: the tests import from `scripts/` via pytest's `pythonpath`,
|
||||||
|
# and the dependencies are declared as a PEP 735 dependency group that
|
||||||
|
# `scripts/bootstrap.sh` installs with `pip install --group dev`. Adding a
|
||||||
|
# build backend would buy a packaging step this repo never uses.
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8",
|
||||||
|
"pytest-socket",
|
||||||
|
# Pinned to an exact tag, and not vendored under any circumstances. The
|
||||||
|
# guard is the trust boundary for M3 ingestion; a floating version is not
|
||||||
|
# a boundary. A tag that cannot be resolved blocks M3 -- it is not a cue
|
||||||
|
# to substitute a different one.
|
||||||
|
"llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.3.0",
|
||||||
|
]
|
||||||
|
# Optional extra, installed by `scripts/bootstrap.sh --med-xlsx`. Declared here
|
||||||
|
# so the spreadsheet export path is reachable on a clean bootstrap instead of
|
||||||
|
# being asserted against a library nothing installs.
|
||||||
|
xlsx = [
|
||||||
|
"openpyxl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["scripts"]
|
||||||
|
# --disable-socket makes the offline guarantee enforced rather than intended:
|
||||||
|
# any test that opens a socket fails instead of quietly reaching the network.
|
||||||
|
# --strict-markers means an undeclared marker is an error, which is why
|
||||||
|
# `network` is declared below -- without it, `-m 'not network'` would itself
|
||||||
|
# be the error.
|
||||||
|
addopts = "-q --strict-markers --disable-socket -m 'not network'"
|
||||||
|
markers = [
|
||||||
|
"network: test needs real network access; deselected by default and never run in CI",
|
||||||
|
]
|
||||||
91
scripts/bootstrap.sh
Executable file
91
scripts/bootstrap.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build the jobbsok Python environment and pin the ingestion guard.
|
||||||
|
#
|
||||||
|
# bash 3.2-clean and ASCII-only on purpose: the system bash on this Mac is 3.2,
|
||||||
|
# and a multibyte character has crashed a `set -u` script here before.
|
||||||
|
#
|
||||||
|
# Where the environment lives is load-bearing, not incidental. When
|
||||||
|
# CLAUDE_PLUGIN_DATA is set the environment is built there, because that
|
||||||
|
# directory survives a plugin update -- the plugin's own cache does not. A
|
||||||
|
# repo-root-only .venv would leave every non-development install without the
|
||||||
|
# guard, which is risk H2. The repo-root .venv is the development case.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/bootstrap.sh # dev dependencies
|
||||||
|
# bash scripts/bootstrap.sh --med-xlsx # also the openpyxl extra
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
MIN_MAJOR=3
|
||||||
|
MIN_MINOR=10
|
||||||
|
|
||||||
|
WITH_XLSX=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--med-xlsx) WITH_XLSX=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,20p' "$0"
|
||||||
|
exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "bootstrap: unknown argument: $arg" >&2
|
||||||
|
echo "bootstrap: usage: bash scripts/bootstrap.sh [--med-xlsx]" >&2
|
||||||
|
exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||||
|
|
||||||
|
if [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
||||||
|
VENV_DIR="${CLAUDE_PLUGIN_DATA}/venv"
|
||||||
|
VENV_KIND="plugin-data (survives a plugin update)"
|
||||||
|
else
|
||||||
|
VENV_DIR="${REPO_ROOT}/.venv"
|
||||||
|
VENV_KIND="repo root (development)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Refuse an interpreter that is too old rather than building an environment
|
||||||
|
# that parses everything and fails at runtime. Under a GUI-spawned process with
|
||||||
|
# an empty PATH, python3 here can resolve to 3.9.6.
|
||||||
|
PYTHON_BIN="${JOBBSOK_PYTHON:-python3}"
|
||||||
|
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "bootstrap: no such interpreter: $PYTHON_BIN" >&2
|
||||||
|
echo "bootstrap: set JOBBSOK_PYTHON to a Python ${MIN_MAJOR}.${MIN_MINOR}+ executable." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! "$PYTHON_BIN" -c "import sys; sys.exit(0 if sys.version_info >= ($MIN_MAJOR, $MIN_MINOR) else 1)"; then
|
||||||
|
FOUND=$("$PYTHON_BIN" -c "import sys; print('%d.%d.%d' % sys.version_info[:3])")
|
||||||
|
echo "bootstrap: $PYTHON_BIN is Python $FOUND; ${MIN_MAJOR}.${MIN_MINOR}+ is required." >&2
|
||||||
|
echo "bootstrap: refusing to build an environment on it. Set JOBBSOK_PYTHON instead." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "bootstrap: interpreter $("$PYTHON_BIN" -c 'import sys; print(sys.executable)')"
|
||||||
|
echo "bootstrap: environment $VENV_DIR [$VENV_KIND]"
|
||||||
|
|
||||||
|
if [ ! -x "${VENV_DIR}/bin/python" ]; then
|
||||||
|
mkdir -p "$(dirname "$VENV_DIR")"
|
||||||
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VENV_PY="${VENV_DIR}/bin/python"
|
||||||
|
|
||||||
|
"$VENV_PY" -m pip install --quiet --upgrade pip
|
||||||
|
# PEP 735 dependency groups need pip 25.1+; the upgrade above is what makes
|
||||||
|
# this line safe, and a pip too old to understand --group fails loudly here.
|
||||||
|
"$VENV_PY" -m pip install --quiet --group "${REPO_ROOT}/pyproject.toml:dev"
|
||||||
|
if [ "$WITH_XLSX" -eq 1 ]; then
|
||||||
|
"$VENV_PY" -m pip install --quiet --group "${REPO_ROOT}/pyproject.toml:xlsx"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$VENV_PY" - <<'PYEOF'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import llm_ingestion_guard
|
||||||
|
|
||||||
|
print("bootstrap: sys.executable %s" % sys.executable)
|
||||||
|
print("bootstrap: python %s" % sys.version.split()[0])
|
||||||
|
print("bootstrap: guard %s" % llm_ingestion_guard.__version__)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo "bootstrap: ok"
|
||||||
47
tests/test_toolchain.py
Normal file
47
tests/test_toolchain.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""The toolchain contract for every later test in this repo (plan Step 2).
|
||||||
|
|
||||||
|
Three things have to hold before anything else is worth running, and each has
|
||||||
|
already bitten this project once:
|
||||||
|
|
||||||
|
1. The interpreter is 3.10 or newer. A GUI-spawned process on this Mac can
|
||||||
|
resolve `python3` to 3.9.6, which parses the code and then fails at runtime.
|
||||||
|
2. The ingestion guard is present and pinned to exactly v1.3.0. It is the trust
|
||||||
|
boundary for M3; a floating version is not a boundary.
|
||||||
|
3. pytest-socket is installed and the `network` marker is registered. The suite
|
||||||
|
runs with `--disable-socket --strict-markers`, so an unregistered marker
|
||||||
|
would make `-m 'not network'` an error rather than a filter -- and the
|
||||||
|
offline guarantee would quietly stop being tested.
|
||||||
|
|
||||||
|
This is the first test file in the repo and sets the style for the rest.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpreter_is_3_10_or_newer():
|
||||||
|
assert sys.version_info >= (3, 10), (
|
||||||
|
"the toolchain requires Python 3.10+, got %s from %s"
|
||||||
|
% (sys.version.split()[0], sys.executable)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_guard_is_pinned_to_1_3_0():
|
||||||
|
import llm_ingestion_guard
|
||||||
|
|
||||||
|
assert llm_ingestion_guard.__version__ == "1.3.0", (
|
||||||
|
"guard must be pinned to v1.3.0, got %r -- a different tag is not a "
|
||||||
|
"substitute and the source is never vendored"
|
||||||
|
% (llm_ingestion_guard.__version__,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pytest_socket_installed_and_network_marker_registered(pytestconfig):
|
||||||
|
import pytest_socket # noqa: F401 -- import IS the assertion
|
||||||
|
|
||||||
|
markers = pytestconfig.getini("markers")
|
||||||
|
names = [m.split(":", 1)[0].split("(", 1)[0].strip() for m in markers]
|
||||||
|
assert "network" in names, (
|
||||||
|
"the 'network' marker must be declared in pyproject.toml; with "
|
||||||
|
"--strict-markers an undeclared marker makes -m 'not network' an "
|
||||||
|
"error, not a filter. Declared markers: %r" % (names,)
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue