|
| 1 | +import argparse |
| 2 | +import os |
| 3 | +import shutil |
| 4 | +import subprocess |
| 5 | +import sys |
| 6 | +import venv |
| 7 | +from dataclasses import dataclass |
| 8 | +from multiprocessing import Pool |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +import mkdocs_get_deps |
| 12 | +import yaml |
| 13 | +from jinja2 import Environment |
| 14 | +from shot_scraper.cli import cli as shot_scraper |
| 15 | +from tqdm import tqdm |
| 16 | + |
| 17 | + |
| 18 | +@dataclass |
| 19 | +class Theme: |
| 20 | + name: str |
| 21 | + mkdocs_id: str |
| 22 | + url: str = "" |
| 23 | + pypi_id: str = "" |
| 24 | + builtin: bool = False |
| 25 | + |
| 26 | + |
| 27 | +_builtin_themes = [ |
| 28 | + Theme(name="MkDocs", mkdocs_id="mkdocs", builtin=True), |
| 29 | + Theme(name="ReadTheDocs", mkdocs_id="readthedocs", builtin=True), |
| 30 | +] |
| 31 | + |
| 32 | + |
| 33 | +# Fetch themes from MkDocs catalog. |
| 34 | +def get_themes() -> list[Theme]: |
| 35 | + with open("projects.yaml") as file: |
| 36 | + catalog = yaml.safe_load(file) |
| 37 | + projects = catalog["projects"] |
| 38 | + theming_category = [project for project in projects if project["category"] == "theming"] |
| 39 | + themes = [] |
| 40 | + for project in theming_category: |
| 41 | + if mkdocs_theme := project.get("mkdocs_theme"): |
| 42 | + if "github_id" in project: |
| 43 | + url = f"https://github.com/{project['github_id']}" |
| 44 | + elif "gitlab_id" in project: |
| 45 | + url = f"https://gitlab.com/{project['gitlab_id']}" |
| 46 | + else: |
| 47 | + url = "" |
| 48 | + pypi_id = project.get("pypi_id", f"git+{url}") |
| 49 | + if isinstance(mkdocs_theme, str): |
| 50 | + themes.append(Theme(name=project["name"], url=url, pypi_id=pypi_id, mkdocs_id=mkdocs_theme)) |
| 51 | + else: |
| 52 | + for theme in mkdocs_theme: |
| 53 | + themes.append( |
| 54 | + Theme(name=f"{project['name']} - {theme.title()}", url=url, pypi_id=pypi_id, mkdocs_id=theme) |
| 55 | + ) |
| 56 | + return _builtin_themes + sorted(themes, key=lambda theme: theme.name.lower()) |
| 57 | + |
| 58 | + |
| 59 | +# Copy files and expand Jinja templates. |
| 60 | +def _prepare_site(src_dir: Path, dest_dir: Path, themes: list[Theme], theme: Theme | None = None) -> None: |
| 61 | + jinja = Environment(autoescape=False) |
| 62 | + dest_dir.mkdir(parents=True, exist_ok=True) |
| 63 | + |
| 64 | + for src_path in src_dir.rglob("*"): |
| 65 | + if not src_path.is_file(): |
| 66 | + continue |
| 67 | + dest_path = dest_dir.joinpath(src_path.relative_to(src_dir)) |
| 68 | + dest_path.parent.mkdir(parents=True, exist_ok=True) |
| 69 | + if src_path.suffix in (".md", ".yml"): |
| 70 | + content = src_path.read_text() |
| 71 | + content = jinja.from_string(content).render(themes=themes, theme=theme) |
| 72 | + dest_path.write_text(content) |
| 73 | + else: |
| 74 | + shutil.copyfile(src_path, dest_path) |
| 75 | + |
| 76 | + |
| 77 | +# Prepare each theme (docs directory and configuration file). |
| 78 | +def prepare_themes(themes: list[Theme]) -> None: |
| 79 | + specimen_dir = Path("templates", "specimen") |
| 80 | + for theme in themes: |
| 81 | + # Copy specific directory, or default to specimen. |
| 82 | + theme_dir = Path("themes", theme.mkdocs_id) |
| 83 | + theme_conf_dir = Path("templates", "themes", theme.mkdocs_id) |
| 84 | + if not theme_conf_dir.exists(): |
| 85 | + theme_conf_dir = specimen_dir |
| 86 | + shutil.copytree(theme_conf_dir, theme_dir, dirs_exist_ok=True) |
| 87 | + |
| 88 | + _prepare_site(specimen_dir, theme_dir, themes, theme=theme) |
| 89 | + |
| 90 | + |
| 91 | +# Prepare the main documentation site. |
| 92 | +def prepare_main(themes: list[Theme]) -> None: |
| 93 | + _prepare_site(Path("templates", "main"), Path("."), themes) |
| 94 | + |
| 95 | + |
| 96 | +# Create virtualenvs and install dependencies. |
| 97 | +def install_deps(theme: Theme) -> None: |
| 98 | + theme_dir = Path("themes", theme.mkdocs_id) |
| 99 | + venv_dir = theme_dir / ".venv" |
| 100 | + if not venv_dir.exists(): |
| 101 | + venv.create(venv_dir, with_pip=True) |
| 102 | + deps = mkdocs_get_deps.get_deps(config_file=theme_dir / "mkdocs.yml") |
| 103 | + subprocess.run( |
| 104 | + [venv_dir / "bin" / "pip", "install", *deps], |
| 105 | + check=False, |
| 106 | + stdout=subprocess.DEVNULL, |
| 107 | + stderr=subprocess.DEVNULL, |
| 108 | + ) |
| 109 | + |
| 110 | + |
| 111 | +# Build theme sites. |
| 112 | +def build_themes(themes: list[Theme]) -> None: |
| 113 | + parser = argparse.ArgumentParser(prog="build_gallery.py") |
| 114 | + parser.add_argument( |
| 115 | + "-D", |
| 116 | + "--no-deps", |
| 117 | + dest="install_deps", |
| 118 | + action="store_false", |
| 119 | + default=True, |
| 120 | + help="Don't install Python dependencies.", |
| 121 | + ) |
| 122 | + parser.add_argument( |
| 123 | + "-T", |
| 124 | + "--no-themes", |
| 125 | + dest="build_themes", |
| 126 | + action="store_false", |
| 127 | + default=True, |
| 128 | + help="Don't rebuild each theme site.", |
| 129 | + ) |
| 130 | + parser.add_argument( |
| 131 | + "-S", |
| 132 | + "--no-shots", |
| 133 | + dest="take_screenshots", |
| 134 | + action="store_false", |
| 135 | + default=True, |
| 136 | + help="Don't take screenshots of each theme.", |
| 137 | + ) |
| 138 | + |
| 139 | + opts = parser.parse_args() |
| 140 | + |
| 141 | + if not opts.install_deps: |
| 142 | + print("Skipping dependencies installation") |
| 143 | + else: |
| 144 | + print("Preparing environments") |
| 145 | + |
| 146 | + with Pool(len(os.sched_getaffinity(0))) as pool: |
| 147 | + tuple(tqdm(pool.imap(install_deps, themes), total=len(themes))) |
| 148 | + |
| 149 | + if not opts.build_themes: |
| 150 | + print("Skipping themes building") |
| 151 | + else: |
| 152 | + logs_dir = Path("logs") |
| 153 | + logs_dir.mkdir(exist_ok=True) |
| 154 | + shutil.rmtree(Path("gallery", "themes"), ignore_errors=True) |
| 155 | + Path("gallery", "themes").mkdir(parents=True) |
| 156 | + |
| 157 | + def _build_theme(theme: Theme) -> None: |
| 158 | + theme_dir = Path("themes", theme.mkdocs_id).absolute() |
| 159 | + dest_dir = Path("gallery", "themes", theme.mkdocs_id).absolute() |
| 160 | + print(f"Building {theme.name}") |
| 161 | + with logs_dir.joinpath(f"{theme.mkdocs_id}.txt").open("w") as logs_file: |
| 162 | + try: |
| 163 | + subprocess.run( |
| 164 | + [theme_dir.joinpath(".venv", "bin", "mkdocs"), "build", "-d", dest_dir], |
| 165 | + stdout=logs_file, |
| 166 | + stderr=logs_file, |
| 167 | + check=True, |
| 168 | + text=True, |
| 169 | + cwd=theme_dir, |
| 170 | + ) |
| 171 | + except subprocess.CalledProcessError: |
| 172 | + print("FAILED!") |
| 173 | + |
| 174 | + for theme in themes: |
| 175 | + _build_theme(theme) |
| 176 | + |
| 177 | + if not opts.take_screenshots: |
| 178 | + print("Skipping screenshots") |
| 179 | + else: |
| 180 | + print("Taking screenshots") |
| 181 | + Path("docs", "assets", "img").mkdir(parents=True, exist_ok=True) |
| 182 | + for theme in tqdm(themes): |
| 183 | + try: |
| 184 | + shot_scraper( |
| 185 | + [f"gallery/themes/{theme.mkdocs_id}/index.html", "-o", f"docs/assets/img/{theme.mkdocs_id}.png"] |
| 186 | + ) |
| 187 | + except SystemExit: |
| 188 | + pass |
| 189 | + except Exception as error: |
| 190 | + print(error) |
| 191 | + |
| 192 | + |
| 193 | +# Build main documentation site. |
| 194 | +def build_main() -> None: |
| 195 | + print("Building gallery's main site") |
| 196 | + subprocess.run([sys.executable, "-mmkdocs", "build", "--dirty"], check=True) |
| 197 | + |
| 198 | + |
| 199 | +# Run everything. |
| 200 | +def main() -> None: |
| 201 | + themes = get_themes() |
| 202 | + prepare_themes(themes) |
| 203 | + prepare_main(themes) |
| 204 | + build_themes(themes) |
| 205 | + build_main() |
| 206 | + |
| 207 | + |
| 208 | +if __name__ == "__main__": |
| 209 | + main() |
0 commit comments