repomatic.tooling.tool_runner module

Unified tool runner with managed config resolution.

Provides repomatic run <tool> — a single entry point that installs an external tool at a pinned version, resolves its configuration through a strict 4-level precedence chain, translates [tool.X] sections from pyproject.toml into the tool’s native format, and invokes the tool with the resolved config. The tool catalog it drives (ToolSpec entries, pinned versions, checksums) lives in tool_registry.py.

Important

Config resolution precedence (first match wins, no merging):

  1. Native config file — tool’s own config file in the repo.

  2. ``[tool.X]`` in ``pyproject.toml`` — translated to native format.

  3. Bundled default — from repomatic/data/.

  4. Bare invocation — no config at all.

repomatic.tooling.tool_runner.load_pyproject_tool_section(tool_name)[source]

Load [tool.<tool_name>] from pyproject.toml in the current directory.

Returns the live tomlrt.Table (a dict subclass) rather than a plain-dict copy, so the section keeps its comment trivia for formats that can preserve it on materialization (see NativeFormat.serialize()). Callers that only read values or test truthiness are unaffected.

Return type:

dict[str, Any]

Returns:

The tool’s config table, or empty dict if not found.

repomatic.tooling.tool_runner.resolve_config(spec, tool_config=None)[source]

Resolve config for a tool using the 4-level precedence chain.

Caution

The levels do not merge. The walk stops at its first hit, so a native config file or a [tool.X] section replaces the bundled default in full rather than layering on top of it. A downstream repo overriding one rule must restate every bundled rule it wants to keep, and gains nothing when the bundled default later grows a rule. resolve_config_source() labels a shadowing config so repomatic run --list shows the loss.

Parameters:
  • spec (ToolSpec) – Tool specification.

  • tool_config (dict[str, Any] | None) – Pre-loaded [tool.X] config dict. If None, reads from pyproject.toml in the current directory.

Return type:

tuple[list[str], Path | None]

Returns:

Tuple of (extra CLI args for config, path to clean up). The path is None when no cleanup is needed (cache-based configs persist across runs). Non-None paths are CWD files written for tools that have no --config flag.

repomatic.tooling.tool_runner.DOWNLOAD_TIMEOUT = 30

Socket-level timeout for artifact downloads, in seconds.

A stall guard, not a transfer budget: urlopen applies it to each blocking socket operation, so a healthy multi-minute download is unaffected while a dead connection fails in seconds instead of hanging a CI job to the runner ceiling. Deliberately larger than repomatic.http.DEFAULT_TIMEOUT, which is sized for small JSON API responses.

repomatic.tooling.tool_runner.download_to(url, dest_path, *, label=None, progress=True)[source]

Stream url into dest_path and return its SHA-256 hex digest.

Chunked download with incremental hash computation, so large binaries never load fully into memory. Shows a progress bar on interactive terminals when the server provides a Content-Length header; pass progress=False from concurrent callers whose fan-out draws its own progress display.

The single download seam for every artifact repomatic fetches by hand: whatever consumes the digest (verification in _download_and_verify(), checksum harvesting in checksums.py) builds on this so the truncation guard below applies to all of them. A short body (proxy hiccup, dropped connection) hashes to a wrong digest, so without the guard it would surface later as a checksum mismatch: that reads as a stale pin or a tampered artifact when nothing is wrong upstream. Name the real failure instead.

Parameters:
  • url (str) – URL to download.

  • dest_path (Path) – Where to write the downloaded file.

  • label (str | None) – Progress bar label. Defaults to the destination filename.

  • progress (bool) – Draw per-download feedback on interactive terminals.

Return type:

str

Returns:

Lowercase hex SHA-256 digest of the downloaded bytes.

Raises:

OSError – If the body is shorter than the advertised Content-Length.

repomatic.tooling.tool_runner.ensure_binary(name: str) Path[source]

Install a registry binary tool and return the path to its executable.

The seam for repomatic code that shells out to a third-party binary but is not itself a run_tool() invocation. It buys the same guarantees every repomatic run binary gets: the registry-pinned version, its archive verified against the recorded SHA-256, and a shared cache so repeated calls in one run download once.

Prefer this over looking the tool up on PATH. Whatever PATH offers is whichever version the machine or CI image happens to carry, unpinned and unverified, and it differs between a developer’s laptop and every runner.

Memoized per tool name: callers in a loop (format-images optimizing one PNG per call) hit the install-and-verify path once per process, not once per file. Failures are not memoized, so a transient download error can be retried.

Parameters:

name (str) – Registry key of a tool whose ToolSpec declares a binary.

Return type:

Path

Returns:

Absolute path to the ready-to-run executable.

Raises:

ClickException – If the tool is unknown, ships no binary, or cannot be downloaded and verified.

repomatic.tooling.tool_runner.resolve_default_args(spec)[source]

Build the argument batches for a bare repomatic run <tool>.

Combines default_args with the file list named by default_paths, splitting into one batch per file when per_file is set.

Parameters:

spec (ToolSpec) – The tool to resolve defaults for.

Return type:

list[list[str]] | None

Returns:

One argument list per invocation; a single empty-argument batch when the tool declares no defaults, so the caller runs it bare as before. None when the tool wants targets and the repository holds none, which means skip the tool rather than invoke it pathless.

repomatic.tooling.tool_runner.TOOL_CRASH_EXIT_CODE = 70

Exit code reported when a tool contradicts its own rewrite status.

EX_SOFTWARE from sysexits.h: an internal error in the tool being run. Deliberately outside the set a formatter’s caller tolerates, so a crash cannot land on the code that means “I reformatted a file”. See rewrite_exit_code.

repomatic.tooling.tool_runner.run_tool(name, extra_args=(), version=None, checksum=None, skip_checksum=False, no_cache=False)[source]

Run an external tool with managed config resolution.

With no extra_args, a tool declaring default_args or default_paths runs the invocation CI performs, resolved in-process by resolve_default_args(). Any explicit argument suppresses that entirely and is passed through as before.

Parameters:
  • name (str) – Tool name (must be in TOOL_REGISTRY).

  • extra_args (Sequence[str]) – Extra arguments passed through to the tool.

  • version (str | None) – Override the pinned version.

  • checksum (str | None) – Override the SHA-256 checksum for the current platform.

  • skip_checksum (bool) – Skip SHA-256 verification entirely.

  • no_cache (bool) – Bypass the binary cache when True.

Return type:

int

Returns:

The tool’s exit code; the first non-zero one when the defaults resolved to several invocations, or TOOL_CRASH_EXIT_CODE when a tool declaring rewrite_exit_code reports a rewrite it did not perform.

repomatic.tooling.tool_runner.verify_via_write_path(name, extra_args=(), **run_kwargs)[source]

Check a post_process tool’s formatting without touching the tree.

A tool pairing post_process with check_flags has no trustworthy check mode: the fixup only runs on the write path, so the check status can flag drift the write path would reconcile, or miss drift it would introduce (see check_flags). This runs the write path against throwaway copies instead, then compares, which is the only authoritative answer.

Important

The copies are made inside the working directory, not in the system temp area. Formatters discover their config by walking up from each file, so a copy parked outside the repository resolves a different config and silently reports drift that does not exist.

The working tree is never written to: only the copies are formatted, and they are removed before returning.

Parameters:
  • name (str) – Tool name, as in run_tool().

  • extra_args (Sequence[str]) – Arguments for the tool. Any existing path among them is copied and rewritten to its copy; check flags are dropped, since they would defeat the write path this relies on. Every other argument is passed through untouched. Empty resolves the tool’s registry defaults, the same set run_tool() would have run, flattened into one batch: the copies are per-path already, so a per_file split would only cost extra invocations.

  • run_kwargs (Any) – Forwarded verbatim to run_tool().

Return type:

tuple[int, list[str]]

Returns:

(exit_code, drifted), where exit_code is 0 when every target is already formatted and 1 otherwise, and drifted names the paths the write path would have changed. A tool that fails on the copies yields its own exit code and no drift, since it measured nothing.

repomatic.tooling.tool_runner.resolve_config_source(spec)[source]

Return a human-readable description of the active config source.

Used by repomatic run --list to show which precedence level is active for each tool in the current repo.

Return type:

str

repomatic.tooling.tool_runner.find_unmodified_configs(root=None)[source]

Find native config files identical to their bundled defaults.

Iterates over every tool in TOOL_REGISTRY that has a default_config. For each, checks whether any of its native_config_files exists on disk and is content-identical to the bundled default after trailing-whitespace normalization.

The normalization (rstrip() + "\n") matches the convention used by _init_config_files when writing files during init.

Parameters:

root (Path | None) – Directory the relative config paths resolve against. Defaults to the working directory; run_init passes its output_dir so the scan and the deletion the CLI derives from it (--delete-unmodified) agree on one tree.

Return type:

list[tuple[str, str]]

Returns:

List of (tool_name, relative_path) tuples for each unmodified file found.