repomatic.deps.dep_report module

Shared rendering of dependency-update reports.

The markdown diff, held-back, and cooldown-bypass tables, the release-notes sections, and the comparison URLs that every updater’s PR body and terminal output route through: sync-uv-lock, sync-deps, sync-dep-sources, the three version-sync bumpers (sync-tool-versions, sync-action-pins, sync-workflow-pins), and audit --fix.

The computations stay with their datasources (repomatic.deps.uv for the lock, repomatic.release.version_sync for GitHub/PyPI/npm); this module only renders their results.

repomatic.deps.dep_report.RELEASE_NOTES_MAX_LENGTH = 2000

Maximum characters per package release body before truncation.

Render a table’s subject cell, linked when a URL is known for it.

Parameters:
  • name (str) – Package, action or tool name.

  • name_urls (dict[str, str] | None) – Mapping of names to their URL. Names absent from it (or a None mapping) render as plain text.

Return type:

str

Returns:

A markdown link, or the bare name.

repomatic.deps.dep_report.markdown_section(heading, note, headers, rows)[source]

Assemble a report section: heading, optional intro, and a table.

The one place the section layout is defined, so every updater’s PR body keeps the same shape. The alignment row is derived from headers rather than written out, which is what stops a column from being added to one and not the other.

Parameters:
  • heading (str) – Full heading line, emoji included, without the ##. Omitted when empty, for a caller embedding the table under a title of its own (the release PR’s blocker banner sits inside a [!CAUTION] blockquote that already says what it is).

  • note (str) – Intro paragraph shown between heading and table. Omitted when empty.

  • headers (tuple[str, ...]) – Column titles.

  • rows (list[tuple[str, ...]]) – One tuple of pre-rendered cells per row, each as long as headers.

Return type:

str

Returns:

The rendered markdown, with no trailing newline.

repomatic.deps.dep_report.format_upload_date(iso_datetime)[source]

Format an ISO 8601 datetime as a human-readable date string.

Parameters:

iso_datetime (str) – An ISO 8601 datetime string (e.g., "2026-03-13T12:00:00Z").

Return type:

str

Returns:

A formatted date like 2026-03-13, or the raw string if parsing fails.

repomatic.deps.dep_report.format_released(raw_upload, reference)[source]

Format an upload time as a date, optionally with a relative hint.

Parameters:
  • raw_upload (str) – ISO 8601 upload-time string, or empty.

  • reference (date | None) – Date to measure the relative offset from. When None, only the absolute date is returned.

Return type:

str

Returns:

A string like 2026-06-24 (2 days ago), the bare date when reference is None, or empty when raw_upload is empty.

repomatic.deps.dep_report.format_eligible(eligible, today)[source]

Render an eligibility date with a human-readable countdown.

Parameters:
  • eligible (date) – The date a release leaves the cooldown window.

  • today (date) – The current date, for the relative offset.

Return type:

str

Returns:

A string like 2026-06-25 (in 4 days), ... (today), or the bare date once the window has elapsed.

repomatic.deps.dep_report.pypi_name_urls(changes)[source]

Map each changed package name to its PyPI project URL.

Convenience for format_diff_table()’s name_urls when the changes come from a PyPI-resolved source (sync-uv-lock, fix-vulnerable-deps).

Return type:

dict[str, str]

repomatic.deps.dep_report.format_exclude_newer_note(exclude_newer)[source]

Render the uv exclude-newer cutoff sentence for a diff table.

The format_diff_table() counterpart for sync-uv-lock and fix-vulnerable-deps, which gate on uv’s absolute exclude-newer timestamp. The relative-cooldown updaters (repomatic.release.version_sync) render their own minimum-release-age note instead.

Parameters:

exclude_newer (str) – ISO 8601 datetime from the lock’s [options].exclude-newer, as carried by repomatic.deps.uv.LockFile.exclude_newer, or empty.

Return type:

str

Returns:

A one-line markdown note, or empty when exclude_newer is empty.

repomatic.deps.dep_report.format_diff_table(changes, upload_times=None, cooldown_note='', comparison_urls=None, reference_date=None, name_urls=None, heading='Updated packages', subject='Package', released_overrides=None)[source]

Format version changes as a markdown table with heading.

The shared PR-body table for every dependency updater (sync-uv-lock, fix-vulnerable-deps, sync-tool-versions, sync-action-pins, sync-workflow-pins) so they all render identically.

When upload_times is provided, a “Released” column is added so reviewers can visually verify that all updated packages respect the cooldown. A row whose version was decided outside that cooldown check (the upstream toolkit’s lockstep-aligned pin) marks itself through released_overrides instead of showing a date, so the exemption reads as deliberate rather than as missing data. When cooldown_note is provided, that pre-rendered sentence (the absolute exclude-newer cutoff for uv, or the relative minimum-release-age cutoff for the version-sync updaters) is shown above the table.

Parameters:
Return type:

str

Returns:

A markdown string with a ## 🆙 {heading} heading and table, or an empty string if there are no changes.

class repomatic.deps.dep_report.HeldBackPackage(name, locked_version, available_version, released, eligible)[source]

Bases: object

A newer release withheld from the lock by the exclude-newer cooldown.

Built by repomatic.deps.uv.compute_held_back_packages() for the ## Held back by cooldown report section: a package has already published a newer version, but it is still inside the cooldown window, so uv lock --upgrade keeps the older locked_version.

name: str

Package name, as it appears on PyPI.

locked_version: str

Version held in the lock: the newest release outside the cooldown.

available_version: str

Newer version already on the index, still inside the cooldown window.

released: str

Upload date of available_version (YYYY-MM-DD), or empty when the lock records no upload time (a git or path source).

eligible: str

Date available_version leaves the cooldown and becomes lockable, with a human-readable countdown (2026-06-25 (in 4 days)), or empty when it cannot be computed.

repomatic.deps.dep_report.EXCLUDE_NEWER_HELD_BACK_NOTE = 'Newer releases already published but withheld because they are still inside the [`exclude-newer`](https://docs.astral.sh/uv/reference/settings/#exclude-newer) cooldown window.'

Intro paragraph for the sync-uv-lock held-back section.

The repomatic.release.version_sync updaters pass their own minimum-release-age wording to format_held_back_table() instead.

repomatic.deps.dep_report.HELD_BACK_COLUMNS = ('Locked', 'Available', 'Released', 'Eligible')

Held-back columns following the caller-supplied subject column.

Shared with repomatic.sync_ops.print_held_back_table(), so a run’s markdown PR body and its terminal table name the same columns in the same order instead of drifting apart as two hand-kept literals.

repomatic.deps.dep_report.build_held_back(name, pinned, available, available_date, min_age, today)[source]

Assemble a HeldBackPackage row from raw selection data.

The formatting half of the version-sync held-back report: repomatic.release.version_sync.select_held_back() picks the withheld candidate, and this turns its raw version and upload date into the same released/eligible strings repomatic.deps.uv.compute_held_back_packages() produces for uv, so format_held_back_table() renders both identically. Unlike the uv path, no second resolution is needed: the candidates are already in hand from the datasource sweep.

Parameters:
  • name (str) – Display name (package, action slug, or tool).

  • pinned (str) – Version this run settled on (held in place by the cooldown).

  • available (str) – The newer version still inside the cooldown window.

  • available_date (str) – Upload date of available (YYYY-MM-DD), or empty.

  • min_age (timedelta) – The minimum-release-age cooldown width.

  • today (date) – Reference date for the relative countdown.

Return type:

HeldBackPackage

Returns:

A populated HeldBackPackage.

repomatic.deps.dep_report.format_held_back_table(held_back, note='Newer releases already published but withheld because they are still inside the [`exclude-newer`](https://docs.astral.sh/uv/reference/settings/#exclude-newer) cooldown window.', *, name_urls=None, subject='Package')[source]

Format cooldown-withheld releases as a markdown section.

Shared by every cooldown-gated updater: sync-uv-lock (rows from repomatic.deps.uv.compute_held_back_packages()) and the version-sync commands (rows from build_held_back()), so the section renders identically.

Parameters:
  • held_back (list[HeldBackPackage]) – Withheld releases as HeldBackPackage rows.

  • note (str) – Intro paragraph describing the cooldown. Defaults to the uv exclude-newer wording; version-sync passes its minimum-release-age wording.

  • name_urls (dict[str, str] | None) – Optional mapping of names to a URL the name links to (PyPI, GitHub, npm). Names absent from the mapping render plain.

  • subject (str) – Header for the first column (e.g. Action, Tool).

Return type:

str

Returns:

A markdown string with a ## ⏸️ Held back by cooldown heading and table, or an empty string when held_back is empty.

repomatic.deps.dep_report.BYPASS_NEEDS_RELEASE = 'needs release'

Expiry placeholder for a freeze holding an unreleased version.

A fixed-timestamp exclude-newer-package entry whose held version has no upload time in the lock (a git, path, or otherwise unpublished source) can never age past the rolling exclude-newer cutoff on its own: the freeze only ends once the package ships a release the lock can adopt. The markdown report renders the marker in italics to set it apart from real dates.

class repomatic.deps.dep_report.BypassForecast(name, held_version, expires)[source]

Bases: object

A cooldown-bypass freeze and the date it self-clears.

Built by repomatic.deps.uv.compute_bypass_forecasts() (freezes still active) and repomatic.deps.uv.compute_pruned_forecasts() (freezes the run just cleared) for the ## ❄️ Cooldown bypasses report section: a fixed-timestamp exclude-newer-package entry holds name at held_version until that version ages past the exclude-newer cutoff, at which point sync-uv-lock prunes the entry and the package resumes normal cooldown resolution.

name: str

Package name, as it appears on PyPI.

held_version: str

Version the freeze holds in the lock.

expires: str

Date the freeze expires and the entry is pruned, with a human-readable countdown (2026-07-08 (in 2 days), in the past for an already-cleared freeze), BYPASS_NEEDS_RELEASE when the held version has no upload time in the lock, or empty when there is no rolling exclude-newer span to forecast against.

repomatic.deps.dep_report.BYPASS_SECTION_NOTE = 'Packages pulled in ahead of the cooldown by an [`exclude-newer-package`](https://docs.astral.sh/uv/reference/settings/#exclude-newer-package) freeze. Each entry is cleared from `pyproject.toml` automatically once its held version ages past the `exclude-newer` cutoff.'

Intro paragraph for the sync-uv-lock cooldown-bypasses section.

repomatic.deps.dep_report.BYPASS_COLUMNS = ('Package', 'Held at', 'Held until')

Columns of the cooldown-bypass table.

Shared with repomatic.sync_ops.print_bypass_table() for the reason HELD_BACK_COLUMNS is.

repomatic.deps.dep_report.format_bypass_section(forecasts, pruned=None, frozen=None, *, name_urls=None)[source]

Format the cooldown-bypass lifecycle as a single markdown table.

The sync-uv-lock report section covering exclude-newer-package freezes. Every lifecycle state is a row in one table so the section scans like the ## 🆙 Updated packages one: freezes still active render plain, entries this run rewrote into freeze cutoffs are labelled 📌 frozen:, and expired entries this run removed from pyproject.toml are labelled 🧹 cleared:, keeping the version and expiry data the freeze had. A freeze holding an unreleased version is labelled 🚧 unreleased: and its BYPASS_NEEDS_RELEASE expiry renders in italics.

Parameters:
Return type:

str

Returns:

A markdown string with a ## ❄️ Cooldown bypasses heading and table, or an empty string when there is no row to report.

repomatic.deps.dep_report.fetch_release_notes(changes)[source]

Fetch release notes for all updated packages.

For each package with a new version, discovers the GitHub repository via PyPI and fetches the release notes from GitHub Releases for all versions in the range (old, new]. Falls back to a changelog link from PyPI project_urls when no GitHub Release exists.

Parameters:

changes (list[tuple[str, str, str]]) – List of (name, old_version, new_version) tuples.

Return type:

dict[str, tuple[str, list[tuple[str, str]]]]

Returns:

A dict mapping package names to (repo_url, versions) tuples where versions is a list of (tag, body) pairs sorted ascending. Only packages with at least one non-empty body are included. When a changelog URL is used as fallback, tag is empty and body contains a markdown link.

repomatic.deps.dep_report.format_release_notes(notes)[source]

Render release notes as collapsible <details> blocks.

A ### Release notes heading (an h3, nesting the section under the PR body’s h2 update table) with one collapsible section per package, each version introduced by an h4 tag heading. Long release bodies are truncated to RELEASE_NOTES_MAX_LENGTH characters with a link to the full release.

Parameters:

notes (dict[str, tuple[str, list[tuple[str, str]]]]) – A dict mapping package names to (repo_url, versions) tuples where versions is a list of (tag, body) pairs, as returned by fetch_release_notes().

Return type:

str

Returns:

A markdown string with the release notes section, or an empty string if no notes are available.

repomatic.deps.dep_report.build_comparison_urls(changes, notes)[source]

Build GitHub comparison URLs from version changes and release notes.

Uses the tag format discovered by fetch_release_notes() to construct comparison URLs. Only packages with both old and new versions and a known GitHub repository are included.

A package whose notes carry no tag at all is skipped. That happens when fetch_release_notes() found no GitHub release for the range and fell back to a changelog link, which is positive evidence that the tags this URL would name do not exist: guessing a v prefix there yields a 404 in the PR body.

Parameters:
Return type:

dict[str, str]

Returns:

Dict mapping package names to GitHub comparison URLs.