fix(install): keep the lint bootstrap off third-party imports

The lint installer imports this module, and it runs on the runner's bare
interpreter before a single dependency exists. Sharing the version check
with it therefore handed the bootstrap a PyYAML import, which killed it:
'ModuleNotFoundError: No module named yaml' raised through
utils/install/lint/__main__.py:35.

Routing the read through utils.cache.yaml would not have helped, that
imports yaml too. The requirements files are this repository's own and
hold one shape, so the pins are read with two regexes and the standard
library. A line that does not match the shape leaves the entry unpinned,
and unpinned already counts as unsatisfied, so the parser errs towards
installing rather than towards skipping.

Quoted versions are unwrapped: yaml accepts version: "13.4.0" and a
formatter may introduce it, and carrying the quotes into the comparison
would silently defeat the check instead of failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-16 21:45:56 +02:00
1 parent 0aa314bb9b
commit 67f5398985
2 files changed
+78 -13

No files matched your search

@@ -3,7 +3,7 @@ import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from utils.install.collections import unsatisfied
from utils.install.collections import declared_pins, unsatisfied
REQUIREMENTS = """---
collections:
@@ -29,6 +29,41 @@ def _install(collections_dir: Path, fqcn: str, version: str) -> None:
)
class TestDeclaredPins(unittest.TestCase):
def test_a_quoted_version_is_read_without_its_quotes(self):
body = (
"---\ncollections:\n"
' - name: double.quoted\n version: "13.4.0"\n'
" - name: single.quoted\n version: '1.0.0'\n"
" - name: commented\n version: 2.2.2 # pinned\n"
)
with TemporaryDirectory() as tmp:
root = Path(tmp)
requirements = _write_requirements(root, body)
self.assertEqual(
declared_pins(requirements),
[
("double.quoted", "13.4.0"),
("single.quoted", "1.0.0"),
("commented", "2.2.2"),
],
)
def test_an_entry_without_a_version_stays_unpinned(self):
body = "---\ncollections:\n - name: bare.entry\n"
with TemporaryDirectory() as tmp:
root = Path(tmp)
self.assertEqual(
declared_pins(_write_requirements(root, body)), [("bare.entry", None)]
)
def test_a_missing_file_declares_nothing(self):
with TemporaryDirectory() as tmp:
self.assertEqual(declared_pins(Path(tmp) / "absent.yml"), [])
class TestUnsatisfied(unittest.TestCase):
def test_every_pin_installed_at_its_version_is_satisfied(self):
with TemporaryDirectory() as tmp:
+42 -12
View File
@@ -1,12 +1,48 @@
"""Compare the pinned Galaxy requirements against what is on disk."""
"""Compare the pinned Galaxy requirements against what is on disk.
Imported by the lint bootstrap, which runs on a bare interpreter before any
dependency is installed, so this module stays on the standard library. The
requirements files it reads are this repository's own and hold one shape:
a ``collections:`` list of ``- name:`` entries with an optional ``version:``.
A line that does not match that shape leaves the entry unpinned, which the
caller treats as unsatisfied and installs.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
import yaml
_NAME = re.compile(r"^\s*-\s+name:\s*(?P<name>[A-Za-z0-9_.]+)\s*(?:#.*)?$")
_VERSION = re.compile(
r"^\s*version:\s*(?P<quote>[\"']?)(?P<version>[^\s#\"']+)(?P=quote)\s*(?:#.*)?$"
)
def declared_pins(requirements_file: Path) -> list[tuple[str, str | None]]:
"""The ``(fqcn, version)`` pairs a requirements file declares, in order.
Args:
requirements_file: the requirements.galaxy.yml to read.
"""
pins: list[tuple[str, str | None]] = []
try:
lines = requirements_file.read_text(encoding="utf-8").splitlines()
except OSError:
return pins
for line in lines:
name_match = _NAME.match(line)
if name_match:
pins.append((name_match.group("name"), None))
continue
version_match = _VERSION.match(line)
if version_match and pins:
fqcn, _ = pins[-1]
pins[-1] = (fqcn, version_match.group("version"))
return pins
def _installed_version(collections_dir: Path, namespace: str, name: str) -> str | None:
@@ -38,19 +74,13 @@ def unsatisfied(requirements_file: Path, collections_dir: Path) -> list[str]:
requirements_file: the requirements.galaxy.yml to read.
collections_dir: the ``-p`` target ansible-galaxy installs into.
"""
declared = yaml.safe_load(requirements_file.read_text(encoding="utf-8")) or {}
missing: list[str] = []
for entry in declared.get("collections") or []:
if not isinstance(entry, dict):
missing.append(str(entry))
continue
fqcn = str(entry.get("name", ""))
pinned = entry.get("version")
if not fqcn or "." not in fqcn or not pinned:
missing.append(fqcn or repr(entry))
for fqcn, pinned in declared_pins(requirements_file):
if "." not in fqcn or not pinned:
missing.append(fqcn)
continue
namespace, _, name = fqcn.partition(".")
if _installed_version(collections_dir, namespace, name) != str(pinned):
if _installed_version(collections_dir, namespace, name) != pinned:
missing.append(f"{fqcn}:{pinned}")
return missing