mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-13 14:16:46 +00:00
req-011 — descriptive role-level metadata
* Move logo / homepage / video / display out of
meta/main.yml.galaxy_info into a new project-owned
roles/<role>/meta/info.yml (file-root convention, no `info:` wrapper).
* 78 role meta/main.yml files migrated via ruamel round-trip
(key-order / quoting / inline-comments preserved). Roles with none
of the four fields are left untouched (no empty info.yml created).
* web-app-dashboard/lookup_plugins/docker_cards.py reads logo +
display from meta/info.yml; description / galaxy_tags continue to
come from galaxy_info (Galaxy-spec fields).
* docker_cards.py now routes ALL its file reads through the
process-wide caches: load_yaml_any for vars/main.yml,
meta/main.yml, meta/info.yml; read_text from utils.cache.files for
README.md. The orphan `import yaml` is gone.
New utils/cache/files (renamed from tests/utils/fs)
* The process-wide filesystem cache (read_text, iter_project_files,
iter_project_files_with_content, _all_project_files) is not
test-specific; it now lives under utils/cache/files.py next to the
other process-wide caches. 34 consumer imports rewritten across
tests/ and docs/. The empty tests/utils/ package is removed.
* New unit suite tests/unit/utils/cache/test_files.py — 12 cases
covering read_text, iter_project_files (skip-dirs, extensions,
exclude_tests, exclude_dirs, walk-cache invariants), the combined
iterator, non-UTF-8 silent skip, and _reset.
utils/cache redundancy cleanup
* applications.py + users.py + cli/create/inventory/services_disabler.py
drop their direct yaml.safe_load calls and their private
_load_yaml_mapping/_load_yaml_variant_list copies; everything routes
through utils.cache.yaml.load_yaml / load_yaml_any. The dead
utils.cache.base._load_yaml_mapping is removed.
* users.py:_load_store_users (tokens.yml read) likewise migrated.
* _load_yaml_variant_list moves out of base.py to applications.py
(its only consumer) and is renamed _load_variants_overrides — also
routes through the cached loader now.
* utils.cache.yaml._reset_cache_for_tests is renamed _reset (kept as
alias) for parity with the per-domain reset helpers.
* utils.cache.files grows a _reset() that clears its two LRU caches.
* utils.cache.__init__._reset_cache_for_tests now orchestrates ALL
six caches (applications, users, domains, base, yaml, files) so
tests get clean state across cache layers — needed because
applications/users now share the yaml cache and would otherwise
serve stale parses across reset boundaries.
* utils.cache.files imports PROJECT_ROOT from base instead of
redefining it.
* tests/unit/utils/cache/test_data.py + test_base.py: obsolete
TestLoadYamlMapping / TestLoadYamlVariantList classes deleted
(covered by test_yaml.py + test_variants.py).
* README.md updated for the new module list (files, yaml) and the
six-module orchestrator.
Galaxy schema lint
* tests/lint/repository/test_role_meta_main_galaxy_schema.py: split
galaxy_info field requirements into hard-required {author,
description, license} and soft-recommended {min_ansible_version,
platforms}. Soft-recommended -> emit a GitHub Actions ::warning::
annotation via utils.annotations.message.warning (titled
"meta/main.yml galaxy_info" with file= pointing at the offender),
no test failure. Rationale: min_ansible_version + platforms are
only enforced by Galaxy when *publishing*, which Infinito.Nexus
does not do.
* Galaxy lint goes green (was 226 offenders before req-011 +
license_url/repository/documentation strip; was 62 before this
relaxation; now 0 hard offenders, 62 soft warnings).
* roles/svc-opt-keyboard-color/meta/main.yml: backfill `license`
(only role missing the hard-required field).
New lint
* tests/lint/repository/test_role_meta_info.py — enforces meta/info.yml
file-root convention, the four-field allowlist (logo, homepage,
video, display), and shape (logo.class string, homepage/video
non-empty strings, display bool). Also fails if any
meta/main.yml.galaxy_info still carries one of the four migrated
fields.
Docs
* docs/contributing/design/services/layout.md: new section
"Descriptive Role-Level Metadata — meta/info.yml" with allowed-
fields table, optionality rule, materialised path, and consumer
note. File-Layout table grows the meta/info.yml row;
meta/main.yml row gets the explicit "no logo/homepage/video/display"
note. req-011 added to header and Related Pages.
* utils/cache/README.md: files.py + yaml.py rows added; six-module
reset orchestrator documented.
Validation
* Unit: 1573 passed.
* Lint: 55 passed.
* Integration: 161 passed (1 skipped) via pre-commit hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
"""Process-level cached filesystem helpers for project-tree scans.
|
|
|
|
Many integration/lint tests and CLI tools walk the whole repo and read every
|
|
matching file. Doing that N times per process is wasteful — the tree doesn't
|
|
change during a run. These helpers walk the tree once per process and memoise
|
|
both the path list and file contents, so every caller after the first one pays
|
|
near-zero cost.
|
|
|
|
Usage:
|
|
|
|
from utils.cache.files import iter_project_files, read_text
|
|
|
|
for path in iter_project_files(extensions=(".yml", ".yml.j2"), exclude_tests=True):
|
|
content = read_text(path)
|
|
...
|
|
|
|
`PROJECT_ROOT` is the repo root derived from this file's location.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from functools import lru_cache
|
|
from typing import Iterable, Iterator, Tuple
|
|
|
|
from .base import PROJECT_ROOT
|
|
|
|
# Directories never worth descending into during project-tree scans.
|
|
_DEFAULT_SKIP_DIRS: frozenset[str] = frozenset(
|
|
{
|
|
".git",
|
|
".cache",
|
|
".pytest_cache",
|
|
".ruff_cache",
|
|
".venv",
|
|
"venv",
|
|
"__pycache__",
|
|
"node_modules",
|
|
}
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _all_project_files() -> tuple[str, ...]:
|
|
"""One-shot filesystem walk from PROJECT_ROOT.
|
|
|
|
Returns absolute paths as a tuple (immutable → safe to cache).
|
|
Pruned at walk-time by `_DEFAULT_SKIP_DIRS`, since those dirs are never
|
|
relevant for any project-tree test and descending into them is pure waste.
|
|
"""
|
|
root_str = str(PROJECT_ROOT)
|
|
paths: list[str] = []
|
|
for dirpath, dirnames, filenames in os.walk(root_str, topdown=True):
|
|
# prune in-place (topdown=True): stops descent into skipped dirs
|
|
dirnames[:] = [d for d in dirnames if d not in _DEFAULT_SKIP_DIRS]
|
|
for fn in filenames:
|
|
paths.append(os.path.join(dirpath, fn))
|
|
return tuple(paths)
|
|
|
|
|
|
def iter_project_files(
|
|
*,
|
|
extensions: Iterable[str] | None = None,
|
|
exclude_tests: bool = False,
|
|
exclude_dirs: Iterable[str] | None = None,
|
|
) -> Iterator[str]:
|
|
"""Yield absolute paths of project files matching the filter.
|
|
|
|
Args:
|
|
extensions: Suffix strings (e.g. ``(".yml", ".yaml.j2")``). Matching is
|
|
case-insensitive via plain string suffix. ``None`` yields all files.
|
|
exclude_tests: Skip everything under ``<root>/tests/``.
|
|
exclude_dirs: Additional top-level-or-nested directory names to skip.
|
|
Matched by path-component containment, not by prefix. E.g.
|
|
``exclude_dirs=("docs",)`` skips any file whose path contains a
|
|
``docs`` segment.
|
|
"""
|
|
all_files = _all_project_files()
|
|
tests_prefix = os.path.join(str(PROJECT_ROOT), "tests") + os.sep
|
|
ext_lower = tuple(e.lower() for e in extensions) if extensions else None
|
|
exclude_dirs_set = set(exclude_dirs) if exclude_dirs else None
|
|
|
|
for path in all_files:
|
|
if exclude_tests and path.startswith(tests_prefix):
|
|
continue
|
|
if exclude_dirs_set is not None:
|
|
# Cheap path-segment check; avoids splitting the full path.
|
|
rel = os.path.relpath(path, str(PROJECT_ROOT))
|
|
segments = rel.split(os.sep)
|
|
if exclude_dirs_set.intersection(segments):
|
|
continue
|
|
if ext_lower is not None:
|
|
lower = path.lower()
|
|
if not any(lower.endswith(e) for e in ext_lower):
|
|
continue
|
|
yield path
|
|
|
|
|
|
@lru_cache(maxsize=8192)
|
|
def read_text(path: str) -> str:
|
|
"""Return the UTF-8 text content of a file; cached process-wide.
|
|
|
|
Raises OSError / UnicodeDecodeError unchanged — callers that scan arbitrary
|
|
files can wrap this in try/except if non-UTF-8 blobs may appear.
|
|
"""
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def iter_project_files_with_content(
|
|
*,
|
|
extensions: Iterable[str] | None = None,
|
|
exclude_tests: bool = False,
|
|
exclude_dirs: Iterable[str] | None = None,
|
|
) -> Iterator[Tuple[str, str]]:
|
|
"""Yield ``(path, content)`` for matching files; content is cached.
|
|
|
|
Files that fail to read (permission errors, non-UTF-8) are silently
|
|
skipped — that matches the prior caller behaviour of wrapping
|
|
``open().read()`` in a bare ``try/except Exception: continue``.
|
|
"""
|
|
for path in iter_project_files(
|
|
extensions=extensions,
|
|
exclude_tests=exclude_tests,
|
|
exclude_dirs=exclude_dirs,
|
|
):
|
|
try:
|
|
content = read_text(path)
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
yield path, content
|
|
|
|
|
|
def _reset() -> None:
|
|
"""Test-only helper: clear the project-walk and file-content caches.
|
|
|
|
Named ``_reset`` for parity with the per-domain reset helpers in
|
|
sibling modules; ``utils.cache._reset_cache_for_tests`` orchestrates
|
|
calls to all of them.
|
|
"""
|
|
_all_project_files.cache_clear()
|
|
read_text.cache_clear()
|