-
Notifications
You must be signed in to change notification settings - Fork 417
Expand file tree
/
Copy pathsync_env_docs.py
More file actions
351 lines (290 loc) · 12.6 KB
/
Copy pathsync_env_docs.py
File metadata and controls
351 lines (290 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/env python3
"""Sync environment documentation with the envs/ directory.
Ensures every environment in envs/ with a README.md has a corresponding
doc stub in docs/source/environments/<slug>.md, and that existing stubs
stay in sync with their source README.
Also detects orphaned stubs that reference envs which no longer exist,
and stubs that are not listed in docs/source/_toctree.yml (sidebar) or
docs/source/environments.md (HTML catalog).
Modes:
--check : Exit non-zero if out of sync (for CI)
--fix : Auto-create missing stubs, refresh stale ones, delete orphans
--dry-run : Preview what --fix would do without writing anything
Note: entries in docs/source/environments.md (HTML catalog) and
docs/source/_toctree.yml are managed manually. This script only
writes the per-environment stub files; missing toctree/catalog
entries are reported so they can be added by hand.
"""
import argparse
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ENVS_DIR = os.path.join(ROOT, "envs")
DOCS_ENVS_DIR = os.path.join(ROOT, "docs", "source", "environments")
TOCTREE_PATH = os.path.join(ROOT, "docs", "source", "_toctree.yml")
CATALOG_PATH = os.path.join(ROOT, "docs", "source", "environments.md")
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/huggingface/OpenEnv/main"
SKIP_DIRS = {"README.md"}
# ---------------------------------------------------------------------------
# Discovery helpers
# ---------------------------------------------------------------------------
def get_env_dirs():
"""Return sorted list of environment directory names under envs/."""
return sorted(
d
for d in os.listdir(ENVS_DIR)
if os.path.isdir(os.path.join(ENVS_DIR, d)) and d not in SKIP_DIRS
)
def get_existing_stub_mapping():
"""Build slug → env_dir mapping by reading stubs in docs/source/environments/.
Supports two formats:
- New: ``<!-- openenv-source: env_dir -->`` comment at top of file
- Legacy: ``{include} ../../../envs/<env_dir>/README.md`` directive
"""
mapping = {}
for fname in os.listdir(DOCS_ENVS_DIR):
if not fname.endswith(".md"):
continue
slug = fname[:-3]
stub_path = os.path.join(DOCS_ENVS_DIR, fname)
with open(stub_path) as f:
content = f.read()
# New format: <!-- openenv-source: env_dir -->
match = re.search(r"<!--\s*openenv-source:\s*(\S+)\s*-->", content)
if match:
mapping[slug] = match.group(1)
continue
# Legacy format: {include}
match = re.search(
r"\{include\}\s+\.\./\.\./\.\./envs/([^/]+)/README\.md", content
)
if match:
mapping[slug] = match.group(1)
return mapping
def env_dir_to_slug(env_dir):
"""Convert an env directory name to a doc slug (best-effort default)."""
slug = env_dir
if slug.endswith("_env"):
slug = slug[:-4]
return slug
# ---------------------------------------------------------------------------
# README helpers
# ---------------------------------------------------------------------------
def _strip_frontmatter(text):
"""Remove YAML frontmatter (--- ... ---) from the start of text."""
if text.startswith("---"):
try:
end = text.index("---", 3)
return text[end + 3 :].lstrip("\n")
except ValueError:
pass
return text
def generate_stub(env_dir):
"""Return the doc stub content for an environment.
Inlines the README content (without HF Spaces YAML frontmatter) and
prepends a ``<!-- openenv-source: env_dir -->`` comment so the script
can identify the source env when re-reading the file later.
"""
readme_path = os.path.join(ENVS_DIR, env_dir, "README.md")
with open(readme_path) as f:
content = f.read()
content = _strip_frontmatter(content)
# Rewrite relative assets/ paths to absolute GitHub raw URLs so images
# render correctly when the README is inlined into the doc-builder site.
base_url = f"{GITHUB_RAW_BASE}/envs/{env_dir}"
content = re.sub(r'(src=["\'])assets/', rf'\1{base_url}/assets/', content)
return f"<!-- openenv-source: {env_dir} -->\n{content}"
# ---------------------------------------------------------------------------
# Analysis
# ---------------------------------------------------------------------------
def analyze(env_dirs, stub_mapping):
"""Return (missing, orphaned, stale, no_readme) lists."""
reverse_map = {v: k for k, v in stub_mapping.items()}
documented_env_dirs = set(stub_mapping.values())
missing = []
stale = []
no_readme = []
for env_dir in env_dirs:
readme = os.path.join(ENVS_DIR, env_dir, "README.md")
if not os.path.exists(readme):
no_readme.append(env_dir)
continue
if env_dir in documented_env_dirs:
slug = reverse_map[env_dir]
# Check if existing stub is stale (content drifted from README)
stub_path = os.path.join(DOCS_ENVS_DIR, f"{slug}.md")
if os.path.exists(stub_path):
expected = generate_stub(env_dir)
with open(stub_path) as f:
actual = f.read()
if actual != expected:
stale.append((env_dir, slug))
else:
slug = env_dir_to_slug(env_dir)
missing.append((env_dir, slug))
orphaned = []
env_dir_set = set(env_dirs)
for slug, env_dir in stub_mapping.items():
if env_dir not in env_dir_set:
orphaned.append((env_dir, slug))
return missing, orphaned, stale, no_readme
def find_unlisted(stub_mapping, orphaned):
"""Return (unlisted_toctree, unlisted_catalog) slug lists.
A stub is unlisted when docs/source/_toctree.yml has no
``- local: environments/<slug>`` entry (the page is unreachable from the
sidebar) or docs/source/environments.md has no ``environments/<slug>``
link (the env has no card in the catalog). Both files are managed
manually, so these are reported rather than auto-fixed. Orphaned stubs
are skipped: the fix there is deleting the stub, not listing it.
"""
with open(TOCTREE_PATH) as f:
toctree = f.read()
with open(CATALOG_PATH) as f:
catalog = f.read()
orphaned_slugs = {slug for _, slug in orphaned}
unlisted_toctree = []
unlisted_catalog = []
for slug in sorted(stub_mapping):
if slug in orphaned_slugs:
continue
if not re.search(
rf"^\s*-\s*local:\s*environments/{re.escape(slug)}\s*$", toctree, re.M
):
unlisted_toctree.append(slug)
if not re.search(rf"environments/{re.escape(slug)}\b", catalog):
unlisted_catalog.append(slug)
return unlisted_toctree, unlisted_catalog
# ---------------------------------------------------------------------------
# Reporting and fixing
# ---------------------------------------------------------------------------
def run_check(missing, orphaned, stale, no_readme, unlisted_toctree, unlisted_catalog):
ok = True
if no_readme:
print(
"⚠️ The following environments have no README.md and will not appear on the docs site:\n"
)
for env_dir in no_readme:
print(f" envs/{env_dir}/")
print()
print(" This is a warning only — it will not block your PR.\n")
if missing:
ok = False
print("❌ Missing stubs for the following environments:\n")
for env_dir, slug in missing:
print(f" envs/{env_dir}/ → docs/source/environments/{slug}.md")
print()
print(" Run: python scripts/sync_env_docs.py --fix\n")
if stale:
ok = False
print("⚠️ The following stubs are out of date with their source README:\n")
for env_dir, slug in stale:
print(f" docs/source/environments/{slug}.md ← envs/{env_dir}/README.md")
print()
print(" Run: python scripts/sync_env_docs.py --fix\n")
if orphaned:
ok = False
print("⚠️ Orphaned stubs (env directory no longer exists):\n")
for env_dir, slug in orphaned:
print(f" docs/source/environments/{slug}.md (was envs/{env_dir}/)")
print()
print(" Run: python scripts/sync_env_docs.py --fix\n")
if unlisted_toctree:
ok = False
print("❌ Stubs missing from the docs sidebar (docs/source/_toctree.yml):\n")
for slug in unlisted_toctree:
print(f" docs/source/environments/{slug}.md")
print()
print(
" Add an entry to the Environments section of docs/source/_toctree.yml:\n"
" - local: environments/<slug>\n"
" title: <Env Name>\n"
)
if unlisted_catalog:
ok = False
print("❌ Stubs missing from the HTML catalog (docs/source/environments.md):\n")
for slug in unlisted_catalog:
print(f" docs/source/environments/{slug}.md")
print()
print(
" Add a card to docs/source/environments.md (copy an existing\n"
' <div class="border ..."> card and link to environments/<slug>).\n'
)
if ok:
print("✅ All environment stubs are present and up to date.")
return 0 if ok else 1
def run_fix(missing, orphaned, stale, dry_run=False):
def label(action):
return f"[dry-run] Would {action}" if dry_run else action
for env_dir, slug in missing:
stub_path = os.path.join(DOCS_ENVS_DIR, f"{slug}.md")
if dry_run:
print(f" {label('create')} {os.path.relpath(stub_path, ROOT)}")
else:
with open(stub_path, "w") as f:
f.write(generate_stub(env_dir))
print(f" ✅ Created {os.path.relpath(stub_path, ROOT)}")
for env_dir, slug in stale:
stub_path = os.path.join(DOCS_ENVS_DIR, f"{slug}.md")
if dry_run:
print(f" {label('refresh')} {os.path.relpath(stub_path, ROOT)}")
else:
with open(stub_path, "w") as f:
f.write(generate_stub(env_dir))
print(f" 🔄 Refreshed {os.path.relpath(stub_path, ROOT)}")
for env_dir, slug in orphaned:
stub_path = os.path.join(DOCS_ENVS_DIR, f"{slug}.md")
if os.path.exists(stub_path):
if dry_run:
print(f" {label('delete')} {os.path.relpath(stub_path, ROOT)}")
else:
os.remove(stub_path)
print(f" 🗑️ Deleted {os.path.relpath(stub_path, ROOT)}")
def main():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--check", action="store_true", help="Check sync status (CI mode)")
group.add_argument("--fix", action="store_true", help="Fix missing, stale, and orphaned stubs")
group.add_argument("--dry-run", action="store_true", help="Preview --fix without writing")
args = parser.parse_args()
env_dirs = get_env_dirs()
stub_mapping = get_existing_stub_mapping()
missing, orphaned, stale, no_readme = analyze(env_dirs, stub_mapping)
unlisted_toctree, unlisted_catalog = find_unlisted(stub_mapping, orphaned)
if args.check:
sys.exit(
run_check(
missing, orphaned, stale, no_readme, unlisted_toctree, unlisted_catalog
)
)
# --fix and --dry-run
if no_readme:
print("⚠️ Environments without README.md (skipped):\n")
for env_dir in no_readme:
print(f" envs/{env_dir}/")
print()
if not missing and not orphaned and not stale:
print("✅ All stubs are already in sync.")
else:
print(
"Fixing documentation...\n"
if not args.dry_run
else "Dry run — no files will be modified:\n"
)
run_fix(missing, orphaned, stale, dry_run=args.dry_run)
# Toctree/catalog entries are managed manually; remind about gaps that
# --fix cannot write (including stubs it just created).
manual_toctree = sorted(set(unlisted_toctree) | {slug for _, slug in missing})
manual_catalog = sorted(set(unlisted_catalog) | {slug for _, slug in missing})
if manual_toctree:
print("\n⚠️ Add these to docs/source/_toctree.yml manually (- local: environments/<slug>):")
for slug in manual_toctree:
print(f" {slug}")
if manual_catalog:
print("\n⚠️ Add a card for these to docs/source/environments.md manually:")
for slug in manual_catalog:
print(f" {slug}")
if __name__ == "__main__":
main()