mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
fix(dev-python): take the supported Python range from requires-python
bf9c5be173 capped the bootstrap at 3.14 with constants inside install.sh, a
second copy of the range pyproject.toml already declares. requires-python is
now the single source (>=3.11,<3.15): install.sh derives its floor and cap
from it, in the repository or, on a remote host where the script runs alone,
from INFINITO_PYTHON_REQUIRES that the dev-python role reads from the same
file; any other form fails loudly. The Dockerfile copies pyproject.toml
before the bootstrap, and pip now refuses 3.15 for the package as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf9c5be173
commit
76adaeb9e8
5 files changed
+80
-7
No files matched your search
@@ -29,6 +29,7 @@ RUN set -euo pipefail; \
|
||||
COPY default.env ${INFINITO_SRC_DIR}/default.env
|
||||
COPY scripts/install ${INFINITO_SRC_DIR}/scripts/install
|
||||
COPY roles/dev-python/files/shell ${INFINITO_SRC_DIR}/roles/dev-python/files/shell
|
||||
COPY pyproject.toml ${INFINITO_SRC_DIR}/pyproject.toml
|
||||
COPY roles/update/files/shell/centos_mirror.sh ${INFINITO_SRC_DIR}/roles/update/files/shell/centos_mirror.sh
|
||||
COPY requirements ${INFINITO_SRC_DIR}/requirements
|
||||
COPY utils/__init__.py ${INFINITO_SRC_DIR}/utils/__init__.py
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ name = "infinito-nexus"
|
||||
version = "16.2.3"
|
||||
description = "Infinito.Nexus"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.11,<3.15"
|
||||
license = { file = "LICENSE.md" }
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PYTHON_MIN_MINOR=11
|
||||
PYTHON_MAX_MINOR=14
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
|
||||
DOCKER_APT_SANITIZER="${REPO_ROOT}/roles/sys-svc-container/files/shell/apt_repo.sh"
|
||||
|
||||
python_requires_spec() {
|
||||
if [[ -v INFINITO_PYTHON_REQUIRES ]]; then
|
||||
printf '%s\n' "${INFINITO_PYTHON_REQUIRES}"
|
||||
return 0
|
||||
fi
|
||||
local pyproject="${REPO_ROOT}/pyproject.toml"
|
||||
if [[ ! -f "${pyproject}" ]]; then
|
||||
echo "[ERROR] Neither INFINITO_PYTHON_REQUIRES nor ${pyproject} names the supported Python range." >&2
|
||||
return 1
|
||||
fi
|
||||
sed -n -E 's/^requires-python[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "${pyproject}"
|
||||
}
|
||||
|
||||
load_python_bounds() {
|
||||
local spec pattern='^>=3\.([0-9]+),<3\.([0-9]+)$'
|
||||
spec="$(python_requires_spec)"
|
||||
if [[ ! "${spec}" =~ ${pattern} ]]; then
|
||||
echo "[ERROR] requires-python '${spec}' must read >=3.X,<3.Y." >&2
|
||||
return 1
|
||||
fi
|
||||
PYTHON_MIN_MINOR="${BASH_REMATCH[1]}"
|
||||
PYTHON_MAX_MINOR="$((BASH_REMATCH[2] - 1))"
|
||||
}
|
||||
|
||||
load_python_bounds
|
||||
|
||||
log() {
|
||||
printf '>>> [dev-python] %s\n' "$*"
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
name: sys-pip
|
||||
when: run_once_sys_pip is not defined
|
||||
|
||||
- name: 📦 Install and prefer Python 3.11+ as default
|
||||
- name: 📦 Install and prefer the highest supported Python as default
|
||||
ansible.builtin.script: "{{ role_path }}/files/shell/install.sh"
|
||||
environment:
|
||||
SKIP_DOCKER_APT_SANITIZER: "1"
|
||||
INFINITO_PYTHON_REQUIRES: >-
|
||||
{{ lookup('file', lookup('path_absolute', 'pyproject.toml'))
|
||||
| regex_search('(?m)^requires-python\s*=\s*"([^"]+)"', '\1')
|
||||
| first }}
|
||||
become: true
|
||||
|
||||
- include_tasks: utils/once/flag.yml
|
||||
@@ -1,31 +1,55 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from utils.cache.files import PROJECT_ROOT
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
|
||||
SCRIPT = PROJECT_ROOT / "roles/dev-python/files/shell/install.sh"
|
||||
ROLE_TASKS = PROJECT_ROOT / "roles/dev-python/tasks/00_core.yml"
|
||||
|
||||
|
||||
def _call(snippet: str, *args: str) -> subprocess.CompletedProcess:
|
||||
def _call(
|
||||
snippet: str, *args: str, requires: str | None = None
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Source install.sh without running its entrypoint, then run a snippet.
|
||||
|
||||
Args:
|
||||
snippet: bash code evaluated after sourcing; sees ``args`` as ``"$@"``.
|
||||
args: positional arguments handed to the snippet.
|
||||
requires: value for ``INFINITO_PYTHON_REQUIRES``; ``None`` leaves it
|
||||
unset so the script reads the repository's ``pyproject.toml``.
|
||||
|
||||
Returns:
|
||||
The completed bash process.
|
||||
"""
|
||||
env = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
if key != "INFINITO_PYTHON_REQUIRES"
|
||||
}
|
||||
if requires is not None:
|
||||
env["INFINITO_PYTHON_REQUIRES"] = requires
|
||||
return subprocess.run(
|
||||
["bash", "-c", f'source "$1"; shift; {snippet}', "bash", str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
def _declared_minors() -> tuple[int, int]:
|
||||
spec = tomllib.loads(read_text(str(PROJECT_ROOT / "pyproject.toml")))["project"][
|
||||
"requires-python"
|
||||
]
|
||||
match = re.fullmatch(r">=3\.(\d+),<3\.(\d+)", spec)
|
||||
return int(match[1]), int(match[2]) - 1
|
||||
|
||||
|
||||
def _bound(name: str) -> int:
|
||||
return int(_call(f'printf "%s" "${name}"').stdout)
|
||||
|
||||
@@ -162,5 +186,26 @@ class TestFindHighestPythonPackage(unittest.TestCase):
|
||||
self.assertEqual(result.stdout.strip(), f"python3.{self.cap - 1}")
|
||||
|
||||
|
||||
class TestBoundsFromRequiresPython(unittest.TestCase):
|
||||
def test_the_bounds_come_from_pyproject_requires_python(self) -> None:
|
||||
self.assertEqual(
|
||||
(_bound("PYTHON_MIN_MINOR"), _bound("PYTHON_MAX_MINOR")),
|
||||
_declared_minors(),
|
||||
)
|
||||
|
||||
def test_the_role_hands_requires_python_to_the_remote_script(self) -> None:
|
||||
result = _call(
|
||||
'printf "%s %s" "$PYTHON_MIN_MINOR" "$PYTHON_MAX_MINOR"',
|
||||
requires=">=3.12,<3.14",
|
||||
)
|
||||
self.assertEqual(result.stdout, "12 13")
|
||||
self.assertIn("INFINITO_PYTHON_REQUIRES", read_text(str(ROLE_TASKS)))
|
||||
|
||||
def test_an_unsupported_requires_python_form_fails_loudly(self) -> None:
|
||||
result = _call("true", requires=">=3.11")
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("must read >=3.X,<3.Y", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user