From 7a87bc282559e1383c8321add36b16a670c360ce Mon Sep 17 00:00:00 2001 From: Tobias Fischer Date: Sat, 24 Oct 2026 14:39:49 +1000 Subject: [PATCH] Pinning updates: name the changed pins and the packages that use them The update-conda-forge-pinning report said which packages to rebuild, but not which pins had changed, which packages use them directly (as opposed to depending on one that does), or whether any migration was new. So it wasn't clear from #94 why only jazzy rebuilds: shaderc 2026.3 -> 2026.4, which only jazzy's gz_ogre_next_vendor uses. Per distribution the report now shows the changed pins (before -> after), those that packages use in a table with their direct users, the rest folded away; the rebuild list says how many use a changed pin and how many depend on one, direct users first; the migrations line names the new ones or says there are none. A pin whose value only changes its quoting ('6' -> 6) isn't a change. Co-Authored-By: Claude Opus 5.5 --- tools/maintenance.py | 63 ++++++++++++++++++++++++------ tools/tests/test_pinning_update.py | 12 ++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/tools/maintenance.py b/tools/maintenance.py index 37edd5bd9..1028a013e 100644 --- a/tools/maintenance.py +++ b/tools/maintenance.py @@ -192,18 +192,49 @@ def bump_rebuild(distro: str) -> list[str]: FULL_REBUILD_SHARE = 0.5 -def plan_rebuild(distro: str, old_cbc: str) -> tuple[dict[str, str], int]: +def _pin_values(value) -> list[str]: + return [str(v) for v in (value if isinstance(value, list) else [] if value is None else [value])] + + +def plan_rebuild(distro: str, old_cbc: str) -> tuple[dict[str, str], int, dict[str, tuple]]: """Packages of the distribution that the change from old_cbc to its current - conda_build_config.yaml has to rebuild (with the reason), and how many packages - it has (vinca-rebuild-plan).""" + conda_build_config.yaml has to rebuild (with the reason), how many packages it has + (vinca-rebuild-plan), and the changed pins ({name: (before, after)}).""" from vinca import rebuild new = yaml.safe_load((rs.DISTROS / distro / "conda_build_config.yaml").read_text()) or {} changed = rebuild.changed_pins(yaml.safe_load(old_cbc) or {}, new) + # only the quoting changed (e.g. '6' -> 6): the same pin + changed = {k: v for k, v in changed.items() if _pin_values(v[0]) != _pin_values(v[1])} if not changed: - return {}, 0 + return {}, 0, {} requirements, prefix = rebuild.requirements_from_vinca(rs.prepare(distro)) - return rebuild.plan(requirements, changed, prefix), len(requirements) + return rebuild.plan(requirements, changed, prefix), len(requirements), changed + + +def pin_report(changed: dict[str, tuple], packages: dict[str, str]) -> list[str]: + """The changed pins, those packages use first (with the packages using them + directly), the others folded away.""" + users: dict[str, list[str]] = {} + for name, reason in packages.items(): + if reason.startswith("uses "): + for pin in reason[len("uses "):].split(", "): + users.setdefault(pin, []).append(name) + + def change(pin: str) -> str: + before, after = (", ".join(_pin_values(v)) or "(none)" for v in changed[pin]) + return f"`{before}` → `{after}`" + + used = sorted(p for p in changed if p in users) + unused = sorted(p for p in changed if p not in users) + lines = [] + if used: + lines += ["| changed pin | before → after | used directly by |", "|---|---|---|"] + lines += [f"| `{p}` | {change(p)} | {', '.join(f'`{n}`' for n in sorted(users[p]))} |" for p in used] + if unused: + lines += ["" if lines else None, *_details(f"Changed pins no package uses ({len(unused)})", + [f"- `{p}`: {change(p)}" for p in unused])] + return [line for line in lines if line is not None] def bump_partial(distro: str, packages: dict[str, str]) -> list[str]: @@ -520,7 +551,8 @@ def _move_pinning(distros: list[str], before: tuple, version: str, migrations: l f"Moves **{who}** from conda-forge-pinning `{before[0]}` to `{version}` (`{where}`).", "", "Migrations: " + (", ".join(f"`{m}`" for m in migrations) or "none") - + (f"; new: {', '.join(f'`{m}`' for m in added)}" if added else "") + + (f"; **new: {', '.join(f'`{m}`' for m in added)}**" if added + else "; none new (the pins below change with conda-forge-pinning itself)") + (f"; finished (now part of the base pinning): {', '.join(f'`{m}`' for m in finished)}" if finished else "") + (f"; dropped: {', '.join(f'`{m}`' for m in removed)}" if removed else ""), ] @@ -559,18 +591,23 @@ def _rebuild_for_pins(distro: str, before_cbc: str, conflicts_before) -> tuple[l return ["The rendered pins don't change: nothing to rebuild."], False lines = [] print(f"Planning the rebuild of {distro}", flush=True) - packages, total = plan_rebuild(distro, before_cbc) + packages, total, changed = plan_rebuild(distro, before_cbc) + if not changed: + return ["Only the formatting of the rendered pins changes: nothing to rebuild."], False + lines += [*pin_report(changed, packages), ""] if not packages: - lines.append("The pins change, but no package uses them: nothing to rebuild.") + lines.append("No package uses a changed pin: nothing to rebuild.") elif total and len(packages) > FULL_REBUILD_SHARE * total: lines.append(f"**Full rebuild**: {len(packages)} of {total} packages use a changed pin or depend " "on one. " + "; ".join(bump_rebuild(distro))) else: - lines.append(f"**Rebuild {len(packages)} of {total} packages** (they use a changed pin or depend " - "on one): " + "; ".join(bump_partial(distro, packages))) - if packages: - lines += ["", *_details(f"Packages to rebuild ({len(packages)})", [ - f"- `{name}`: {reason}" for name, reason in sorted(packages.items())])] + direct = sum(reason.startswith("uses ") for reason in packages.values()) + lines.append(f"**Rebuild {len(packages)} of {total} packages**: {direct} " + f"{'uses' if direct == 1 else 'use'} a changed pin, {len(packages) - direct} " + f"{'depends' if len(packages) - direct == 1 else 'depend'} on one. " + "; ".join(bump_partial(distro, packages))) + lines += ["", *_details(f"Packages to rebuild ({len(packages)})", [ + f"- `{name}`: {reason}" for name, reason in sorted( + packages.items(), key=lambda item: (not item[1].startswith("uses "), item[0]))])] conflicts_after = dependency_conflicts(distro) if conflicts_after is None or conflicts_before is None: conflicts = conflicts_after is None diff --git a/tools/tests/test_pinning_update.py b/tools/tests/test_pinning_update.py index 3b0946c2f..335f29b0c 100644 --- a/tools/tests/test_pinning_update.py +++ b/tools/tests/test_pinning_update.py @@ -34,3 +34,15 @@ def test_no_shared_update_when_every_distribution_has_its_own(monkeypatch, tmp_p lambda d: calls.append(d) or maintenance.Result(d, d)) maintenance.main("update-pinning", ["--summary", str(tmp_path / "s.md")]) assert calls == ["humble", "jazzy", "rolling"] + + +def test_pin_report_lists_the_direct_users(): + changed = {"shaderc": ("2026.3", "2026.4"), "zig-compiler": (None, "zig"), "libgdal": (["11.4.2"], ["10.7.4", "11.4.3"])} + packages = {"gz_ogre_next_vendor": "uses shaderc", "gz_rendering_vendor": "depends on gz_ogre_next_vendor"} + lines = maintenance.pin_report(changed, packages) + assert lines[:3] == ["| changed pin | before → after | used directly by |", "|---|---|---|", + "| `shaderc` | `2026.3` → `2026.4` | `gz_ogre_next_vendor` |"] + assert "- `libgdal`: `11.4.2` → `10.7.4, 11.4.3`" in lines + assert "- `zig-compiler`: `(none)` → `zig`" in lines + # nothing used: the folded list comes first + assert maintenance.pin_report({"zig-compiler": (None, "zig")}, {})[0] == "
"