Files
kevinveenbirkenbachandClaude Opus 5 fe7a9d53ea fix(cli,utils): settle three CodeQL error findings
CodeQL reports cmd and sort_spec as possibly uninitialised after the except branch that calls parser.error, because it does not model the argparse error method as NoReturn. It is: usage is printed and the process exits with status 2. An explicit return 2 on that branch states the exit status the call already produces, so the analyser and a reader see the same path.

The retry loop in the registry client counted two attempts in a variable the body never read; the underscore prefix was the convention for that, but CodeQL only honours the bare name.

No path changes behaviour; the three alerts stop pointing at correct code so the scanner stays readable for the ones that are not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:40:13 +02:00

417 lines
15 KiB
Python

"""Argument parsing, sorting/filtering and the ``main`` entry point."""
from __future__ import annotations
import argparse
import re
import sys
from typing import TYPE_CHECKING, Any
from utils import PROJECT_ROOT
from utils.cache.applications import get_variants
from utils.roles.applications.topics import overridden_providers
if TYPE_CHECKING:
from collections.abc import Mapping
from .filter import FilterError, compile_predicate
from .model import (
TESTED_LIFECYCLES,
ComplexityRow,
attach_siblings,
compute_complexity_rows,
compute_variant_complexity_rows,
)
from .render import render_json, render_string, render_table, render_yaml
_SORT_KEYS = {
"embeds": lambda r: r.embeds,
"consumers": lambda r: r.consumers,
"weight": lambda r: r.weight,
"name": lambda r: r.name,
"random": lambda r: r.random,
"variant": lambda r: r.variant if r.variant is not None else -1,
"variants": lambda r: r.variants,
"id": lambda r: r.id,
"covered_by": lambda r: r.covered_by,
"lifecycle": lambda r: r.lifecycle,
"compose": lambda r: int(r.compose),
"swarm": lambda r: int(r.swarm),
"host": lambda r: int(r.host),
"stack": lambda r: int(r.stack),
"test_compose": lambda r: int(r.test_compose),
"test_swarm": lambda r: int(r.test_swarm),
"test_host": lambda r: int(r.test_host),
"integrated": lambda r: int(r.integrated),
"clone": lambda r: int(r.clone),
}
def _row_fields(r: ComplexityRow) -> dict[str, Any]:
"""The ``--filter`` view of a row: scalar fields keyed by name. ``row`` and
``jobs`` are excluded (not assigned until after filtering)."""
return {
"name": r.name,
"lifecycle": r.lifecycle,
"dna": r.dna,
"clone": r.clone,
"embeds": r.embeds,
"embeds_direct": r.embeds_direct,
"consumers": r.consumers,
"consumers_direct": r.consumers_direct,
"weight": r.weight,
"variants": r.variants,
"id": r.id,
"covered_by": r.covered_by,
"variant": r.variant if r.variant is not None else -1,
"siblings": len(r.siblings),
"random": r.random,
"compose": r.compose,
"swarm": r.swarm,
"host": r.host,
"stack": r.stack,
"test_compose": r.test_compose,
"test_swarm": r.test_swarm,
"test_host": r.test_host,
"integrated": r.integrated,
}
FILTER_FIELDS = frozenset(
_row_fields(ComplexityRow("", 0, [], 0, [], 0, [], 0, [], 0, "", []))
)
_DIRECTIONS = {"asc": False, "desc": True}
DEFAULT_SORT = "asc embeds"
def parse_lifecycles(tokens: list[str] | None) -> set[str] | None:
"""Normalise ``--lifecycles`` tokens into a lowercase set, splitting each on
commas and whitespace so ``'alpha,beta'`` and ``'alpha beta'`` are
equivalent. ``None`` (flag absent) stays ``None`` so the model falls back to
its built-in default envelope."""
if not tokens:
return None
values = {
part.strip().lower()
for token in tokens
for part in re.split(r"[,\s]+", token)
if part.strip()
}
return values or None
def parse_sort_spec(spec: str) -> list[tuple[str, bool]]:
"""Parse a ``--sort`` value into an ordered ``[(column, reverse), ...]``.
Args:
spec: Comma-separated clauses, each a column optionally prefixed or
suffixed with a direction, e.g. ``"desc embeds, asc total"``.
Direction defaults to ``asc``.
Returns:
Clauses in significance order (first = primary sort key). ``reverse``
is True for ``desc``.
Raises:
ValueError: An unknown token, or a clause naming no column.
"""
out: list[tuple[str, bool]] = []
for clause in spec.split(","):
tokens = clause.split()
if not tokens:
continue
reverse = False
column: str | None = None
for token in tokens:
low = token.lower()
if low in _DIRECTIONS:
reverse = _DIRECTIONS[low]
elif low in _SORT_KEYS:
column = low
else:
raise ValueError(
f"invalid --sort token {token!r}; expected a direction "
f"(asc/desc) or a column ({', '.join(_SORT_KEYS)})"
)
if column is None:
raise ValueError(f"--sort clause {clause!r} names no column")
out.append((column, reverse))
if not out:
raise ValueError("--sort requires at least one column")
return out
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="infinito meta roles applications complexity",
description=(
"For every application role, list its transitively resolved "
"shared-service dependencies and the resulting complexity score."
),
formatter_class=argparse.RawTextHelpFormatter,
)
p.add_argument(
"--sort",
default=DEFAULT_SORT,
metavar="SPEC",
help=(
"Sort order: comma-separated columns, each optionally prefixed "
"with a direction, applied in order so later columns break ties "
"of earlier ones. e.g. 'desc embeds, asc weight' sorts by embeds "
"descending, then by weight ascending within equal embeds. "
"Direction defaults to 'asc'. Columns: 'embeds' (service deps the "
"role embeds), 'consumers' (roles that embed this one), "
"'weight' (sum of direct + transitive in both directions), "
"'name' (alphabetical), 'random' (the per-row nonce), 'variant' / "
"'variants' (the variant index / the role's variant count), 'id' "
"and 'covered_by' (assigned after the coverage pass, so "
"sorting by them reorders within the ties of the more significant "
f"keys). Default: {DEFAULT_SORT!r}."
),
)
p.add_argument(
"--variant",
action="store_true",
help=(
"List each role's meta/variants.yml variants individually "
"instead of the whole role: one row per variant, its 'embeds' "
"recomputed from that variant's enabled+shared service flags. "
"The 'variant' column shows the variant index. Roles keep their "
"role-level consumer counts."
),
)
p.add_argument(
"--filter",
default=None,
metavar="EXPR",
help=(
"Boolean filter expression over row fields. Operators: '%%' "
"(contains / set membership), '==' '!=' '<' '>' '<=' '>=', "
"combined with 'and' 'or' 'xor' 'not' and parentheses; set "
"literals like {alpha,beta} are allowed. Fields: name, "
"lifecycle, base, embeds, embeds_direct, consumers, "
"consumers_direct, weight, variants, id, covered_by, "
"variant, siblings, random, compose, swarm, stack. "
"compose/swarm/stack are "
"booleans (compare with ==true / ==false). 'stack' is True when the "
"role ships its own compose stack template. "
"String compares are "
"case-"
"insensitive. A bare word (no operator) means 'name %% word'. "
"e.g. 'lifecycle == beta and weight > 50', "
'"lifecycle %% {alpha,pre} or name %% ldap". Scores are '
"computed against the full role tree first; only the rendered "
"rows are filtered."
),
)
p.add_argument(
"--lifecycles",
nargs="+",
default=None,
metavar="STAGE",
help=(
"Lifecycle envelope the 'compose'/'swarm' columns treat as "
"CI-tested: a role scores True for a mode only if its "
"meta/services.yml lifecycle is in this set (and it is invokable "
"and not skipped for the mode). Comma- or whitespace-separated, "
"e.g. 'beta rc' or 'alpha,beta'. Omitted: the "
f"built-in default ({' '.join(sorted(TESTED_LIFECYCLES))})."
),
)
p.add_argument(
"--no-group-names",
action="store_true",
help=(
"Ignore services whose enabled/shared flag is the "
"'<role>' in group_names Jinja form. Only literal `true` "
"flags count as deps."
),
)
p.add_argument(
"--format",
choices=("cli", "json", "yaml", "string"),
default="cli",
help=(
"Output format. 'cli' (default) shows counts only (name, "
"embeds, consumers, base, siblings) for a compact terminal "
"view. 'json' / 'yaml' emit the full payload including the "
"resolved service, consumer and sibling role lists. 'string' "
"prints only the role names, one per line (feed into `make "
"roundtrip apps=...`)."
),
)
p.add_argument(
"-s",
"--symbol",
action="store_true",
help=(
"Compact emoji view of the 'cli' table: emoji-only headers, "
"true/false as ✅/❌, and lifecycle stages as symbols. No effect on "
"the json/yaml/string formats."
),
)
p.add_argument(
"-L",
"--level",
type=int,
default=None,
metavar="N",
help=(
"Limit recursion depth: 1 = direct deps only, 2 = direct + "
"their direct, ... Default: unbounded (full closure)."
),
)
return p
def _apply_sort(rows: list[ComplexityRow], sort_spec: list[tuple[str, bool]]) -> None:
"""Stable multi-key sort in place: ``name`` as the deterministic least-
significant fallback, then each spec clause from least to most significant.
Run once before ``_mark_covered`` (where the ``id``/``covered_by`` keys are
still unset and sort as a no-op) and once after (where they order rows by
the just-computed coverage)."""
rows.sort(key=_SORT_KEYS["name"])
for column, reverse in reversed(sort_spec):
rows.sort(key=_SORT_KEYS[column], reverse=reverse)
def _mark_covered(
rows: list[ComplexityRow],
overridden: Mapping[tuple[str, int], set[str]] | None = None,
variant_counts: Mapping[str, int] | None = None,
) -> list[ComplexityRow]:
"""Assign each sorted row its numeric ``id`` (its position in sort order)
and its ``covered_by`` via a greedy set-cover: the first row is green, and
every later row's ``covered_by`` is the (1-based) ``id`` of the first
already-green predecessor OF A DIFFERENT ROLE that actually embeds this row
(its role name is in the predecessor's transitive ``services``); a row with
no such predecessor becomes green itself, leaving ``covered_by`` at the
sentinel ``0`` (no real ``id`` is 0). Because ``services`` is the
transitive closure, embedding the row pulls its whole subtree, so the
coverer's deploy genuinely brings this row up. Two variants of the same
role never cover each other (a role never embeds itself).
Coverage is variant-aware: only a row's variant-0 (or whole-role) form can
be covered, because a coverer only ever brings a provider up at variant 0.
Which coverers do that is not a property of the coverer alone. A job's
round index is the primary role's own variant index, and
``inventory.planner.plan_dev_inventory_matrix`` hands each pulled-in
dependency ``round_index if round_index < its variant count else 0``. So a
variant N row covers a provider exactly when N lands past that provider's
last variant and falls back to 0 - which is why ``variant_counts`` is
needed here: N alone does not say it.
A variant that dictates a provider's own config is not evidence about that
provider in any variant: the deploy ran a configuration the provider does
not declare. ``overridden`` maps ``(role, variant)`` to the providers it
overrides, and such a pair never covers those providers.
Args:
rows: the complexity rows, in ranking order.
overridden: ``{(role, variant): {provider role, ...}}``, or None when
no variant dictates anything.
variant_counts: ``{role: how many variants it declares}``. A role
missing from it counts as one, which is the whole-role case.
"""
demands = overridden or {}
counts = variant_counts or {}
def pulls_at_zero(coverer_variant: int | None, provider: str) -> bool:
index = coverer_variant or 0
return index == 0 or index >= counts.get(provider, 1)
green: list[tuple[int, str, int | None, set[str]]] = []
out: list[ComplexityRow] = []
for index, row in enumerate(rows, start=1):
coverable = row.variant in (None, 0)
coverer = (
next(
(
gid
for gid, gname, gvariant, gset in green
if gname != row.name
and row.name in gset
and row.name not in demands.get((gname, gvariant or 0), ())
and pulls_at_zero(gvariant, row.name)
),
None,
)
if coverable
else None
)
if coverer is None:
green.append((index, row.name, row.variant, set(row.services)))
out.append(row._replace(id=index, covered_by=coverer or 0))
return out
def main(argv: list[str] | None = None) -> int:
p = build_parser()
args = p.parse_args(argv)
if args.level is not None and args.level < 1:
p.error("--level/-L must be >= 1")
try:
sort_spec = parse_sort_spec(args.sort)
except ValueError as exc:
p.error(str(exc))
return 2
roles_dir = PROJECT_ROOT / "roles"
if not roles_dir.is_dir():
print(f"Error: roles directory not found: {roles_dir}", file=sys.stderr)
return 1
lifecycles = parse_lifecycles(args.lifecycles)
if args.variant:
rows = compute_variant_complexity_rows(
roles_dir,
include_group_names=not args.no_group_names,
max_level=args.level,
lifecycles=lifecycles,
)
else:
rows = compute_complexity_rows(
roles_dir,
include_group_names=not args.no_group_names,
max_level=args.level,
lifecycles=lifecycles,
)
if args.filter:
try:
predicate = compile_predicate(args.filter, FILTER_FIELDS)
except FilterError as exc:
p.error(f"--filter: {exc}")
rows = [r for r in rows if predicate(_row_fields(r))]
rows = attach_siblings(rows)
_apply_sort(rows, sort_spec)
rows = _mark_covered(
rows,
overridden_providers(roles_dir),
{
role: max(1, len(entries or [{}]))
for role, entries in get_variants(roles_dir=roles_dir).items()
},
)
_apply_sort(rows, sort_spec)
rows = [r._replace(row=line) for line, r in enumerate(rows, start=1)]
if args.format == "json":
rendered = render_json(rows)
elif args.format == "yaml":
rendered = render_yaml(rows)
elif args.format == "string":
rendered = render_string(rows)
else:
rendered = render_table(rows, symbol=args.symbol)
if rendered:
print(rendered)
return 0