Skip to content

Commit 8a19d12

Browse files
kdeldyckeclaude
andcommitted
Reconcile working tree for the 6.7.0 release
Consolidate the unreleased changelog into a release summary: 8 entries to 7, ordered breaking-first, each tightened to a single sentence. In cli.py, lift the cooldown-supported manager list to a pool-derived `COOLDOWN_SUPPORTED_MANAGERS` constant and fix the `--cooldown` help to list pnpm; drop an unused `Context` import. Repair two docstring cross-references in execution.py. Add targeted type-ignore comments clearing 24 mypy errors in the new concurrency, pool, and metadata tests. Fix two lint violations in the Guix deps-check workflow: a shellcheck SC2024 redirect warning and an over-length line. List pnpm among the cooldown-enforcing managers in docs/configuration.md. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 035d56b commit 8a19d12

8 files changed

Lines changed: 97 additions & 30 deletions

File tree

.github/workflows/check-guix-deps.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ jobs:
6666
echo "${guix_root}/bin" >> "${GITHUB_PATH}"
6767
# Authorize the build farm so substitutes are usable.
6868
for pub in "${guix_root}"/share/guix/*.pub; do
69+
# The *.pub keys are world-readable, so reading them via the input
70+
# redirect works as the unprivileged user; only the ACL write that
71+
# `guix archive --authorize` performs needs root, hence sudo.
72+
# shellcheck disable=SC2024
6973
sudo "${guix_root}/bin/guix" archive --authorize < "${pub}"
7074
done
7175
@@ -110,7 +114,11 @@ jobs:
110114
echo "log files: ${paths:-<none>}"
111115
for logf in ${paths}; do
112116
echo "=================== ${logf} ==================="
113-
{ sudo zcat "${logf}" 2>/dev/null || sudo bzcat "${logf}" 2>/dev/null || zcat "${logf}" 2>/dev/null; } | tail -250
117+
{
118+
sudo zcat "${logf}" 2>/dev/null \
119+
|| sudo bzcat "${logf}" 2>/dev/null \
120+
|| zcat "${logf}" 2>/dev/null
121+
} | tail -250
114122
done
115123
exit 1
116124

changelog.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
> [!WARNING]
66
> This version is **not released yet** and is under active development.
77
8-
- **Breaking:** [mpm] Rename the `--allow-no-cooldown` flag (shipped in `6.6.0`) to the `--require-cooldown-support`/`--allow-unsupported-managers` boolean pair, and rename its `allow_no_cooldown` config to `require_cooldown_support` (default `true`). Pass `--allow-unsupported-managers` (or set `require_cooldown_support = false`) for the previous `--allow-no-cooldown` behavior.
9-
- [pnpm] Add the pnpm package manager, with `installed`, `outdated`, `search`, `install`, `upgrade`, `remove` and `cleanup` support. Enforce the supply-chain `--cooldown` through pnpm's native `minimumReleaseAge` gate.
10-
- [mpm] Bundle the click-extra config-format extras (`toml`, `yaml`, `json5`, `jsonc`, `hjson`, `xml`) into the standalone binary via `[tool.repomatic] nuitka.extras` plus matching `[tool.nuitka] include-package` entries, so the binary can read configs in all six formats (the source distribution already supported them as optional extras).
11-
- [mpm] `install` and `remove` now exit with a non-zero status when a requested package could not be installed or removed by any selected manager. Both commands previously always exited `0`, masking failures. As part of this, `install` installs every requested package instead of stopping after the first one that succeeds.
12-
- [mpm] `--timeout` now defaults to a per-operation value instead of a flat 500s: read-only queries (`installed`, `outdated`, `search`) and the version-detection probe get a shorter 120s cap so a wedged or pathologically slow CLI (like `guix search` scanning every package) fails fast, while state-changing operations (`install`, `upgrade`, `remove`, `sync`, `cleanup`) keep the 500s cap for source builds and channel syncs. Passing `--timeout` or setting a per-manager `timeout` still overrides every operation.
13-
- [mpm] Read-only operations (`installed`, `outdated`, `search`) now query managers in parallel: a slow manager (like `guix search` scanning every package) no longer blocks the others, dropping the wall-clock from the sum of every manager to the slowest single one. Manager availability detection (the per-manager `--version` probe run while selecting managers) is warmed in parallel the same way, shaving startup latency off every command that touches many managers. Add a `--jobs`/`-j` option (default: CPU count minus one) to size the thread pool; `--jobs 1` or `--verbosity DEBUG` runs sequentially. State-changing operations (`install`, `upgrade`, `remove`, `sync`, `cleanup`) stay sequential, and a single aggregate spinner replaces the per-manager ones while a batch runs concurrently: it shows a live `8/12 managers` count, leaves a `✓`/`✗` trail naming each manager (and whether it erred) as it finishes, and ends with a persistent `✓ Searched 12 managers (15s)` line when it was on screen. Refs {issue}`529`.
14-
- [mpm] Show a progress spinner on stderr while a manager CLI call runs longer than a second, so a slow operation (like `guix search` scanning every package) no longer looks like a hang. It carries the elapsed time (`⠙ guix search (12.3s)`) so a long call reads as progressing rather than stuck. Built on the `Spinner` widget and the `--progress`/`--no-progress` default option from click-extra; mpm additionally suppresses it for serialized output and at DEBUG verbosity (where logs already narrate). The spinner self-disables off a terminal (pipes, `TERM=dumb`, CI) and under `--no-progress`/`--accessible`.
15-
- [mpm] Widen the binary smoke tests in `tests/cli-test-plan.yaml`: per-format `--validate-config` against a tiny fixture for each of the seven readers; `--table-format` rendering in JSON, YAML, TOML, and XML; the cycle's new `--man`, `--show-params`, `--accessible`, `--summary`, and `--cooldown` globals; the `config-template`, `help`, and `search` subcommands; and negative cases that assert click-extra's usage errors stay surfaced as exit code 2 instead of leaking a Python traceback from the onefile binary.
8+
- **Breaking:** [mpm] Rename the `--allow-no-cooldown` flag to the `--require-cooldown-support`/`--allow-unsupported-managers` pair, and its `allow_no_cooldown` config to `require_cooldown_support` (default `true`).
9+
- [pnpm] Add the pnpm package manager (`installed`, `outdated`, `search`, `install`, `upgrade`, `remove`, `cleanup`), enforcing `--cooldown` via pnpm's native `minimumReleaseAge` gate.
10+
- [mpm] Read-only operations (`installed`, `outdated`, `search`) now query managers in parallel, as does the manager-availability probe, via a new `--jobs`/`-j` option (default: CPU count minus one); state-changing operations stay sequential. Refs {issue}`529`.
11+
- [mpm] Show a progress spinner with elapsed time on stderr for manager CLI calls running longer than a second, with a single aggregate spinner during parallel batches; toggle it with click-extra's `--progress`/`--no-progress`.
12+
- [mpm] The standalone binary now reads configuration in all six formats (`toml`, `yaml`, `json5`, `jsonc`, `hjson`, `xml`), matching the source distribution.
13+
- [mpm] `--timeout` now defaults per-operation — 120s for read-only queries (`installed`, `outdated`, `search`) and 500s for state-changing operations — instead of a flat 500s; an explicit `--timeout` still overrides.
14+
- [mpm] `install` and `remove` now exit non-zero when no manager could fulfill a request (previously always `0`); `install` also installs every requested package, not just the first to succeed.
1615

1716
## [`6.6.0` (2026-06-17)](https://github.com/kdeldycke/meta-package-manager/compare/v6.5.1...v6.6.0)
1817

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ These go under `[mpm]` (or `[tool.mpm]` in `pyproject.toml`):
7878

7979
`cooldown` is a supply-chain safeguard: it refuses to install or upgrade any package version published more recently than the given age, giving a freshly-published (and possibly compromised) release time to be caught and pulled before it reaches the system.
8080

81-
`mpm` enforces the cooldown through each manager's own release-age mechanism, so only managers that ship one are covered: `uv` and `uvx` (via `exclude-newer`), `npm` (via `min-release-age`), `pip` (via `--uploaded-prior-to`), and `pipx` (which inherits the pip setting). Managers without native support cannot honor the gate. By default they are skipped during install and upgrade (fail-closed), so nothing slips in unguarded. Pass `--allow-unsupported-managers` (or set `require_cooldown_support = false`) to run them anyway, without the safeguard. Read-only operations (`outdated`, `installed`, `search`) are never blocked.
81+
`mpm` enforces the cooldown through each manager's own release-age mechanism, so only managers that ship one are covered: `uv` and `uvx` (via `exclude-newer`), `npm` (via `min-release-age`), `pnpm` (via `minimumReleaseAge`), `pip` (via `--uploaded-prior-to`), and `pipx` (which inherits the pip setting). Managers without native support cannot honor the gate. By default they are skipped during install and upgrade (fail-closed), so nothing slips in unguarded. Pass `--allow-unsupported-managers` (or set `require_cooldown_support = false`) to run them anyway, without the safeguard. Read-only operations (`outdated`, `installed`, `search`) are never blocked.
8282

8383
See {doc}`cooldown` for the full support matrix and the rationale.
8484

meta_package_manager/cli.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
from click_extra import (
4545
STRING,
4646
Choice,
47-
Context,
4847
EnumChoice,
4948
File,
5049
IntRange,
@@ -157,6 +156,17 @@ class SortableField(StrEnum):
157156
See the corresponding :issue:`implementation rationale in issue #10 <10>`.
158157
"""
159158

159+
COOLDOWN_SUPPORTED_MANAGERS = tuple(
160+
sorted(mid for mid, manager in pool.items() if manager.supports_cooldown)
161+
)
162+
"""IDs of the managers that natively enforce a release-age :option:`mpm --cooldown`.
163+
164+
Derived from the pool so the ``--cooldown`` help text never drifts from the set of
165+
managers that actually carry a :py:attr:`cooldown_env_var
166+
<meta_package_manager.execution.CLIExecutor.cooldown_env_var>`: adding cooldown
167+
support to a manager surfaces it here automatically.
168+
"""
169+
160170

161171
class Duration(ParamType):
162172
"""Parse a cooldown spec into a :py:class:`datetime.timedelta`.
@@ -655,8 +665,8 @@ def bar_plugin_path(ctx: Context, param: Parameter, value: str | None):
655665
"attacks. Accepts a friendly duration ('7 days', '1 week', '12h'), an "
656666
"ISO 8601 duration ('P7D', 'PT12H'), or an RFC 3339 absolute timestamp "
657667
"('2024-05-01T00:00:00Z'). Only honored by managers with native "
658-
"release-age support (uv, npm, pip, pipx); the others are skipped "
659-
"unless --allow-unsupported-managers is set.",
668+
"release-age support (" + ", ".join(COOLDOWN_SUPPORTED_MANAGERS) + "); the "
669+
"others are skipped unless --allow-unsupported-managers is set.",
660670
),
661671
option(
662672
"--require-cooldown-support/--allow-unsupported-managers",

meta_package_manager/execution.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ class CLIExecutor:
278278
"""Maximum number of seconds to wait for a CLI call to complete.
279279
280280
``None`` means the user expressed no explicit preference: the effective cap is
281-
then resolved per-operation by :py:meth:`_resolve_timeout` from
281+
then resolved per-operation by ``_resolve_timeout()`` from
282282
:py:data:`OPERATION_TIMEOUTS`. A non-``None`` value (the ``--timeout`` flag or a
283283
per-manager override) wins for every operation.
284284
"""
@@ -297,8 +297,8 @@ class CLIExecutor:
297297
298298
Set by the CLI to an interactive, human-facing run only (a TTY, no serialized
299299
output, not at DEBUG verbosity). Even when ``True`` the spinner still
300-
self-suppresses off a TTY: see :py:meth:`_make_spinner`. Defaults to ``False``
301-
so programmatic use stays silent.
300+
self-suppresses off a TTY: see ``_make_spinner()``. Defaults to ``False`` so
301+
programmatic use stays silent.
302302
"""
303303

304304
cooldown: timedelta | None = None

tests/test_cli_concurrency.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ def test_runs_sequentially_in_main_thread(jobs, verbosity, manager_count):
8484
managers = [FakeManager(f"m{i}") for i in range(manager_count)]
8585
threads: list = []
8686
collect_from_managers(
87-
ctx, "Testing", "Tested", managers, _record_thread(threads, threading.Lock())
87+
ctx, # type: ignore[arg-type]
88+
"Testing",
89+
"Tested",
90+
managers, # type: ignore[arg-type]
91+
_record_thread(threads, threading.Lock()),
8892
)
8993
assert threads, "work was never called"
9094
assert all(thread is threading.main_thread() for thread in threads)
@@ -95,7 +99,11 @@ def test_runs_concurrently_off_the_main_thread():
9599
managers = [FakeManager(f"m{i}") for i in range(4)]
96100
threads: list = []
97101
collect_from_managers(
98-
ctx, "Testing", "Tested", managers, _record_thread(threads, threading.Lock())
102+
ctx, # type: ignore[arg-type]
103+
"Testing",
104+
"Tested",
105+
managers, # type: ignore[arg-type]
106+
_record_thread(threads, threading.Lock()),
99107
)
100108
assert len(threads) == 4
101109
assert all(thread is not threading.main_thread() for thread in threads)
@@ -112,7 +120,13 @@ def work(manager):
112120
time.sleep(0.01 * (len(managers) - index))
113121
return manager.id, {"index": index}
114122

115-
results = collect_from_managers(ctx, "Testing", "Tested", managers, work)
123+
results = collect_from_managers(
124+
ctx, # type: ignore[arg-type]
125+
"Testing",
126+
"Tested",
127+
managers, # type: ignore[arg-type]
128+
work,
129+
)
116130
assert [manager_id for manager_id, _ in results] == [f"m{i}" for i in range(8)]
117131

118132

@@ -121,7 +135,11 @@ def test_suppresses_per_manager_spinners_when_concurrent():
121135
ctx = FakeContext(jobs=4)
122136
managers = [FakeManager(f"m{i}", progress=True) for i in range(4)]
123137
collect_from_managers(
124-
ctx, "Testing", "Tested", managers, lambda manager: (manager.id, {})
138+
ctx, # type: ignore[arg-type]
139+
"Testing",
140+
"Tested",
141+
managers, # type: ignore[arg-type]
142+
lambda manager: (manager.id, {}),
125143
)
126144
assert all(manager.progress is False for manager in managers)
127145

@@ -131,7 +149,11 @@ def test_keeps_per_manager_spinners_when_sequential():
131149
ctx = FakeContext(jobs=1)
132150
managers = [FakeManager(f"m{i}", progress=True) for i in range(3)]
133151
collect_from_managers(
134-
ctx, "Testing", "Tested", managers, lambda manager: (manager.id, {})
152+
ctx, # type: ignore[arg-type]
153+
"Testing",
154+
"Tested",
155+
managers, # type: ignore[arg-type]
156+
lambda manager: (manager.id, {}),
135157
)
136158
assert all(manager.progress is True for manager in managers)
137159

@@ -140,7 +162,11 @@ def test_empty_manager_list_returns_empty():
140162
ctx = FakeContext(jobs=4)
141163
assert (
142164
collect_from_managers(
143-
ctx, "Testing", "Tested", [], lambda manager: (manager.id, {})
165+
ctx, # type: ignore[arg-type]
166+
"Testing",
167+
"Tested",
168+
[],
169+
lambda manager: (manager.id, {}),
144170
)
145171
== []
146172
)
@@ -155,7 +181,11 @@ def test_no_finisher_line_off_terminal(capsys):
155181
ctx = FakeContext(jobs=4)
156182
managers = [FakeManager(f"m{i}", progress=True) for i in range(4)]
157183
collect_from_managers(
158-
ctx, "Searching", "Searched", managers, lambda manager: (manager.id, {})
184+
ctx, # type: ignore[arg-type]
185+
"Searching",
186+
"Searched",
187+
managers, # type: ignore[arg-type]
188+
lambda manager: (manager.id, {}),
159189
)
160190
assert "Searched" not in capsys.readouterr().err
161191

@@ -174,7 +204,13 @@ def slow_work(manager):
174204
time.sleep(0.1) # Outlast the zeroed delay so the spinner draws a frame.
175205
return manager.id, {}
176206

177-
collect_from_managers(ctx, "Searching", "Searched", managers, slow_work)
207+
collect_from_managers(
208+
ctx, # type: ignore[arg-type]
209+
"Searching",
210+
"Searched",
211+
managers, # type: ignore[arg-type]
212+
slow_work,
213+
)
178214
output = tty.getvalue()
179215
# The spinner draws its seeded running count before any manager lands...
180216
assert "Searching 0/4 managers" in output
@@ -200,7 +236,13 @@ def work(manager):
200236
errors = ["boom"] if manager.id == "m2" else []
201237
return manager.id, {"errors": errors}
202238

203-
collect_from_managers(ctx, "Searching", "Searched", managers, work)
239+
collect_from_managers(
240+
ctx, # type: ignore[arg-type]
241+
"Searching",
242+
"Searched",
243+
managers, # type: ignore[arg-type]
244+
work,
245+
)
204246
output = tty.getvalue()
205247
assert KO_GLYPH in output # The failure glyph, for m2.
206248
assert OK_GLYPH in output # The success glyph, for the other managers.
@@ -229,7 +271,13 @@ def work(manager):
229271
time.sleep(0.03 if int(manager.id[1:]) < 3 else 0.4)
230272
return manager.id, {}
231273

232-
collect_from_managers(ctx, "Checking", "Checked", managers, work)
274+
collect_from_managers(
275+
ctx, # type: ignore[arg-type]
276+
"Checking",
277+
"Checked",
278+
managers, # type: ignore[arg-type]
279+
work,
280+
)
233281
output = tty.getvalue()
234282
# Every manager — fast and slow alike — appears, not just the slow three.
235283
assert all(f"m{i}" in output for i in range(6))

tests/test_metadata.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
def _load_pyproject() -> dict:
5353
"""Parse the project's ``pyproject.toml`` into a dictionary."""
5454
path = PROJECT_ROOT.joinpath("pyproject.toml")
55-
return tomllib.loads(path.read_text(encoding="UTF-8"))
55+
content = path.read_text(encoding="UTF-8")
56+
return tomllib.loads(content) # type: ignore[no-any-return]
5657

5758

5859
def _major_minor(version: str) -> tuple[int, int]:

tests/test_pool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ def _jobs_context(jobs: int, verbosity: str = "INFO") -> click.Context:
277277
def test_warm_availability_skips_without_context():
278278
"""No active CLI context: leave probing to the lazy, sequential filter loop."""
279279
accessed: list = []
280-
warm_availability([_RecordingManager(accessed), _RecordingManager(accessed)])
280+
managers = [_RecordingManager(accessed), _RecordingManager(accessed)]
281+
warm_availability(managers) # type: ignore[arg-type]
281282
assert accessed == []
282283

283284

@@ -293,7 +294,7 @@ def test_warm_availability_skips_when_not_concurrent(jobs, verbosity, count):
293294
accessed: list = []
294295
managers = [_RecordingManager(accessed) for _ in range(count)]
295296
with _jobs_context(jobs, verbosity):
296-
warm_availability(managers)
297+
warm_availability(managers) # type: ignore[arg-type]
297298
assert accessed == []
298299

299300

@@ -302,6 +303,6 @@ def test_warm_availability_probes_concurrently():
302303
threads: list = []
303304
managers = [_RecordingManager(threads) for _ in range(4)]
304305
with _jobs_context(jobs=4):
305-
warm_availability(managers)
306+
warm_availability(managers) # type: ignore[arg-type]
306307
assert len(threads) == 4
307308
assert all(thread is not threading.main_thread() for thread in threads)

0 commit comments

Comments
 (0)