repomatic.deps.dep_graph module

Generate Mermaid dependency graphs from uv lockfiles.

Every box in the graph (the primary dependencies rectangle and each --group/--extra subgraph) only holds directly-declared dependencies, drawn as hexagons: the packages under the project’s control, referenced in pyproject.toml. Transitive dependencies always render outside the boxes, as plain ovals.

Note

Uses uv export --format cyclonedx1.5 which provides structured JSON with dependency relationships, replacing the need for pipdeptree.

Warning

The generated Mermaid syntax targets the version bundled with sphinxcontrib-mermaid, currently 11.12.1. See the hard-coded MERMAID_VERSION constant in sphinxcontrib-mermaid’s source. Avoid using Mermaid features introduced after that version.

repomatic.deps.dep_graph.STYLE_PRIMARY_DEPS_SUBGRAPH: str = 'fill:#1565C020,stroke:#42A5F5'

Mermaid style for the primary dependencies subgraph box.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps.dep_graph.STYLE_EXTRA_SUBGRAPH: str = 'fill:#7B1FA220,stroke:#BA68C8'

Mermaid style for extra dependency subgraph boxes.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps.dep_graph.STYLE_GROUP_SUBGRAPH: str = 'fill:#546E7A20,stroke:#90A4AE'

Mermaid style for group dependency subgraph boxes.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps.dep_graph.STYLE_PRIMARY_NODE: str = 'stroke-width:3px'

Mermaid style for root and primary dependency nodes (thick border).

repomatic.deps.dep_graph.STYLE_DUPLICATE_NODE: str = 'stroke-width:3px,stroke-dasharray:5 5'

Mermaid style for duplicate headline nodes (dashed thick border).

The dashes mark the node as a display-only mirror of the real node owned by another subgraph; a dotted identity link ties the two together. Derived from STYLE_PRIMARY_NODE since duplicates are always headline (primary) dependencies of their box.

class repomatic.deps.dep_graph.SubgraphKind(*values)[source]

Bases: Enum

Kind of dependency selector a subgraph box represents.

GROUP = 'group'
EXTRA = 'extra'
property flag: str

CLI flag selecting this kind, shown as the box title prefix.

available(project_root=None)[source]

Discover this kind’s declared names from pyproject.toml.

Groups come from the [dependency-groups] table, extras from [project.optional-dependencies].

Parameters:

project_root (Path | None) – Directory holding pyproject.toml. Defaults to the current working directory.

Return type:

tuple[str, ...]

Returns:

Sorted tuple of group or extra names.

property mermaid_prefix: str

Namespace prefix keeping subgraph IDs distinct from node IDs.

Without it, a json5 extra box would collide with a json5 package node.

property style: str

Mermaid style for boxes of this kind.

class repomatic.deps.dep_graph.Subgraph(kind, name, owned, duplicates)[source]

Bases: object

One --group or --extra box in the rendered graph.

A box only holds the packages its group or extra declares directly: the dependencies under the project’s control, referenced in pyproject.toml. Transitive dependencies always render outside the boxes, exactly like the transitive dependencies of the primary set.

kind: SubgraphKind

Whether the box represents a dependency group or an optional extra.

name: str

Group or extra name, as declared in pyproject.toml.

owned: set[str]

Directly-declared packages this box renders as real hexagon nodes.

duplicates: set[str]

Directly-declared packages owned by a sibling box.

Rendered as display-only duplicate nodes tied to the real node by a dotted identity link. See attribute_subgraph_packages().

property mermaid_id: str

Mermaid subgraph ID, namespaced away from node IDs.

property title: str

Box title, echoing the CLI flag that pulls these packages in.

repomatic.deps.dep_graph.MERMAID_RESERVED_KEYWORDS: frozenset[str] = frozenset({'C4Component', 'C4Container', 'C4Deployment', 'C4Dynamic', '_blank', '_parent', '_self', '_top', 'call', 'class', 'classDef', 'click', 'end', 'flowchart', 'flowchart-v2', 'graph', 'interpolate', 'linkStyle', 'style', 'subgraph'})

Mermaid keywords that cannot be used as node IDs.

repomatic.deps.dep_graph.normalize_package_name(name)[source]

Normalize package name for use as Mermaid node ID.

Converts to lowercase and replaces non-alphanumeric characters with underscores. Appends _0 suffix to avoid conflicts with Mermaid reserved keywords.

Return type:

str

repomatic.deps.dep_graph.resolve_subgraph_selection(kind, explicit, select_all, excluded, only, config_all, config_excluded)[source]

Resolve which groups or extras the graph should render.

Mirrors one selection axis of the update-dep-graph command: explicit CLI values win over the [tool.repomatic] dependency-graph defaults; --only-* replaces the explicit selection; --all-* expands to every name declared in pyproject.toml; --no-* prunes last.

Parameters:
  • kind (SubgraphKind) – The axis to resolve, groups or extras.

  • explicit (tuple[str, ...]) – Names selected one by one (--group/--extra).

  • select_all (bool) – Select every declared name (--all-groups/--all-extras).

  • excluded (tuple[str, ...]) – Names to prune from the selection (--no-group/--no-extra).

  • only (tuple[str, ...]) – Names selected in exclusive mode (--only-group/--only-extra).

  • config_all (bool) – Configured default for select_all, applied when no selection flag is passed.

  • config_excluded (Sequence[str]) – Configured default for excluded.

Return type:

tuple[str, ...] | None

Returns:

Selected names, or None when the axis is not requested at all.

repomatic.deps.dep_graph.get_cyclonedx_sbom(package=None, groups=None, extras=None, frozen=True)[source]

Run uv export and return the CycloneDX SBOM as a dictionary.

Results are cached to avoid redundant subprocess calls within the same process.

Parameters:
  • package (str | None) – Optional package name to focus the export on.

  • groups (tuple[str, ...] | None) – Optional dependency groups to include (e.g., “test”, “typing”).

  • extras (tuple[str, ...] | None) – Optional extras to include (e.g., “xml”, “json5”).

  • frozen (bool) – If True, use –frozen to skip lock file updates.

Return type:

dict[str, Any]

Returns:

Parsed CycloneDX SBOM dictionary.

Raises:
repomatic.deps.dep_graph.get_package_names_from_sbom(sbom)[source]

Extract all package names from a CycloneDX SBOM.

Parameters:

sbom (dict[str, Any]) – Parsed CycloneDX SBOM dictionary.

Return type:

set[str]

Returns:

Set of package names.

repomatic.deps.dep_graph.build_dependency_graph(sbom)[source]

Build a dependency graph from CycloneDX SBOM data.

Parameters:

sbom (dict[str, Any]) – Parsed CycloneDX SBOM dictionary.

Return type:

tuple[str, set[str], list[tuple[str, str]]]

Returns:

Tuple of (root_name, package_names, edges_list) where: - root_name is the root package name - package_names is the set of all package names - edges_list is a list of (from_name, to_name) tuples

repomatic.deps.dep_graph.filter_root_edges(root_name, edges, main_deps, subgraphs)[source]

Drop root edges that no pyproject.toml declaration backs.

uv’s CycloneDX export hangs a dependency-group package off the root as soon as that package lands in the resolved component set, whether or not the group was requested. Exporting click-extra with --extra sphinx and no --group is enough for requests to come back as a direct dependency of the project: Sphinx pulls it in, the test group happens to declare it too, and the export conflates the two. Neither omitting --group nor passing --no-default-groups suppresses it.

Left in place, such an edge lands the package in the primary dependencies box, labelled with the specifier of a group nobody asked for, claiming the project depends on something a plain install never installs. So the root’s direct dependencies are re-derived from uv.lock, which records what pyproject.toml declares rather than what resolution happened to produce.

Edges into a box-owned package survive: render_mermaid() turns those into the box’s dashed arrow. Edges that do not start at the root are never touched, so the dropped package keeps rendering as a transitive dependency of whatever actually pulls it in.

Parameters:
  • root_name (str) – The root package name.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • main_deps (set[str] | None) – Names the root declares as main dependencies, from by_main. None when the lockfile describes no such package, in which case every edge is kept: missing data is not evidence that an edge is spurious.

  • subgraphs (Sequence[Subgraph]) – Boxes whose owned packages legitimately hang off the root.

Return type:

list[tuple[str, str]]

Returns:

The edge list, without the unbacked root edges.

repomatic.deps.dep_graph.filter_graph_to_package(packages, edges, package)[source]

Filter the graph to only include dependencies of a specific package.

Parameters:
  • packages (set[str]) – Set of all package names.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • package (str) – Package name to filter to.

Return type:

tuple[set[str], list[tuple[str, str]]]

Returns:

Filtered (packages, edges) tuple.

repomatic.deps.dep_graph.trim_graph_to_depth(root_name, packages, edges, depth)[source]

Trim the graph to only include nodes within a given depth from the root.

Performs a breadth-first traversal from the root, keeping only nodes reachable within depth hops and edges between those nodes.

Parameters:
  • root_name (str) – The root package name.

  • packages (set[str]) – Set of all package names.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • depth (int) – Maximum depth from root. 0 = root only, 1 = root + primary deps, etc.

Return type:

tuple[set[str], list[tuple[str, str]]]

Returns:

Filtered (packages, edges) tuple.

repomatic.deps.dep_graph.render_mermaid(root_name, packages, edges, subgraphs=None, lock_specs=None)[source]

Render the dependency graph as a Mermaid flowchart.

Warning

Output must stay compatible with the Mermaid version bundled in sphinxcontrib-mermaid. See module docstring for details.

Every box holds only directly-declared dependencies, drawn as hexagons with a thick border; transitive dependencies render outside the boxes as plain ovals. See the module docstring.

Parameters:
  • root_name (str) – The root package name (used to highlight it).

  • packages (set[str]) – Package names to render as nodes.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • subgraphs (list[Subgraph] | None) – Boxes to render, in display order (extras before groups keeps them closer to the main dependencies). See Subgraph.

  • lock_specs (LockSpecifiers | None) – Optional specifiers extracted from uv.lock. Provides edge labels (by_package) and subgraph node labels (by_subgraph).

Return type:

str

Returns:

Mermaid flowchart string.

repomatic.deps.dep_graph.attribute_subgraph_packages(subgraph_closures, base_packages, direct_packages, edges, root_name)[source]

Attribute each directly-declared package to one owning subgraph box.

Boxes only hold the packages their group/extra declares directly; transitive dependencies stay outside every box (see the module docstring). A directly-declared package can still be claimed by several boxes, but a graph node can live in only one: the declarer whose closure holds the most dependents wins the real node (declaration order breaks ties), since arrows point where the package is consumed and the busiest box is its most natural home. The root is not a dependent, as it reaches every declared package by definition.

The losing declarers list the package as a duplicate headline so every box still shows the dependency it exists to install (rendered as a display-only duplicate node by render_mermaid()). For example the carapace and yaml extras both declare only pyyaml, which no other package depends on: the dependent counts tie at zero, carapace owns the node by declaration order, and yaml carries pyyaml as a duplicate.

Parameters:
  • subgraph_closures (list[tuple[str, set[str]]]) – Ordered (name, closure_package_names) pairs. Order is the last-resort tie-break for shared packages (first wins).

  • base_packages (set[str]) – Packages in the base set, excluded from every box.

  • direct_packages (dict[str, set[str]]) – Map of subgraph name to the package names it declares directly (from uv.lock), keyed by SBOM-normalized name.

  • edges (list[tuple[str, str]]) – (from_name, to_name) dependency edges from the full SBOM, used to count each declaring subgraph’s local dependents.

  • root_name (str) – The root package name, excluded from dependent counts.

Return type:

tuple[dict[str, set[str]], dict[str, set[str]]]

Returns:

(owned, duplicates). owned maps each subgraph to the declared packages it renders as real nodes; duplicates maps it to declared packages owned by a sibling box.

repomatic.deps.dep_graph.generate_dependency_graph(package=None, groups=None, extras=None, frozen=True, depth=None, exclude_base=False)[source]

Generate a Mermaid dependency graph.

Each requested group/extra renders as a box holding only the packages it declares directly; the transitive dependencies they pull in render outside the boxes, like the transitive dependencies of the main set.

Parameters:
  • package (str | None) – Optional package name to focus on. If None, shows the entire project dependency tree.

  • groups (tuple[str, ...] | None) – Optional dependency groups to include (e.g., “test”, “typing”).

  • extras (tuple[str, ...] | None) – Optional extras to include (e.g., “xml”, “json5”).

  • frozen (bool) – If True, use –frozen to skip lock file updates.

  • depth (int | None) – Optional maximum depth from root. If None, shows the full tree.

  • exclude_base (bool) – If True, exclude main (base) dependencies from the graph, showing only packages unique to the requested groups/extras. Used by --only-group and --only-extra.

Return type:

str

Returns:

The graph in Mermaid format.