meta_package_manager.manager module¶

Abstract base class tying together every package manager definition.

Defines meta_package_manager.manager.PackageManager, the class each concrete manager in meta_package_manager.managers inherits from, together with its meta_package_manager.manager.MetaPackageManager metaclass and the meta_package_manager.manager.ManagerScope classification.

A subclass declares its identity (supported platforms, version requirement, maintenance status) and implements the operations it supports (installed, outdated, install, upgrade, …). The CLI-execution engine it inherits lives in meta_package_manager.execution, the operation vocabulary in meta_package_manager.capabilities, and the package objects operations yield in meta_package_manager.package. On top of the engine, this module adds the availability policy: whether the manager is supported, fresh, and ready to use.

meta_package_manager.manager.COOLDOWN_EXEMPT = datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)¶

Sentinel a PackageManager.release_date() probe returns for a package outside the cooldown gate’s scope.

A hybrid manager can serve two registries with different threat models: an AUR helper resolves the live, self-published AUR next to Arch’s official repositories, whose archive stages releases on its own (see the N/A rows of docs/cooldown.md). The out-of-scope half reads as infinitely aged, so the hold logic of PackageManager.cooldown_hold_reason() lets it through untouched, and the constant’s name keeps the probe’s intent readable where a bare datetime.min would read as a bug.

meta_package_manager.manager.JSON_FIELD_SELECTOR_REGEX = re.compile('^(?P<key>[^\\[\\]]+?)(?:\\[(?P<index>\\d+)\\])?$')¶

Parse a JSON field selector: a key name with an optional [N] list index.

A bare version maps the package field to the item’s version key. A versions[0] selector additionally picks one element out of a list-valued key (zerobrew reports each package’s installed versions as an array). Anything more nested stays out on purpose: a query needing real JSON traversal is better served by a custom parser. Shared by PackageManager.parse_json_items() and the declarative-manager validation in meta_package_manager.definitions.

class meta_package_manager.manager.ManagerScope(*values)[source]¶

Bases: Enum

Filesystem scope a package manager operates within.

SYSTEM = 'system'¶

Manages software installed globally, machine-wide.

All currently-maintained managers are system-scoped.

PROJECT = 'project'¶

Manages dependencies confined to a project’s working tree.

Not supported yet. The user-facing rationale, the ecosystems this would cover and the architectural work it waits on are catalogued once in Unsupported managers; the extension point is meta_package_manager.manager.PackageManager.discover_projects().

class meta_package_manager.manager.MetaPackageManager(name, bases, dct)[source]¶

Bases: type

Custom metaclass used as a class factory for package managers.

Sets some class defaults, but only if they’re not redefined in the final manager class.

Also normalize list of platform, by ungrouping groups, deduplicate entries and freeze them into a set of unique platforms.

class meta_package_manager.manager.PackageManager[source]¶

Bases: CLIExecutor

Base class from which all package manager definitions inherits.

Initialize cli_errors list.

scope: ClassVar[ManagerScope] = 'system'¶

Whether the manager operates on globally-installed software or project-local dependencies.

Defaults to ManagerScope.SYSTEM, which covers every manager maintained today: they install and query software machine-wide. Project-scoped managers (Poetry, Bundler, Maven, …) resolve dependencies confined to a working tree and are not supported yet.

unmaintained: bool = False¶

A manager whose upstream project is no longer maintained.

Covers projects that are officially retired and those we infer are abandoned: archived on their forge, left without a release or commit for years, formally superseded by a successor, or part of a discontinued platform. See the stability policy in CLAUDE.md for the full criteria.

An unmaintained manager is hidden from package selection by default (you can still use it by explicitly calling for it on the command line), and is exempt from the project stability policy: it may be dropped, in part or in full, in any release and without notice, once keeping it working becomes too burdensome.

Unmaintained managers are kept out of the functional and integration test matrices, so an unreliable or flaky one never blocks a release and we save CI resources. The commitment is to keep the wrapper for as long as that stays cheap: the cheap static invariants (ID format, attribute ordering, …) still apply for as long as the manager’s code lives in the source tree, to keep that code valid.

Every unmaintained manager must document itself through unmaintained_message.

unmaintained_message: str | None = None¶

Evidence and rationale for the unmaintained flag, as a MyST markdown block.

Rendered into the documentation (the manager’s page, and a ⚠️ marker in the manager tables). May embed markdown links to the archival notice, the successor project, or the discontinuation announcement. Required for every manager whose unmaintained flag is set, and only meaningful on such managers. Enforced by test_unmaintained.

maintenance_note: str | None = None¶

A watch note about a still-maintained upstream whose activity is slowing or whose status is ambiguous, as a MyST markdown block.

Unlike unmaintained, this is purely informational: the manager stays in the default selection and in the test matrices. It renders as a {note} admonition atop the manager’s documentation page, flagging upstreams worth keeping an eye on (a slow release cadence, superseded-but-still-shipped tools, a discontinued platform still under vendor support). May embed markdown links. Mutually exclusive with unmaintained: a confirmed-dead manager carries an unmaintained_message instead. Enforced by test_maintenance_note.

id: str = 'packagemanager'¶

Package manager’s ID.

Derived by defaults from the lower-cased class name in which underscores _ are replaced by dashes -.

This ID must be unique among all package manager definitions and lower-case, as they’re used as feature flags for the mpm CLI.

name: str = 'PackageManager'¶

Return package manager’s common name.

Default value is based on class name.

homepage_url: str | None = None¶

Home page of the project, only used in documentation for reference.

Slug of the brand mark standing for this manager in the documentation.

Names an SVG vendored under docs/assets/managers/, whose provenance and license are recorded in docs/assets/managers/logos.yaml. Inlined at the top of the manager’s page by meta_package_manager._docs; a manager leaving it unset keeps the page’s default package glyph.

Several managers legitimately share one slug, either because they wrap the same upstream (brew and cask) or because the tool has no mark of its own and its ecosystem’s stands in (apt under Debian’s swirl, cargo under Rust’s gear). Documentation-only, like homepage_url: no CLI output reads it.

brewfile_entry_type: ClassVar[str | None] = None¶

Name of the Brewfile DSL entry type this manager maps to, or None if the manager has no Brewfile equivalent.

Set by the subset of managers Homebrew Bundle’s DSL covers, and consumed by meta_package_manager.brewfile when rendering the output of mpm dump --brewfile. Which manager maps to which entry is tabulated from these declarations in Snapshot and export, section “Brewfile”, where the export’s own quirks are documented too.

brewfile_skip_warning: ClassVar[str | None] = None¶

Optional stderr warning emitted when this manager’s installed packages are excluded from a Brewfile dump.

Set on managers where silently dropping the entries would mislead the user. The string supports a single {count} placeholder for the installed-package count.

platforms: frozenset[Platform] | Group | Platform | Iterable[Platform | Group] = frozenset({})¶

List of platforms supported by the manager.

Allows for a mishmash of platforms and groups of platforms. Will be normalized into a frozenset of Platform instances at instantiation.

requirement: str | None = None¶

Version requirement specifier.

Supports a comma-separated range of constraints (e.g. ">=1.20.0,<2.0.0"). A bare version string like "1.20.0" is treated as >=1.20.0.

Parsed by meta_package_manager.version.VersionRange.

Defaults to None, which deactivates version check entirely.

virtual: bool = True¶

Should we expose the package manager to the user?

Virtual package manager are just skeleton classes used to factorize code among managers of the same family.

ignore_auto_updates: bool = True¶

Some managers can report or ignore packages which have their own auto-update mechanism.

split_name_version(token)[source]¶

Split a dash-joined <package_id>-<version> token into its two parts.

Matches token against _NAME_VERSION_REGEXP (or the subclass’s override of it) and returns the (package_id, version) pair, or None when the token carries no recognizable version. Shared by every manager whose listings glue the name and version together.

Return type:

tuple[str, str] | None

parse_json(output)[source]¶

Parse a query’s JSON output, tolerating empty and malformed captures.

The shared first step of every JSON-emitting query, for built-in managers and config-defined operations alike (see meta_package_manager.definitions._parse_spec_output()). Returns None when the command produced no output (a manager with nothing to report often prints nothing at all), and when the output is not valid JSON, which logs one warning tagged with the manager ID instead of raising: a query that cannot be parsed yields no packages, mirroring how the fan-out commands swallow a failed CLI call into an empty result.

Queries whose failure semantics differ keep their own parsing: a per-line NDJSON stream (pkg search), a hard CLIError on malformed payloads (pwsh-gallery), a best-effort metadata enrichment logging at DEBUG (brew info).

Return type:

Any | None

parse_regex_lines(pattern, output)[source]¶

Yield one package per line of output matching pattern.

The shared engine of every line-oriented text listing, for built-in managers and config-defined operations alike (see meta_package_manager.definitions._make_query_property()). The pattern is searched in each line, and its named groups map straight onto the package fields: package_id (required: a match without one is skipped), installed_version, latest_version, name, description and arch, empty and absent groups being dropped.

Managers whose listings need per-line post-processing (multi-version reduction, name/version splitting, cross-query joins) keep their own loop and this stays their reference semantics.

Return type:

Iterator[Package]

parse_json_items(output, *, list_path=None, fields)[source]¶

Yield one package per item of a JSON listing.

The shared engine of every flat-JSON query, for built-in managers and config-defined operations alike (see meta_package_manager.definitions._make_query_property()). The document is parsed through parse_json() (so a malformed payload warns and yields nothing), the package array is reached by walking the dotted list_path (None when the document is itself the array), and fields maps each package field (package_id, required, plus any of installed_version, latest_version, name, description, arch) to its JSON selector: a key name with an optional [N] list index, like version or versions[0] (see JSON_FIELD_SELECTOR_REGEX). Items missing their package_id and fields resolving to None are dropped.

Return type:

Iterator[Package]

package(**kwargs)[source]¶

Instantiate a Package object from the manager.

Sets its manage_id to the manager it belongs to.

Return type:

Package

brewfile_entry(package)[source]¶

Return (entry_name, entry_options) for a Brewfile line, or None to skip the package.

Default: emit meta_package_manager.package.Package.id as the entry name with no options. Override on managers whose Brewfile DSL counterpart expects a different shape: mas uses the app name with id: ADAM_ID, flatpak adds with: ["remote"]. Only called when brewfile_entry_type is set.

Return type:

tuple[str, dict[str, object] | None] | None

property supported: bool[source]¶

Is the package manager supported on that platform?

property fresh: bool[source]¶

Does the package manager match the version requirement?

property available: bool[source]¶

Is the package manager available and ready-to-use on the system?

Returns True only if the main CLI:

  1. is supported on the current platform,

  2. was found on the system,

  3. is executable, and

  4. match the version requirement.

property unavailable_reason: str | None¶

Short, human-readable explanation of why available is False, or None if the manager is available.

Returned in priority order so the most actionable cause is reported first: platform support, then CLI lookup, then executable bit, then version requirement.

property installed: Iterator[Package]¶

List packages currently installed on the system.

Optional. Will be simply skipped by mpm if not implemented.

installed_or_empty()[source]¶

Materialized installed, or an empty tuple on CLI failure.

Best-effort inventory snapshot for the installed, dump and sbom subcommands, and for the installed_ids lookup behind remove and upgrade <packages>: each wants “give me what’s installed, and just skip this manager if its CLI blew up” rather than re-implementing the same meta_package_manager.execution.CLIError swallow. Logs one canonical warning on error and returns () so the caller carries on with the other managers.

Return type:

tuple[Package, ...]

property installed_ids: frozenset[str][source]¶

Installed package IDs, materialized once from installed_or_empty().

Routed through the tolerant installed_or_empty() rather than installed() because its callers ask a discovery question: which managers have this package? A manager whose CLI just failed has no answer to give, which is not the same as a fatal error. Sourcing a spec for remove and upgrade <packages> reads this for every selected manager, so a single broken CLI would otherwise abort the whole command before the managers that do have the package are ever tried.

Contrast installed_version_map, which deliberately keeps raising: it is read from inside an outdated parser, where an empty map does not mean “no answer” but silently reports every outdated package with an unknown installed version.

property installed_version_map: dict[str, TokenizedString | str | None][source]¶

Installed versions keyed by package ID, materialized once from installed().

Convenience for outdated parsers that report each package’s latest version but not its currently-installed one, and so must look the latter up by ID (snap, xbps). The value mirrors meta_package_manager.package.Package.installed_version, whose declared type still carries the transient str it normalizes away in __post_init__.

package_metadata_batch(packages)[source]¶

Yield (package, metadata) pairs enriched with whatever rich per-package data this manager can surface.

Called by mpm sbom in --bundled mode to populate licenses, checksums, download URLs, supplier/originator, and the declared dependency graph. The base implementation yields meta_package_manager.package.EMPTY_METADATA for each package and stays compatible with managers that do not (yet) expose richer metadata: their SBOM entries stay at the minimal Package level, matching the historical and --minimal modes.

Manager subclasses override this with their native query path:

  • bulk shell-outs when the CLI accepts a package list (brew info --json=v2 --installed, dpkg-query -W, apt-cache show);

  • on-disk parsing when the metadata already lives on the filesystem (pip’s .dist-info directories, Homebrew’s per-formula sbom.spdx.json, dpkg’s .md5sums).

The yielded pairs do not need to preserve the input order; the SBOM renderer matches by Package identity. Implementations are expected to swallow per-package extraction errors and yield meta_package_manager.package.EMPTY_METADATA for the affected packages rather than failing the whole scan: a single misbehaving formula must not abort an enrichment pass spanning hundreds of packages.

Todo

Today every extractor is local-only (shell-outs to the manager’s CLI, plus on-disk reads). When extractors start reaching for network resources (PyPI’s JSON API, npm’s registry, crates.io, GitHub’s security advisories) the --bundled flag will no longer be a fine-grained enough knob: some users will want enrichment but not network traffic (offline scans, CI without egress). The natural split is a future --network/--no-network flag layered under --bundled to gate the network-touching code paths specifically, leaving local enrichment always-on for --bundled.

Return type:

Iterator[tuple[Package, PackageMetadata]]

property outdated: Iterator[Package]¶

List installed packages with available upgrades.

Optional. Will be simply skipped by mpm if not implemented.

property refiltered_outdated: Iterator[Package]¶

Wraps outdated() with a version-equality filter.

Some package managers report packages as outdated when the version strings differ at the character level but are numerically equal after parsing (e.g., Perl floating-point versions 2.0000 vs 2.000000). This filter drops those false positives.

release_date(package_id)[source]¶

Publication timestamp of the latest release of a package.

The probe behind the synthesized per-package cooldown gate (see cooldown_hold_reason()): a manager without a native cooldown_env_var that implements this method becomes gateable, package by package.

The contract binds the timestamp’s provenance, not just its shape:

  • Returns a timezone-aware datetime, or None when the registry answers but carries no date (the gate then fails closed under the default enforce posture).

  • The timestamp must be server-set: stamped by the registry, store or build service at publication. A client-set or package-embedded date (a git commit date, an archive metadata field the author writes) is forgeable by exactly the attacker the cooldown exists to stop, and must never back this probe.

  • The date is the one of the latest available release, the version the manager would resolve absent a pin. Implementations should read it through the manager’s own CLI, so the probe sees the same registry, mirrors and authentication as the install it guards.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

datetime | None

cooldown_hold_reason(package_id)[source]¶

Decide whether the release-age cooldown holds back one package.

The per-package half of the gate, for managers that implement the release_date() probe instead of carrying a native cooldown_env_var. Returns None when the package may proceed: no active probe-backed cooldown, or a publication old enough to clear the window. Returns a human-readable hold reason otherwise, which the caller renders in its trail and logs.

The probe is fail-closed: a publication date that cannot be read (probe failure, or a registry carrying no date) holds the package under the default enforce posture, and only a best-effort policy lets it through, unguarded.

Return type:

str | None

property orphans: Iterator[Package]¶

List packages installed as dependencies that nothing requires anymore.

The read-only counterpart of the --orphans action flags: where mpm cleanup --orphans removes the orphans, this query only reports them, through the manager’s native listing (pacman --query --deps --unrequired, brew autoremove --dry-run, dnf repoquery --unneeded, …). mpm builds no dependency graph: the manager decides what is orphaned.

Optional. Will be simply skipped by mpm if not implemented.

search(query, extended, exact)[source]¶

Search packages available for install.

There is no need for this method to be perfect and sensitive to extended and exact parameters. If the package manager is not supporting these kind of options out of the box, just returns the closest subset of matching package you can come up with. Finer refiltering will happens in the meta_package_manager.manager.PackageManager.refiltered_search() method below.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

Iterator[Package]

cli_names: tuple[str, ...] = ('packagemanager',)¶

List of CLI names the package manager is known as.

This list of recognized CLI names is ordered by priority. That way we can influence the search of the right binary.

..hint::

This was helpful in the case of the Python transition from 2.x to 3.x, where multiple versions of the same executable were named python or python3.

By default, this property’s value is derived from the manager’s ID (see the MetaPackageManager.__init__ method above).

Returns search results with extra manual refiltering to refine gross matchings.

Some package managers returns unbounded results, and/or don’t support fine search criterions. In which case we use this method to manually refilters meta_package_manager.manager.PackageManager.search() results to either exclude non-extended or non-exact matches.

Returns a generator producing the same data as the meta_package_manager.manager.PackageManager.search() method above.

Tip

If you are implementing a package manager definition, do not waste time to filter CLI results. Let this method do this job.

Instead, just implement the core meta_package_manager.manager.PackageManager.search() method above and try to produce results as precise as possible using the native filtering capabilities of the package manager CLI.

Return type:

Iterator[Package]

install(package_id, version=None)[source]¶

Install one package and one only.

Allows a specific version to be provided.

Return type:

str

upgrade_all_cli()[source]¶

Returns the complete CLI to upgrade all outdated packages on the system.

Return type:

tuple[str, ...]

upgrade_one_cli(package_id, version=None)[source]¶

Returns the complete CLI to upgrade one package and one only.

Allows a specific version to be provided.

Return type:

tuple[str, ...]

upgrade_all_cli_excluding(package_ids)[source]¶

Returns the CLI upgrading all outdated packages except the named ones.

Optional refinement of upgrade_all_cli() for managers whose full upgrade is one transaction with a native exclusion flag (pacman’s --ignore). Under an active probe-backed cooldown, mpm prefers this over per-package upgrades: the manager keeps its own transaction and dependency ordering, and only the held packages are left out.

Return type:

tuple[str, ...]

upgrade(package_id=None, version=None)[source]¶

Perform an upgrade of either all or one package.

Executes the CLI provided by either meta_package_manager.manager.PackageManager.upgrade_all_cli() or meta_package_manager.manager.PackageManager.upgrade_one_cli().

If the manager doesn’t provides a full upgrade one-liner (i.e. if meta_package_manager.manager.PackageManager.upgrade_all_cli() raises NotImplementedError), then the list of all outdated packages will be fetched (via meta_package_manager.manager.PackageManager.outdated()) and each package will be updated one by one by calling meta_package_manager.manager.PackageManager.upgrade_one_cli().

See for example the case of meta_package_manager.managers.pip.Pip.upgrade_one_cli().

An active probe-backed cooldown (see cooldown_hold_reason()) routes through _upgrade_all_with_cooldown() instead of the plain one-shot command, so individual too-fresh releases can be held back while the rest of the upgrade proceeds.

Return type:

str

remove(package_id)[source]¶

Remove one package and one only.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

str

remove_orphan(package_id)[source]¶

Remove one package together with the dependencies it alone pulled in.

The opt-in counterpart to meta_package_manager.manager.PackageManager.remove(), surfaced as mpm remove --orphans. It maps to the manager’s native “remove and drop now-unneeded dependencies” verb (apt remove --auto-remove, pacman --remove --recursive, dnf autoremove, …), so mpm builds no dependency graph of its own.

Optional. A manager with no such native verb leaves this NotImplementedError; mpm remove --orphans then falls back to meta_package_manager.manager.PackageManager.remove() and logs one INFO capability-skip.

Return type:

str

sync()[source]¶

Refresh package metadata from remote repositories.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

None

cleanup()[source]¶

Run the manager’s non-destructive cleanup categories.

Not an operation managers define anymore: cleanup is the fixed composition of the non-destructive category methods a manager overrides (cleanup_cache(), then cleanup_repair()). The orphan sweep never joins in, native or synthesized: it is the one category that removes packages, so it only runs on an explicit mpm cleanup --orphans (or a direct cleanup_orphan() call), keeping a plain cleanup package-preserving on every manager.

A manager overriding no category method does not advertise the cleanup operation at all (see meta_package_manager.capabilities.implements()) and this composer is then a no-op.

Return type:

None

cleanup_orphan()[source]¶

Remove every orphaned package on the system, sparing the caches.

The system-wide “remove all packages nothing depends on anymore” sweep (apt autoremove, brew autoremove, flatpak uninstall --unused, …). The one cleanup category that removes packages, so it is deliberately kept out of the plain cleanup() composition and only runs on an explicit mpm cleanup --orphans.

Distinct from meta_package_manager.manager.PackageManager.remove_orphan(), which is scoped to one package’s own orphaned dependencies. As with cleanup(), mpm builds no dependency graph: the manager decides what is orphaned.

A manager with no native sweep verb is backfilled by this base implementation when it supports both the orphans query and package removal: list the orphans, remove each one (with remove_orphan() when available, so every listed root takes its own now-orphaned subtree along), then re-query and repeat until the listing settles, since removing an orphan can orphan its own dependencies. The exact pattern of the synthesized full upgrade --all, and the in-process equivalent of Arch’s classic pacman -Rns $(pacman -Qtdq) idiom. The re-query loop stops as soon as a round makes no progress, so removal failures cannot spin it forever.

A manager implementing neither a native sweep nor the orphans query propagates NotImplementedError, and mpm cleanup --orphans simply skips it.

Return type:

None

cleanup_cache()[source]¶

Prune the manager’s caches, downloads and other left-over artifacts.

The cache category of cleanup(), surfaced as mpm cleanup --cache and subtracted by --skip-cache (apt clean, dnf clean all, brew cleanup, npm cache clean, …). The broadest category: for most managers the whole cleanup amounts to it.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

None

cleanup_repair()[source]¶

Verify and repair the manager’s local installation state.

The repair category of cleanup(), surfaced as mpm cleanup --repair and subtracted by --skip-repair (flatpak repair --user).

Optional. Will be simply skipped by mpm if not implemented.

Return type:

None

doctor_cli()[source]¶

Returns the complete CLI running the manager’s native self-diagnosis.

The invocation must be read-only (brew doctor, pip check, pacman --database --check, …): doctor() runs it, never mpm’s mutating machinery. The surveyed doctor verbs share one convention this contract leans on: a non-zero exit code means problems were found.

Optional. Will be simply skipped by mpm if not implemented.

Return type:

tuple[str, ...]

doctor()[source]¶

Run the native self-diagnosis, returning (healthy, report).

Runs doctor_cli() and interprets the outcome with a contract of its own, distinct from every other operation:

  • Health is the exit code alone. run()’s failure gate tolerates a non-zero exit with a silent <stderr> (a benign status for query parsers), but for a diagnosis that exit is the verdict: pip check reports its conflicts on <stdout> only and would read as healthy under the gate.

  • The report merges both streams. The tools split their findings across them (brew doctor warns on <stderr>), and the report is relayed verbatim to the user: there is nothing to parse.

  • The diagnosis is not an error. The failure-gate entry an unhealthy exit may have accumulated is reclaimed from cli_errors, so the end-of-run error summary is not inflated by a verdict mpm doctor``already reports on its own. The gate’s``WARNING diagnosis relay is skipped for the same reason (doctor sits in the gate’s _DIAGNOSIS_EXEMPT_OPERATIONS): the findings land in the report, verbatim. A run that never completed (timeout, interrupt, missing binary) keeps its entry: that is a genuine plumbing error, and the manager reports unhealthy.

Return type:

tuple[bool, str]

discover_projects()[source]¶

Locate project trees this manager governs by scanning the filesystem.

Extension point reserved for ManagerScope.PROJECT managers: detecting virtual environments, lockfiles, or project manifests scattered across the filesystem.

Caution

Not implemented for any manager yet. System-scoped managers (the default) own no project trees to discover.

Todo

Implement project-scope discovery. The candidate ecosystems, the project files that signal each and the architecture this waits on are catalogued in Unsupported managers.

Return type:

Iterator[Path]