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