Skip to content

Commit c49c4a5

Browse files
committed
Migrate auto-generated docs code to docs_update.py
1 parent 9fccce1 commit c49c4a5

4 files changed

Lines changed: 185 additions & 187 deletions

File tree

.github/workflows/autofix.yaml

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,14 @@
11
---
2-
name: Autofix
2+
name: 🪄 Autofix
33
"on":
4+
workflow_dispatch:
45
push:
5-
# Only targets main branch to avoid amplification effects of auto-fixing
6-
# the exact same stuff in multiple non-rebased branches.
76
branches:
87
- main
98

109
jobs:
1110

12-
update-readme:
13-
name: Update readme
14-
runs-on: ubuntu-slim
15-
steps:
16-
- uses: actions/checkout@v6.0.2
17-
- uses: astral-sh/setup-uv@v7.2.0
18-
- name: Update readme
19-
run: >
20-
uv --no-progress run --frozen --
21-
python -c 'from meta_package_manager.inventory import update_readme; update_readme()'
22-
- uses: peter-evans/create-pull-request@v8.1.0
23-
with:
24-
assignees: ${{ github.actor }}
25-
commit-message: "[autofix] Update readme"
26-
title: "[autofix] Update readme"
27-
body: >
28-
<details><summary><code>Workflow metadata</code></summary>
29-
30-
31-
> [Auto-generated on run `#${{ github.run_id }}`](${{ github.event.repository.html_url }}/actions/runs/${{
32-
github.run_id }}) by `${{ github.job }}` job from [`docs.yaml`](${{ github.event.repository.html_url
33-
}}/blob/${{ github.sha }}/.github/workflows/labels.yaml) workflow.
34-
35-
36-
</details>
37-
labels: "📚 documentation"
38-
branch: update-readme
39-
add-paths: |
40-
readme.md
41-
4211
autofix:
43-
uses: kdeldycke/workflows/.github/workflows/autofix.yaml@v4.25.5
44-
# Depends on the previous job so that the Markdown syntax auto-fixer can have an effect on auto-updated content.
45-
needs: update-readme
12+
uses: kdeldycke/repomatic/.github/workflows/autofix.yaml@v6.5.0
13+
secrets:
14+
WORKFLOW_UPDATE_GITHUB_PAT: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT }}

docs/docs_update.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
2+
#
3+
# This program is Free Software; you can redistribute it and/or
4+
# modify it under the terms of the GNU General Public License
5+
# as published by the Free Software Foundation; either version 2
6+
# of the License, or (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program; if not, write to the Free Software
15+
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16+
"""Dynamic documentation content generation.
17+
18+
Called by repomatic's ``update-docs`` job to regenerate tables, diagrams and
19+
other dynamic content in ``readme.md``.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from pathlib import Path
25+
from textwrap import dedent
26+
27+
from click_extra.table import TableFormat, render_table
28+
from extra_platforms import Group, extract_members
29+
30+
from meta_package_manager.base import Operations
31+
from meta_package_manager.inventory import MAIN_PLATFORMS
32+
from meta_package_manager.pool import pool
33+
34+
35+
def managers_sankey() -> str:
36+
"""Produce a sankey diagram to map ``mpm`` to all its supported managers."""
37+
table = []
38+
for mid, m in sorted(pool.items()):
39+
line = f"Meta Package Manager,{mid},1"
40+
table.append(line)
41+
42+
output = dedent("""\
43+
```mermaid
44+
---
45+
config: {"sankey": {"showValues": false, "width": 800, "height": 400}}
46+
---
47+
sankey-beta\n
48+
""")
49+
output += "\n".join(table)
50+
output += "\n```"
51+
return output
52+
53+
54+
def operation_matrix() -> tuple[str, str]:
55+
"""Produce a table of managers' metadata and supported operations."""
56+
# Build up the column titles.
57+
headers = [
58+
"Package manager",
59+
"Min. version",
60+
]
61+
62+
# Footnotes are used to details the OSes covered by each platform group.
63+
footnotes = []
64+
65+
for p_obj in MAIN_PLATFORMS:
66+
header_title = p_obj.name
67+
# Add footnote for groups with more than one platform.
68+
if isinstance(p_obj, Group) and len(p_obj) > 1:
69+
footnote_tag = f"[^{p_obj.id}]"
70+
header_title += footnote_tag
71+
platforms_string = ", ".join(
72+
sorted(
73+
(
74+
p.name
75+
for p in p_obj.members.values() # type: ignore[attr-defined]
76+
),
77+
key=str.casefold,
78+
),
79+
)
80+
footnotes.append(f"{footnote_tag}: {p_obj.name}: {platforms_string}.")
81+
headers.append(header_title)
82+
83+
headers.extend(f"`{op.name}`" for op in Operations)
84+
85+
table = []
86+
for mid, m in sorted(pool.items()):
87+
line = [
88+
f"[`{mid}`]({m.homepage_url})"
89+
+ ("" if not m.deprecated else f" [⚠️]({m.deprecation_url})"),
90+
f"{m.requirement}",
91+
]
92+
for p_obj in MAIN_PLATFORMS:
93+
line.append(
94+
p_obj.icon if m.platforms.issuperset(extract_members(p_obj)) else ""
95+
)
96+
for op in Operations:
97+
line.append("✓" if m.implements(op) else "")
98+
table.append(line)
99+
100+
# Set each column alignment.
101+
alignments = ["left", "left"]
102+
alignments.extend(["center"] * len(MAIN_PLATFORMS))
103+
alignments.extend(["center"] * len(Operations))
104+
105+
rendered_table = render_table(
106+
table,
107+
headers=headers,
108+
table_format=TableFormat.GITHUB,
109+
colalign=alignments,
110+
disable_numparse=True,
111+
)
112+
113+
return rendered_table, "\n\n".join(footnotes)
114+
115+
116+
def replace_content(
117+
filepath: Path,
118+
new_content: str,
119+
start_tag: str,
120+
end_tag: str | None = None,
121+
) -> None:
122+
"""Replace in a file the content surrounded by the provided start and end tags.
123+
124+
If no end tag is provided, the whole content found after the start tag will be
125+
replaced.
126+
"""
127+
filepath = filepath.resolve()
128+
assert filepath.exists(), f"File {filepath} does not exist."
129+
assert filepath.is_file(), f"File {filepath} is not a file."
130+
131+
orig_content = filepath.read_text()
132+
133+
assert start_tag, "Start tag must be empty."
134+
assert start_tag in orig_content, f"Start tag {start_tag!r} not found in content."
135+
pre_content, table_start = orig_content.split(start_tag, 1)
136+
137+
if end_tag:
138+
_, post_content = table_start.split(end_tag, 1)
139+
else:
140+
end_tag = ""
141+
post_content = ""
142+
143+
filepath.write_text(
144+
f"{pre_content}{start_tag}{new_content}{end_tag}{post_content}",
145+
)
146+
147+
148+
def update_readme() -> None:
149+
"""Update ``readme.md`` with implementation table for each manager we support."""
150+
readme = Path(__file__).parent.parent.joinpath("readme.md")
151+
152+
replace_content(
153+
readme,
154+
managers_sankey(),
155+
"<!-- managers-sankey-start -->\n\n",
156+
"\n\n<!-- managers-sankey-end -->",
157+
)
158+
159+
matrix, footnotes = operation_matrix()
160+
replace_content(
161+
readme,
162+
matrix,
163+
"<!-- operation-matrix-start -->\n\n",
164+
"\n\n<!-- operation-matrix-end -->",
165+
)
166+
replace_content(
167+
readme,
168+
footnotes,
169+
"<!-- operation-footnotes-start -->\n\n",
170+
# mdformat-footnote is stripping all HTML comments after footnotes:
171+
# https://github.com/executablebooks/mdformat-footnote/issues/11
172+
# So we protect the content to be replaced with an end tag that we put at the
173+
# tail of the last footnote line, without any carriage return.
174+
"<!-- operation-footnotes-end -->\n",
175+
)
176+
177+
178+
if __name__ == "__main__":
179+
update_readme()

0 commit comments

Comments
 (0)