Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
keras-team
GitHub Repository: keras-team/keras-io
Path: blob/master/scripts/autogen.py
7808 views
1
"""Documentation generator for Keras.io
2
3
USAGE:
4
5
python autogen.py make
6
python autogen.py serve
7
"""
8
9
import shutil
10
import copy
11
import json
12
import re
13
import os
14
import sys
15
from pathlib import Path
16
import http.server
17
import socketserver
18
import signal
19
import docstrings
20
import jinja2
21
import multiprocessing
22
import warnings
23
24
import autogen_utils
25
from master import MASTER
26
from examples_master import EXAMPLES_MASTER
27
import tutobooks
28
import generate_tf_guides
29
import render_presets
30
31
EXAMPLES_GH_LOCATION = Path("keras-team") / "keras-io" / "blob" / "master" / "examples"
32
GUIDES_GH_LOCATION = Path("keras-team") / "keras-io" / "blob" / "master" / "guides"
33
KERAS_TEAM_GH = "https://github.com/keras-team"
34
PROJECT_URL = {
35
"keras": f"{KERAS_TEAM_GH}/keras/tree/v3.13.2/",
36
"keras_tuner": f"{KERAS_TEAM_GH}/keras-tuner/tree/v1.4.8/",
37
"keras_hub": f"{KERAS_TEAM_GH}/keras-hub/tree/v0.26.0/",
38
"tf_keras": f"{KERAS_TEAM_GH}/tf-keras/tree/v2.19.0/",
39
"keras_rs": f"{KERAS_TEAM_GH}/keras-rs/tree/v0.4.0/",
40
}
41
USE_MULTIPROCESSING = False
42
43
44
class KerasIO:
45
def __init__(
46
self,
47
master,
48
url,
49
templates_dir,
50
md_sources_dir,
51
site_dir,
52
theme_dir,
53
guides_dir,
54
examples_dir,
55
redirects_dir,
56
refresh_guides=False,
57
refresh_examples=False,
58
):
59
self.master = master
60
self.url = url
61
self.templates_dir = templates_dir
62
self.md_sources_dir = md_sources_dir
63
self.site_dir = site_dir
64
self.theme_dir = theme_dir
65
self.guides_dir = guides_dir
66
self.examples_dir = examples_dir
67
self.redirects_dir = redirects_dir
68
self.refresh_guides = refresh_guides
69
self.refresh_examples = refresh_examples
70
71
self.make_examples_master()
72
self.nav = self.make_nav_index()
73
self.docstring_printer = docstrings.KerasDocumentationGenerator(PROJECT_URL)
74
75
def make_examples_master(self):
76
for entry in self.master["children"]:
77
if entry["path"] == "examples/":
78
examples_entry = entry
79
break
80
for entry in examples_entry["children"]: # e.g. {"path": "nlp", ...}
81
children = entry.get("children", [])
82
preexisting = [e["path"] for e in children]
83
subdir = entry["path"] # e.g. nlp
84
path = Path(self.examples_dir) / subdir # e.g. examples/nlp
85
for fname in sorted(os.listdir(path)):
86
if fname.endswith(".py"): # e.g. examples/nlp/test.py
87
name = fname[:-3]
88
example_path = name.split("/")[-1]
89
if example_path not in preexisting:
90
f = open(path / fname, encoding="utf-8")
91
f.readline()
92
title_line = f.readline()
93
f.close()
94
assert title_line.startswith("Title: ")
95
title = title_line[len("Title: ") :]
96
children.append({"path": example_path, "title": title.strip()})
97
entry["children"] = children
98
99
def make_md_sources(self):
100
print("Generating md sources")
101
if os.path.exists(self.md_sources_dir):
102
print("Clearing", self.md_sources_dir)
103
shutil.rmtree(self.md_sources_dir)
104
os.makedirs(self.md_sources_dir)
105
106
self.make_tutobook_sources(
107
guides=self.refresh_guides, examples=self.refresh_examples
108
)
109
self.sync_tutobook_templates()
110
111
# Find all symbols for all guides and examples in MASTER
112
self.find_apis_in_tutobook(MASTER, [])
113
114
# Recursively generate all md sources based on the MASTER tree
115
self.make_md_source_for_entry(self.master, path_stack=[], title_stack=[])
116
117
def find_apis_in_tutobook(self, entry, path_stack):
118
path = entry["path"]
119
if "children" in entry:
120
# This is a TOC, recurse in tree
121
sub_path_stack = path_stack + [path] if path != "/" else path_stack
122
for child in entry["children"]:
123
self.find_apis_in_tutobook(child, sub_path_stack)
124
return
125
elif "generate" in entry:
126
# This is an API, ignore
127
return
128
129
# This is a guide / example, reverse engineer the path to the Python.
130
url = "/" + str(Path(*path_stack) / path) + "/"
131
is_guide = False
132
if path_stack[0] == "examples/":
133
py_path_stack = [self.examples_dir] + path_stack[1:]
134
elif path_stack[0] == "guides/":
135
py_path_stack = [self.guides_dir] + path_stack[1:]
136
is_guide = True
137
elif len(path_stack) < 2:
138
return
139
elif path_stack[1] == "examples/":
140
py_path_stack = [self.examples_dir, path_stack[0]] + path_stack[2:]
141
elif path_stack[1] == "guides/":
142
py_path_stack = [self.guides_dir, path_stack[0]] + path_stack[2:]
143
is_guide = True
144
else:
145
return
146
147
py_path = os.path.join(*py_path_stack, path + ".py")
148
149
# Parse the Python file and extract all the symbols from Keras packages.
150
apis = autogen_utils.find_all_symbols(
151
py_path, ["keras", "keras_hub", "keras_rs"]
152
)
153
if apis is None:
154
warnings.warn(f"Could not parse '{py_path}'")
155
else:
156
# Provide the results to the docstring_printer.
157
self.docstring_printer.add_example_apis(url, entry["title"], is_guide, apis)
158
159
def preprocess_tutobook_md_source(
160
self, md_content, fname, github_repo_dir, img_dir, site_img_dir
161
):
162
# Insert colab button and github button.
163
name = fname[:-3]
164
md_content_lines = md_content.split("\n")
165
button_lines = [
166
"\n",
167
'<img class="k-inline-icon" src="https://colab.research.google.com/img/colab_favicon.ico"/> '
168
"[**View in Colab**](https://colab.research.google.com/github/"
169
+ github_repo_dir
170
+ "/ipynb/"
171
+ name
172
+ ".ipynb"
173
+ ") "
174
'<span class="k-dot">•</span>'
175
'<img class="k-inline-icon" src="https://github.com/favicon.ico"/> '
176
"[**GitHub source**](https://github.com/"
177
+ github_repo_dir
178
+ "/"
179
+ fname
180
+ ")",
181
"\n",
182
]
183
md_content_lines = md_content_lines[:6] + button_lines + md_content_lines[6:]
184
md_content = "\n".join(md_content_lines)
185
# Normalize img urls
186
md_content = md_content.replace(
187
str(img_dir) + "/" + name, self.url + site_img_dir
188
)
189
# Insert --- before H2 titles
190
md_content = md_content.replace("\n## ", "\n---\n## ")
191
return md_content
192
193
def make_tutobook_sources_for_directory(
194
self, src_dir, target_dir, img_dir, site_img_dir, github_repo_dir
195
):
196
# e.g.
197
# make_tutobook_sources_for_directory(
198
# "examples/nlp", "examples/nlp/md", "examples/nlp/img", "img/examples/nlp")
199
print("Making tutobook sources for", src_dir)
200
201
working_ipynb_dir = Path(src_dir) / "ipynb"
202
if not os.path.exists(working_ipynb_dir):
203
os.makedirs(working_ipynb_dir)
204
205
for fname in os.listdir(src_dir):
206
if fname.endswith(".py"):
207
print("...Processing", fname)
208
name = fname[:-3]
209
py_path = Path(src_dir) / fname
210
nb_path = working_ipynb_dir / (name + ".ipynb")
211
md_path = Path(target_dir) / (name + ".md")
212
tutobooks.py_to_md(py_path, nb_path, md_path, img_dir)
213
md_content = open(md_path).read()
214
md_content = self.preprocess_tutobook_md_source(
215
md_content, fname, github_repo_dir, img_dir, site_img_dir
216
)
217
open(md_path, "w").write(md_content)
218
shutil.rmtree(working_ipynb_dir)
219
220
def make_tutobook_ipynbs(self):
221
def process_one_dir(src_dir, target_dir):
222
if os.path.exists(target_dir):
223
print("Clearing", target_dir)
224
shutil.rmtree(target_dir)
225
os.makedirs(target_dir)
226
for fname in os.listdir(src_dir):
227
if fname.endswith(".py"):
228
print("...Processing", fname)
229
name = fname[:-3]
230
py_path = Path(src_dir) / fname
231
nb_path = target_dir / (name + ".ipynb")
232
tutobooks.py_to_nb(py_path, nb_path, fill_outputs=False)
233
234
# Guides
235
guides_dir = Path(self.guides_dir)
236
ipynb_dir = guides_dir / "ipynb"
237
process_one_dir(guides_dir, ipynb_dir)
238
239
# Examples
240
for name in os.listdir(self.examples_dir):
241
path = Path(self.examples_dir) / name
242
if os.path.isdir(path):
243
ipynb_dir = path / "ipynb"
244
process_one_dir(path, ipynb_dir)
245
246
def add_example(self, path, working_dir=None):
247
"""e.g. add_example('vision/cats_and_dogs')"""
248
249
# Prune out the ../ path
250
if path.startswith("../examples/"):
251
path = path.replace("../examples/", "")
252
253
folder, name = path.split(os.path.sep)
254
assert path.count(os.path.sep) == 1
255
if name.endswith(".py"):
256
name = name[:-3]
257
258
ipynb_dir = Path(self.examples_dir) / folder / "ipynb"
259
if not os.path.exists(ipynb_dir):
260
os.makedirs(ipynb_dir)
261
262
md_dir = Path(self.examples_dir) / folder / "md"
263
if not os.path.exists(md_dir):
264
os.makedirs(md_dir)
265
266
img_dir = Path(self.examples_dir) / folder / "img"
267
if not os.path.exists(img_dir):
268
os.makedirs(img_dir)
269
270
py_path = Path(self.examples_dir) / folder / (name + ".py")
271
md_path = md_dir / (name + ".md")
272
nb_path = ipynb_dir / (name + ".ipynb")
273
274
self.disable_warnings()
275
tutobooks.py_to_nb(py_path, nb_path, fill_outputs=False)
276
tutobooks.py_to_md(py_path, nb_path, md_path, img_dir, working_dir=working_dir)
277
278
md_content = open(md_path).read()
279
github_repo_dir = str(EXAMPLES_GH_LOCATION / folder)
280
site_img_dir = os.path.join("img", "examples", folder, name)
281
md_content = self.preprocess_tutobook_md_source(
282
md_content, name + ".py", github_repo_dir, img_dir, site_img_dir
283
)
284
open(md_path, "w").write(md_content)
285
286
def add_guide(self, name, working_dir=None):
287
"""e.g. add_guide('functional_api')"""
288
289
# Prune out the ../ path
290
if name.startswith("../guides/"):
291
name = name.replace("../guides/", "")
292
293
if name.endswith(".py"):
294
name = name[:-3]
295
ipynb_dir = Path(self.guides_dir) / "ipynb"
296
if not os.path.exists(ipynb_dir):
297
os.makedirs(ipynb_dir)
298
299
md_dir = Path(self.guides_dir) / "md"
300
if not os.path.exists(md_dir):
301
os.makedirs(md_dir)
302
303
img_dir = Path(self.guides_dir) / "img"
304
if not os.path.exists(img_dir):
305
os.makedirs(img_dir)
306
307
py_path = Path(self.guides_dir) / (name + ".py")
308
md_path = md_dir / (name + ".md")
309
nb_path = ipynb_dir / (name + ".ipynb")
310
311
self.disable_warnings()
312
tutobooks.py_to_nb(py_path, nb_path, fill_outputs=False)
313
tutobooks.py_to_md(py_path, nb_path, md_path, img_dir, working_dir=working_dir)
314
315
md_content = open(md_path).read()
316
md_content = md_content.replace("../guides/img/", "/img/guides/")
317
github_repo_dir = str(GUIDES_GH_LOCATION)
318
site_img_dir = "img/guides/" + name
319
md_content = self.preprocess_tutobook_md_source(
320
md_content, name + ".py", github_repo_dir, img_dir, site_img_dir
321
)
322
open(md_path, "w").write(md_content)
323
324
@staticmethod
325
def disable_warnings():
326
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
327
os.environ["AUTOGRAPH_VERBOSITY"] = "0"
328
329
def make_tutobook_sources(self, guides=True, examples=True):
330
"""Populate `examples/nlp/md`, `examples/nlp/img/`, etc.
331
332
- guides/md/ & /png/
333
- examples/nlp/md/ & /png/
334
- examples/computer_vision/md/ & /png/
335
- examples/structured_data/md/ & /png/
336
- examples/timeseries/md/ & /png/
337
- examples/generative_dl/md/ & /png/
338
- examples/keras_recipes/md/ & /png/
339
"""
340
# Guides
341
if guides:
342
target_dir = Path(self.guides_dir) / "md"
343
img_dir = Path(self.guides_dir) / "img"
344
if os.path.exists(target_dir):
345
shutil.rmtree(target_dir)
346
if os.path.exists(img_dir):
347
shutil.rmtree(img_dir)
348
os.makedirs(target_dir)
349
os.makedirs(img_dir)
350
self.make_tutobook_sources_for_directory(
351
src_dir=Path(self.guides_dir),
352
target_dir=target_dir,
353
img_dir=img_dir,
354
site_img_dir="img/guides/",
355
github_repo_dir=str(GUIDES_GH_LOCATION),
356
)
357
358
# Examples
359
if examples:
360
for name in os.listdir(self.examples_dir):
361
path = Path(self.examples_dir) / name
362
if os.path.isdir(path):
363
target_dir = path / "md"
364
img_dir = path / "img"
365
if os.path.exists(target_dir):
366
shutil.rmtree(target_dir)
367
if os.path.exists(img_dir):
368
shutil.rmtree(img_dir)
369
os.makedirs(target_dir)
370
os.makedirs(img_dir)
371
self.make_tutobook_sources_for_directory(
372
src_dir=path, # e.g. examples/nlp
373
target_dir=target_dir, # e.g. examples/nlp/md
374
img_dir=img_dir, # e.g. examples/nlp/img
375
site_img_dir="img/examples/" + name, # e.g. img/examples/nlp
376
github_repo_dir=str(EXAMPLES_GH_LOCATION / name),
377
)
378
379
def sync_tutobook_templates(self):
380
"""Copy generated `.md`s to source_dir.
381
382
Note: intro guides are copied to getting_started.
383
384
guides/md/ -> sources/guides/
385
guides/md/intro_* -> sources/getting_started/
386
examples/*/md/ -> sources/examples/*/
387
"""
388
# Guides
389
copy_inner_contents(
390
Path(self.guides_dir) / "md",
391
Path(self.templates_dir) / "guides",
392
ext=".md",
393
)
394
# Special cases.
395
# - Copy the Keras intro guide to /getting_started/.
396
# - Copy the KerasHub guides to /keras_hub/.
397
# - Copy the KerasTuner guides to /keras_tuner/.
398
templates_path = Path(self.templates_dir)
399
shutil.copyfile(
400
templates_path / "guides" / "intro_to_keras_for_engineers.md",
401
templates_path / "getting_started" / "intro_to_keras_for_engineers.md",
402
)
403
shutil.copyfile(
404
templates_path / "guides" / "keras_hub" / "getting_started.md",
405
templates_path / "keras_hub" / "getting_started.md",
406
)
407
shutil.copyfile(
408
templates_path / "guides" / "keras_tuner" / "getting_started.md",
409
templates_path / "keras_tuner" / "getting_started.md",
410
)
411
shutil.copytree(
412
templates_path / "guides" / "keras_hub",
413
templates_path / "keras_hub" / "guides",
414
dirs_exist_ok=True,
415
)
416
shutil.copytree(
417
templates_path / "guides" / "keras_tuner",
418
templates_path / "keras_tuner" / "guides",
419
dirs_exist_ok=True,
420
)
421
422
# Examples
423
for dir_name in os.listdir(Path(self.examples_dir)):
424
dir_path = Path(self.examples_dir) / dir_name # e.g. examples/nlp
425
if os.path.isdir(dir_path):
426
dst_dir = templates_path / "examples" / dir_name
427
if os.path.exists(dst_dir):
428
shutil.rmtree(dst_dir)
429
os.makedirs(dst_dir)
430
copy_inner_contents(dir_path / "md", dst_dir, ext=".md")
431
432
shutil.copytree(
433
templates_path / "examples" / "keras_rs",
434
templates_path / "keras_rs" / "examples",
435
dirs_exist_ok=True,
436
)
437
438
# Examples touch-up: add Keras version banner to each example
439
example_name_to_version = {}
440
for section in EXAMPLES_MASTER["children"]:
441
section_name = section["path"].replace("/", "")
442
for example in section["children"]:
443
example_name = section_name + "/" + example["path"]
444
if example.get("keras_3"):
445
version = 3
446
else:
447
version = 2
448
example_name_to_version[example_name] = version
449
for section_name in os.listdir(templates_path / "examples"):
450
# e.g. templates/examples/nlp
451
dir_path = templates_path / "examples" / section_name
452
if not os.path.isdir(dir_path):
453
continue
454
for example_fname in os.listdir(dir_path):
455
if example_fname.endswith(".md"):
456
md_path = dir_path / example_fname
457
with open(md_path) as f:
458
md_content = f.read()
459
example_name = (
460
section_name + "/" + example_fname.removesuffix(".md")
461
)
462
version = example_name_to_version.get(example_name, 2)
463
md_content_lines = md_content.split("\n")
464
for i, line in enumerate(md_content_lines):
465
if "View in Colab" in line:
466
md_content_lines.insert(
467
i,
468
f"<div class='example_version_banner keras_{version}'>ⓘ This example uses Keras {version}</div>",
469
)
470
break
471
md_content = "\n".join(md_content_lines) + "\n"
472
with open(md_path, "w") as f:
473
f.write(md_content)
474
475
def sync_tutobook_media(self):
476
"""Copy generated `.png`s to site_dir.
477
478
Note: intro guides are copied to getting_started.
479
480
guides/img/ -> site/img/guides/
481
examples/*/img/ -> site/img/examples/*/
482
"""
483
# Copy images for guide notebooks
484
for name in os.listdir(Path(self.guides_dir) / "img"):
485
path = Path(self.guides_dir) / "img" / name
486
if os.path.isdir(path):
487
shutil.copytree(path, Path(self.site_dir) / "img" / "guides" / name)
488
# Copy images for examples notebooks
489
for dir_name in os.listdir(Path(self.examples_dir)):
490
dir_path = Path(self.examples_dir) / dir_name
491
if os.path.isdir(dir_path):
492
if not os.path.exists(dir_path / "img"):
493
continue # No media was generated for this tutobook.
494
495
dst_dir = Path(self.site_dir) / "img" / "examples" / dir_name
496
if not os.path.exists(dst_dir):
497
os.makedirs(dst_dir)
498
499
for name in os.listdir(dir_path / "img"):
500
path = dir_path / "img" / name
501
if os.path.isdir(path):
502
shutil.copytree(
503
path,
504
Path(self.site_dir) / "img" / "examples" / dir_name / name,
505
)
506
507
def make_nav_index(self):
508
max_depth = 4
509
path_stack = []
510
511
def make_nav_index_for_entry(entry, path_stack, max_depth):
512
if not isinstance(entry, dict):
513
raise ValueError("Incorrectly formatted entry: " f"{entry}")
514
path = entry["path"]
515
if path != "/":
516
path_stack.append(path)
517
url = self.url + str(Path(*path_stack)) + "/"
518
relative_url = "/" + str(Path(*path_stack)) + "/"
519
if len(path_stack) < max_depth:
520
children = [
521
make_nav_index_for_entry(child, path_stack[:], max_depth)
522
for child in entry.get("children", [])
523
]
524
else:
525
children = []
526
return {
527
"title": entry["title"],
528
"relative_url": relative_url,
529
"url": url,
530
"children": children,
531
}
532
533
return [
534
make_nav_index_for_entry(entry, path_stack[:], max_depth)
535
for entry in self.master["children"]
536
]
537
538
def make_md_source_for_entry(self, entry, path_stack, title_stack):
539
path = entry["path"]
540
if path != "/":
541
path_stack.append(path)
542
title_stack.append(entry["title"])
543
print("...Processing", Path(*path_stack))
544
parent_url = self.url + str(Path(*path_stack)) + "/"
545
if path.endswith("/"):
546
dir_path = Path(self.md_sources_dir) / Path(*path_stack)
547
if not os.path.exists(dir_path):
548
os.makedirs(dir_path)
549
550
template_path = Path(self.templates_dir) / Path(*path_stack)
551
if path.endswith("/"):
552
template_path /= "index.md"
553
else:
554
template_path = template_path.with_suffix(".md")
555
556
if os.path.exists(template_path):
557
template_file = open(template_path, encoding="utf8")
558
template = template_file.read()
559
template_file.close()
560
else:
561
template = ""
562
if entry.get("toc"):
563
template += "{{toc}}\n\n"
564
if entry.get("generate"):
565
template += "{{autogenerated}}\n"
566
if not template.startswith("# "):
567
template = "# " + entry["title"] + "\n\n" + template
568
generate = entry.get("generate")
569
children = entry.get("children")
570
if generate:
571
generated_md = ""
572
for element in generate:
573
generated_md += self.docstring_printer.render(element)
574
if "{{autogenerated}}" not in template:
575
raise RuntimeError(
576
"Template found for %s but missing "
577
"{{autogenerated}} tag." % (template_path,)
578
)
579
template = template.replace("{{autogenerated}}", generated_md)
580
if entry.get("toc"):
581
if not children:
582
raise ValueError(
583
f"For template {template_path}, "
584
"a table of contents was requested but "
585
"the entry had no children."
586
)
587
toc = generate_md_toc(children, parent_url)
588
if "{{toc}}" not in template:
589
raise RuntimeError(
590
"Table of contents requested for %s but "
591
"missing {{toc}} tag." % (template_path,)
592
)
593
template = template.replace("{{toc}}", toc)
594
if "keras_hub/" in path_stack:
595
template = render_presets.render_tags(template)
596
source_path = Path(self.md_sources_dir) / Path(*path_stack)
597
if path.endswith("/"):
598
md_source_path = source_path / "index.md"
599
metadata_path = source_path / "index_metadata.json"
600
else:
601
md_source_path = source_path.with_suffix(".md")
602
metadata_path = str(source_path) + "_metadata.json"
603
604
# Save md source file
605
autogen_utils.save_file(md_source_path, template)
606
607
# Save metadata file
608
location_history = []
609
for i in range(len(path_stack)):
610
stripped_path_stack = [s.strip("/") for s in path_stack[: i + 1]]
611
url = self.url + "/".join(stripped_path_stack) + "/"
612
location_history.append(
613
{
614
"url": url,
615
"title": title_stack[i],
616
}
617
)
618
metadata = json.dumps(
619
{
620
"location_history": location_history[:-1],
621
"outline": (
622
autogen_utils.make_outline(template)
623
if entry.get("outline", True)
624
else []
625
),
626
"location": "/"
627
+ "/".join([s.replace("/", "") for s in path_stack])
628
+ "/",
629
"url": parent_url,
630
"title": entry["title"],
631
}
632
)
633
autogen_utils.save_file(metadata_path, metadata)
634
635
if children:
636
for entry in children:
637
self.make_md_source_for_entry(entry, path_stack[:], title_stack[:])
638
639
def make_symbol_to_link_map(self):
640
def recursive_make_map(entry, current_url):
641
current_url /= entry["path"]
642
entry_map = {}
643
if "generate" in entry:
644
for symbol in entry["generate"]:
645
object_ = docstrings.import_object(symbol)
646
object_type = docstrings.get_type(object_)
647
object_name = symbol.split(".")[-1]
648
649
if symbol.startswith("tensorflow.keras."):
650
symbol = symbol.replace("tensorflow.keras.", "keras.")
651
object_name = object_name.lower().replace("_", "")
652
entry_map[symbol] = (
653
str(current_url) + "#" + object_name + "-" + object_type
654
)
655
656
if "children" in entry:
657
for child in entry["children"]:
658
entry_map.update(recursive_make_map(child, current_url))
659
return entry_map
660
661
urls = recursive_make_map(self.master, Path(""))
662
self._symbol_to_link_map = {}
663
for key, value in urls.items():
664
symbol = f"`{key}`"
665
link = f"[{symbol}]({value})"
666
self._symbol_to_link_map[symbol] = link
667
668
def generate_examples_landing_page(self):
669
"""Create the html file /examples/index.html.
670
671
- Load examples information and metadata
672
- Group them by category (e.g. CV) and subcategory (e.g. image classification)
673
- Render a card for each example
674
"""
675
examples_by_category = {}
676
category_names = []
677
category_paths = []
678
for child in self.master["children"]:
679
if child["path"] == "examples/":
680
examples_master = child
681
break
682
683
for category in examples_master["children"]:
684
category_name = category["title"]
685
category_names.append(category_name)
686
category_paths.append(category["path"])
687
examples_by_category[category_name] = category["children"]
688
689
categories_to_render = []
690
for category_name, category_path in zip(category_names, category_paths):
691
examples_by_subcategory = {}
692
subcategory_names = []
693
for example in examples_by_category[category_name]:
694
subcategory_name = example.get("subcategory", "Other")
695
if subcategory_name not in examples_by_subcategory:
696
examples_by_subcategory[subcategory_name] = []
697
subcategory_names.append(subcategory_name)
698
example["path"] = "/examples/" + category_path + example["path"]
699
examples_by_subcategory[subcategory_name].append(example)
700
701
subcategories_to_render = []
702
for subcategory_name in subcategory_names:
703
subcategories_to_render.append(
704
{
705
"title": subcategory_name,
706
"examples": examples_by_subcategory[subcategory_name],
707
}
708
)
709
710
category_dict = {
711
"title": category_name,
712
"path": "/examples/" + category_path,
713
}
714
if len(subcategories_to_render) > 1:
715
category_dict["subcategories"] = subcategories_to_render
716
else:
717
category_dict["examples"] = subcategories_to_render[0]["examples"]
718
categories_to_render.append(category_dict)
719
720
with open(Path(self.templates_dir) / "examples/index.md") as f:
721
md_content = f.read()
722
723
with open(Path(self.md_sources_dir) / "examples/index_metadata.json") as f:
724
metadata = json.loads(f.read())
725
726
examples_template = jinja2.Template(
727
open(Path(self.theme_dir) / "examples.html").read()
728
)
729
html_example_cards = examples_template.render(
730
{"categories": categories_to_render, "legend": True}
731
)
732
733
html_content = autogen_utils.render_markdown_to_html(md_content)
734
html_content = html_content.replace(
735
"<p>{{examples_list}}</p>", html_example_cards
736
)
737
html_content = insert_title_ids_in_html(html_content)
738
739
relative_url = "/examples/"
740
local_nav = [
741
autogen_utils.set_active_flag_in_nav_entry(entry, relative_url)
742
for entry in self.nav
743
]
744
self.render_single_docs_page_from_html(
745
target_path=Path(self.site_dir) / "examples/index.html",
746
title="Code examples",
747
html_content=html_content,
748
location_history=metadata["location_history"],
749
outline=metadata["outline"],
750
local_nav=local_nav,
751
relative_url=relative_url,
752
)
753
754
# Save per-category landing pages
755
for category_name, category_path in zip(category_names, category_paths):
756
with open(
757
Path(self.md_sources_dir)
758
/ "examples"
759
/ category_path
760
/ "index_metadata.json"
761
) as f:
762
metadata = json.loads(f.read())
763
relative_url = f"/examples/{category_path}"
764
local_nav = [
765
autogen_utils.set_active_flag_in_nav_entry(entry, relative_url)
766
for entry in self.nav
767
]
768
to_render = [
769
cat for cat in categories_to_render if cat["title"] == category_name
770
]
771
html_example_cards = examples_template.render(
772
{"categories": to_render, "legend": False}
773
)
774
self.render_single_docs_page_from_html(
775
target_path=Path(self.site_dir)
776
/ "examples"
777
/ category_path
778
/ "index.html",
779
title=category_name,
780
html_content=html_example_cards,
781
location_history=metadata["location_history"],
782
outline=metadata["outline"],
783
local_nav=local_nav,
784
relative_url=relative_url,
785
)
786
787
def render_md_sources_to_html(self):
788
self.make_symbol_to_link_map()
789
print("Rendering md sources to HTML")
790
base_template = jinja2.Template(open(Path(self.theme_dir) / "base.html").read())
791
docs_template = jinja2.Template(open(Path(self.theme_dir) / "docs.html").read())
792
793
all_urls_list = []
794
795
if os.path.exists(self.site_dir):
796
print("Clearing", self.site_dir)
797
shutil.rmtree(self.site_dir)
798
799
if USE_MULTIPROCESSING:
800
for src_location, _, fnames in os.walk(self.md_sources_dir):
801
pool = multiprocessing.Pool(processes=8)
802
workers = [
803
pool.apply_async(
804
self.render_single_file,
805
args=(src_location, fname, self.nav),
806
)
807
for fname in fnames
808
]
809
810
for worker in workers:
811
url = worker.get()
812
if url is not None:
813
all_urls_list.append(url)
814
pool.close()
815
pool.join()
816
else:
817
for src_location, _, fnames in os.walk(self.md_sources_dir):
818
for fname in fnames:
819
print("...Rendering", fname)
820
self.render_single_file(src_location, fname, self.nav)
821
822
# Images & css & js
823
shutil.copytree(Path(self.theme_dir) / "js", Path(self.site_dir) / "js")
824
shutil.copytree(Path(self.theme_dir) / "css", Path(self.site_dir) / "css")
825
shutil.copytree(Path(self.theme_dir) / "img", Path(self.site_dir) / "img")
826
shutil.copytree(Path(self.theme_dir) / "icons", Path(self.site_dir) / "icons")
827
828
# Landing page
829
landing_template = jinja2.Template(
830
open(Path(self.theme_dir) / "landing.html").read()
831
)
832
landing_page = landing_template.render({"base_url": self.url})
833
autogen_utils.save_file(Path(self.site_dir) / "index.html", landing_page)
834
835
# Search page
836
search_main = open(Path(self.theme_dir) / "search.html").read()
837
search_page = base_template.render(
838
{
839
"title": "Search Keras documentation",
840
"nav": self.nav,
841
"base_url": self.url,
842
"main": search_main,
843
}
844
)
845
autogen_utils.save_file(Path(self.site_dir) / "search.html", search_page)
846
847
# 404 page
848
page404 = base_template.render(
849
{
850
"title": "Page not found",
851
"nav": self.nav,
852
"base_url": self.url,
853
"main": docs_template.render(
854
{
855
"title": "404",
856
"content": "<h1>404: Page not found</h1>",
857
"base_url": self.url,
858
}
859
),
860
}
861
)
862
autogen_utils.save_file(Path(self.site_dir) / "404.html", page404)
863
864
# Keras 3 announcement page
865
keras_3_template = jinja2.Template(
866
open(Path(self.theme_dir) / "keras_3.html").read()
867
)
868
md_content = open(
869
Path(self.templates_dir) / "keras_3" / "keras_3_announcement.md"
870
).read()
871
content = autogen_utils.render_markdown_to_html(md_content)
872
keras_core_page = keras_3_template.render(
873
{"base_url": self.url, "content": content}
874
)
875
autogen_utils.save_file(
876
Path(self.site_dir) / "keras_3" / "index.html",
877
keras_core_page,
878
)
879
880
# Favicon
881
shutil.copyfile(
882
Path(self.theme_dir) / "favicon.ico",
883
Path(self.site_dir) / "favicon.ico",
884
)
885
886
# Tutobooks
887
self.sync_tutobook_media()
888
sitemap = "\n".join(all_urls_list) + "\n"
889
autogen_utils.save_file(Path(self.site_dir) / "sitemap.txt", sitemap)
890
891
# Examples landing page
892
self.generate_examples_landing_page()
893
894
# Redirects
895
self.check_redirects()
896
shutil.copytree(self.redirects_dir, self.site_dir, dirs_exist_ok=True)
897
898
def check_redirects(self):
899
"""Validate our redirects"""
900
for file in Path(self.redirects_dir).glob("**/*.html"):
901
with open(file) as f:
902
content = f.read()
903
# Read url.
904
url = content[content.find("URL=") + 5 :]
905
url = url[: url.find("'")]
906
# Strip to path.
907
path = url.replace("https://keras.io/", "")
908
target = Path(self.site_dir) / Path(path)
909
if not target.exists():
910
raise ValueError(
911
f"Redirect target {path} does not exist referenced "
912
f"from file {file}."
913
)
914
site_path = Path(self.site_dir) / file.relative_to(self.redirects_dir)
915
if site_path.exists():
916
raise ValueError(f"Redirect at {file} would overwrite a real page.")
917
918
def render_single_file(self, src_location, fname, nav):
919
if not fname.endswith(".md"):
920
return
921
922
src_dir = Path(src_location)
923
target_dir = src_location.replace(self.md_sources_dir, self.site_dir)
924
if not os.path.exists(target_dir):
925
try:
926
os.makedirs(target_dir)
927
except FileExistsError:
928
# Might be created by a concurrent process.
929
pass
930
931
# Load metadata for page
932
with open(str(Path(src_location) / fname[:-3]) + "_metadata.json") as f:
933
metadata = json.loads(f.read())
934
935
if fname == "index.md":
936
# Render as index.html
937
target_path = Path(target_dir) / "index.html"
938
relative_url = (str(target_dir) + "/").replace(self.site_dir, "/")
939
relative_url = relative_url.replace("//", "/")
940
else:
941
# Render as fname_no_ext/index.tml
942
fname_no_ext = ".".join(fname.split(".")[:-1])
943
full_target_dir = Path(target_dir) / fname_no_ext
944
os.makedirs(full_target_dir)
945
target_path = full_target_dir / "index.html"
946
relative_url = (str(full_target_dir) + "/").replace(self.site_dir, "/")
947
relative_url = relative_url.replace("//", "/")
948
if not relative_url.endswith("/"):
949
relative_url += "/"
950
951
md_file = open(src_dir / fname, encoding="utf-8")
952
md_content = md_file.read()
953
md_file.close()
954
md_content = replace_links(md_content)
955
956
# Convert Keras symbols to links to the Keras docs
957
for symbol, link in self._symbol_to_link_map.items():
958
md_content = md_content.replace(symbol, link)
959
960
# Convert TF symbols to links to tensorflow.org
961
tmp_content = copy.copy(md_content)
962
replacements = {}
963
while "`tf." in tmp_content:
964
index = tmp_content.find("`tf.")
965
if tmp_content[index - 1] == "[":
966
tmp_content = tmp_content[tmp_content.find("`tf.") + 1 :]
967
tmp_content = tmp_content[tmp_content.find("`") + 1 :]
968
else:
969
tmp_content = tmp_content[tmp_content.find("`tf.") + 1 :]
970
symbol = tmp_content[: tmp_content.find("`")]
971
tmp_content = tmp_content[tmp_content.find("`") + 1 :]
972
if "/" not in symbol and "(" not in symbol:
973
# Check if we're looking at a method on a class
974
symbol_parts = symbol.split(".")
975
if len(symbol_parts) >= 3 and symbol_parts[-2][0].isupper():
976
# In this case the link should look like ".../class#method"
977
path = "/".join(symbol_parts[:-1]) + "#" + symbol_parts[-1]
978
else:
979
# Otherwise just ".../module/class_or_fn"
980
path = symbol.replace(".", "/")
981
path = path.replace("(", "")
982
path = path.replace(")", "")
983
replacements["`" + symbol + "`"] = (
984
"[`"
985
+ symbol
986
+ "`](https://www.tensorflow.org/api_docs/python/"
987
+ path
988
+ ")"
989
)
990
for key, value in replacements.items():
991
md_content = md_content.replace(key, value)
992
993
html_content = autogen_utils.render_markdown_to_html(md_content)
994
html_content = insert_title_ids_in_html(html_content)
995
local_nav = [
996
autogen_utils.set_active_flag_in_nav_entry(entry, relative_url)
997
for entry in nav
998
]
999
title = md_content[2 : md_content.find("\n")]
1000
1001
self.render_single_docs_page_from_html(
1002
target_path,
1003
title,
1004
html_content,
1005
metadata["location_history"],
1006
metadata["outline"],
1007
local_nav,
1008
relative_url,
1009
)
1010
return relative_url
1011
1012
def render_single_docs_page_from_html(
1013
self,
1014
target_path,
1015
title,
1016
html_content,
1017
location_history,
1018
outline,
1019
local_nav,
1020
relative_url,
1021
):
1022
base_template = jinja2.Template(open(Path(self.theme_dir) / "base.html").read())
1023
docs_template = jinja2.Template(open(Path(self.theme_dir) / "docs.html").read())
1024
html_docs = docs_template.render(
1025
{
1026
"title": title,
1027
"content": html_content,
1028
"location_history": location_history,
1029
"base_url": self.url,
1030
"outline": outline,
1031
}
1032
)
1033
html_page = base_template.render(
1034
{
1035
"title": title,
1036
"nav": local_nav,
1037
"base_url": self.url,
1038
"main": html_docs,
1039
"relative_url": relative_url,
1040
}
1041
)
1042
html_page = html_page.replace("../guides/img/", "/img/guides/")
1043
autogen_utils.save_file(target_path, html_page)
1044
1045
def make(self):
1046
self.make_md_sources()
1047
self.render_md_sources_to_html()
1048
self.make_tutobook_ipynbs()
1049
1050
def serve(self):
1051
os.chdir(self.site_dir)
1052
socketserver.ThreadingTCPServer.allow_reuse_address = True
1053
server = socketserver.ThreadingTCPServer(
1054
("", 8000), http.server.SimpleHTTPRequestHandler
1055
)
1056
server.daemon_threads = True
1057
1058
def signal_handler(signal, frame):
1059
try:
1060
if server:
1061
server.server_close()
1062
finally:
1063
sys.exit(0)
1064
1065
signal.signal(signal.SIGINT, signal_handler)
1066
try:
1067
print("Serving on 0.0.0.0:8000")
1068
server.serve_forever()
1069
except KeyboardInterrupt:
1070
pass
1071
finally:
1072
server.server_close()
1073
1074
1075
def replace_links(content):
1076
# Make sure all Keras guides point to keras.io.
1077
for entry in generate_tf_guides.CONFIG:
1078
keras_name = entry["source_name"]
1079
tf_name = entry["target_name"]
1080
content = content.replace(
1081
"https://www.tensorflow.org/guide/keras/" + tf_name,
1082
"https://keras.io/guides/" + keras_name,
1083
)
1084
return content
1085
1086
1087
def strip_markdown_tags(md):
1088
# Strip links
1089
md = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", md)
1090
return md
1091
1092
1093
def copy_inner_contents(src, dst, ext=".md"):
1094
for fname in os.listdir(src):
1095
fpath = Path(src) / fname
1096
fdst = Path(dst) / fname
1097
if fname.endswith(ext):
1098
shutil.copyfile(fpath, fdst)
1099
if os.path.isdir(fpath):
1100
if not os.path.exists(fdst):
1101
os.mkdir(fdst)
1102
copy_inner_contents(fpath, fdst, ext)
1103
1104
1105
def insert_title_ids_in_html(html):
1106
marker = "replace_me_with_id_for:"
1107
marker_end = ":end_of_title"
1108
for i in range(1, 5):
1109
match = "<h" + str(i) + ">(.*?)</h" + str(i) + ">"
1110
replace = (
1111
"<h"
1112
+ str(i)
1113
+ r' id="'
1114
+ marker
1115
+ r"\1"
1116
+ marker_end
1117
+ r'">\1</h'
1118
+ str(i)
1119
+ ">"
1120
)
1121
html = re.sub(match, replace, html)
1122
1123
while 1:
1124
start = html.find(marker)
1125
if start == -1:
1126
break
1127
title = html[start + len(marker) :]
1128
title = title[: title.find(marker_end)]
1129
normalized_title = title
1130
normalized_title = normalized_title.replace("<code>", "")
1131
normalized_title = normalized_title.replace("</code>", "")
1132
if ">" in normalized_title:
1133
normalized_title = normalized_title[normalized_title.find(">") + 1 :]
1134
normalized_title = normalized_title[: normalized_title.find("</")]
1135
normalized_title = autogen_utils.turn_title_into_id(normalized_title)
1136
html = html.replace(marker + title + marker_end, normalized_title)
1137
return html
1138
1139
1140
def generate_md_toc(entries, url, depth=2):
1141
assert url.endswith("/")
1142
entries = [e for e in entries if not e.get("skip_from_toc")]
1143
generated = ""
1144
if set(len(x.get("generate", [])) for x in entries) == {1}:
1145
print_generate = False
1146
else:
1147
print_generate = True
1148
for entry in entries:
1149
title = entry["title"]
1150
path = entry["path"]
1151
if not path.endswith("/"):
1152
path += "/"
1153
full_url = url + path
1154
children = entry.get("children")
1155
generate = entry.get("generate")
1156
if children or (print_generate and generate):
1157
title_prefix = "### "
1158
else:
1159
title_prefix = "- "
1160
generated += title_prefix + "[{title}]({full_url})\n".format(
1161
title=title, full_url=full_url
1162
)
1163
if children:
1164
for child in children:
1165
if child.get("skip_from_toc", False):
1166
continue
1167
child_title = child["title"]
1168
child_path = child["path"]
1169
child_url = full_url + child_path
1170
generated += "- [{child_title}]({child_url})\n".format(
1171
child_title=child_title, child_url=child_url
1172
)
1173
generated += "\n"
1174
elif generate and print_generate:
1175
for gen in generate:
1176
obj = docstrings.import_object(gen)
1177
obj_name = docstrings.get_name(obj)
1178
obj_type = docstrings.get_type(obj)
1179
link = "{full_url}#{obj_name}-{obj_type}".format(
1180
full_url=full_url, obj_name=obj_name, obj_type=obj_type
1181
).lower()
1182
name = gen.split(".")[-1]
1183
generated += "- [{name} {obj_type}]({link})\n".format(
1184
name=name, obj_type=obj_type, link=link
1185
)
1186
generated += "\n"
1187
return generated
1188
1189
1190
def get_working_dir(arg):
1191
if not arg.startswith("--working_dir="):
1192
return None
1193
return arg[len("--working_dir=") :]
1194
1195
1196
if __name__ == "__main__":
1197
root = Path(__file__).parent.parent.resolve()
1198
keras_io = KerasIO(
1199
master=MASTER,
1200
url=os.path.sep,
1201
templates_dir=os.path.join(root, "templates"),
1202
md_sources_dir=os.path.join(root, "sources"),
1203
site_dir=os.path.join(root, "site"),
1204
theme_dir=os.path.join(root, "theme"),
1205
guides_dir=os.path.join(root, "guides"),
1206
examples_dir=os.path.join(root, "examples"),
1207
redirects_dir=os.path.join(root, "redirects"),
1208
refresh_guides=False,
1209
refresh_examples=False,
1210
)
1211
error_msg = (
1212
"Must specify command " "`make`, `serve`, `add_example`, or `add_guide`."
1213
)
1214
if len(sys.argv) < 2:
1215
raise ValueError(error_msg)
1216
cmd = sys.argv[1]
1217
if cmd not in {
1218
"make",
1219
"serve",
1220
"add_example",
1221
"add_guide",
1222
}:
1223
raise ValueError(error_msg)
1224
if cmd in {"add_example", "add_guide"}:
1225
if not len(sys.argv) in (3, 4):
1226
raise ValueError(
1227
"Must specify example/guide to add, e.g. "
1228
"`autogen.py add_example vision/cats_and_dogs`"
1229
)
1230
if cmd == "make":
1231
keras_io.make_md_sources()
1232
keras_io.render_md_sources_to_html()
1233
elif cmd == "serve":
1234
keras_io.serve()
1235
elif cmd == "add_example":
1236
keras_io.add_example(
1237
sys.argv[2],
1238
working_dir=get_working_dir(sys.argv[3]) if len(sys.argv) == 4 else None,
1239
)
1240
elif cmd == "add_guide":
1241
tutobooks.MAX_LOC = 500
1242
keras_io.add_guide(
1243
sys.argv[2],
1244
working_dir=get_working_dir(sys.argv[3]) if len(sys.argv) == 4 else None,
1245
)
1246
1247