Coverage for diagrams / code_diagrams / _readme.py: 100.00%
44 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1"""Hierarchical ``code_diagrams/README.md`` renderer.
3The README is regenerated on every run so the index never drifts
4from what's actually on disk. It groups flowcharts by top-level
5source directory (``lambda/``, ``cli/``, ``gco/``, ...) and then by
6parent directory within that, mirroring the project layout.
7"""
9from __future__ import annotations
11from collections import defaultdict
12from pathlib import Path
14from diagrams.code_diagrams._renderer import RenderedTarget
16_HEADER = """\
17# GCO Code Flowcharts
19<!-- Generated at (UTC): __GENERATED_AT__ -->
20<!-- Generated from Git commit: __SOURCE_COMMIT__ -->
21*Generated at (UTC): `__GENERATED_AT__`.*
22*Generated from Git commit: `__SOURCE_COMMIT__`.*
24This directory holds auto-generated control-flow diagrams for the
25Python source files listed below. Each target produces an interactive
26[flowchart.js](https://github.com/adrai/flowchart.js) HTML page and (if
27Playwright is available) a rendered PNG.
29> Interactive HTML is the primary artifact — open it in any browser to
30> pan, zoom, and export SVG/PNG directly. The PNGs are included for
31> embedding in READMEs and pull requests where JS can't run.
33## Table of Contents
35- [Regeneration](#regeneration)
36- [Prerequisites](#prerequisites)
37- [Flowchart index](#flowchart-index)
39## Regeneration
41Use the aggregate driver for canonical committed output:
43```bash
44# Full code + infrastructure catalogues at the reviewed timestamp
45SOURCE_DATE_EPOCH=1788091200 \
46GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> \
47python diagrams/generate.py
49# Read-only artifact/index/marker/PNG contract
50python diagrams/generate.py --check
52# One catalogue only
53SOURCE_DATE_EPOCH=1788091200 \
54GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> \
55python diagrams/generate.py --code-only
56python diagrams/generate.py --infra-only
58# A single target for local diagnosis
59GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> \\
60python diagrams/code_diagrams/generate.py \\
61 --target lambda/analytics-presigned-url/handler.py:lambda_handler
63# HTML only (skip Playwright and remove older PNGs for selected targets)
64GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> \\
65python diagrams/code_diagrams/generate.py --skip-png
67# Don't insert/refresh the ``# Flowchart:`` markers in source files
68GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> \\
69python diagrams/code_diagrams/generate.py --skip-marker
71# Remove every existing marker from the source tree and exit
72# (useful when tearing the feature down or before a big refactor
73# of placement rules)
74python diagrams/code_diagrams/generate.py --strip-markers
75```
77See the [Prerequisites](#prerequisites) section below for one-time
78browser install steps.
80## Prerequisites
82Install the project's ``diagrams`` extra, which pins ``pyflowchart`` and
83``playwright`` to known-good versions:
85```bash
86pip install -e '.[diagrams]'
87playwright install chromium
88```
90Without Playwright's browser, direct code-generator runs still write HTML and
91remove any older PNG for the selected targets so mixed generation times are
92impossible. Canonical aggregate generation requires ``SOURCE_DATE_EPOCH`` and
93``GCO_DIAGRAM_SOURCE_COMMIT``. Commit substantive source changes first, then
94supply that clean source commit while generating and commit the derived
95artifacts separately; this avoids an impossible self-referential commit SHA.
96The generator verifies every marker-stripped charted source against the supplied
97commit. It records one UTC timestamp and source commit in HTML, PNG pixels, the
98catalogue, and source markers. Each HTML/PNG pair also displays a deterministic
99digest of the pre-annotation flow HTML, so source-flow changes remain visible
100even when flowchart.js collapses them into the same SVG shape. Fixing the
101timestamp prevents metadata-only churn; none of these mechanisms promises
102byte-identical Chromium or Graphviz rasterization across toolchain versions or
103platforms.
104``python diagrams/generate.py --check`` enforces structural
105contracts; ``tests/test_diagram_artifact_contract.py`` also verifies every PNG
106with Pillow.
108## Flowchart index
110Entries below are grouped by top-level directory and listed in source
111order. Each source file may contribute more than one flowchart if it
112has multiple charted entry points.
113"""
116def render_readme(
117 results: list[RenderedTarget],
118 *,
119 output_dir: Path,
120) -> str:
121 """Render the full README markdown body as a string."""
122 sections = _group_by_toplevel(results)
123 # Provenance is per source (see ``provenance.json``), so the catalogue is
124 # legitimately a mix of vintages: incremental regeneration restamps only
125 # the sources that changed. The index header therefore reports the most
126 # recent generation; each source's own stamp lives in its marker block and
127 # in the manifest.
128 newest = max(results, key=lambda result: result.generated_at, default=None)
129 generated_at = newest.generated_at if newest is not None else "unknown"
130 source_commit = newest.source_commit if newest is not None else "unknown"
131 header = _HEADER.replace("__GENERATED_AT__", generated_at).replace(
132 "__SOURCE_COMMIT__", source_commit
133 )
134 lines = [header.rstrip()]
135 for top, dir_groups in sections.items():
136 lines.append("")
137 lines.append(f"### `{top}/`")
138 for dir_path, entries in dir_groups.items():
139 lines.append("")
140 display_dir = dir_path if dir_path else top
141 lines.append(f"- **`{display_dir}/`**")
142 for entry in entries:
143 lines.append(_format_entry(entry, output_dir=output_dir))
144 lines.append("")
145 return "\n".join(lines)
148def _group_by_toplevel(
149 results: list[RenderedTarget],
150) -> dict[str, dict[str, list[RenderedTarget]]]:
151 """Group results into ``{top_level: {parent_dir: [results]}}``.
153 ``dict`` preservation of insertion order keeps the README stable:
154 sort top-level groups alphabetically, then sort inner directory
155 groups alphabetically, then leave each directory's target list in
156 its original :data:`TARGETS` order.
157 """
158 grouped: dict[str, dict[str, list[RenderedTarget]]] = defaultdict(
159 lambda: defaultdict(list),
160 )
161 for result in results:
162 src = Path(result.target.source)
163 top = src.parts[0]
164 parent = str(src.parent)
165 grouped[top][parent].append(result)
167 ordered: dict[str, dict[str, list[RenderedTarget]]] = {}
168 for top in sorted(grouped):
169 ordered[top] = {k: grouped[top][k] for k in sorted(grouped[top])}
170 return ordered
173def _format_entry(
174 entry: RenderedTarget,
175 *,
176 output_dir: Path,
177) -> str:
178 """Render a single bullet for one flowchart target.
180 Uses a 2-space indent on the nested bullet level so the output
181 passes markdownlint's MD007/ul-indent rule (default expected
182 indent = 2 spaces).
183 """
184 html_rel = entry.html_path.relative_to(output_dir)
185 src = entry.target.source
186 func = entry.target.function
187 title = entry.target.title or f"`{func}`"
188 line = f" - {title} — `{src}::{func}` — [HTML](./{html_rel.as_posix()})"
189 if entry.png_path is not None:
190 png_rel = entry.png_path.relative_to(output_dir)
191 line += f" · [PNG](./{png_rel.as_posix()})"
192 return line