From ec6c880b4c8d47b96c37f7c62ef2330308f84b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 2 Dec 2023 12:20:22 +0100 Subject: [PATCH] Migrate Gallery to Catalog Co-authored-by: Oleh Prypin --- .github/workflows/gallery.yml | 61 ++++++++ .gitignore | 7 + Makefile | 10 ++ build_gallery.py | 209 +++++++++++++++++++++++++++ requirements.txt | 7 + templates/main/docs/assets/logo.png | Bin 0 -> 726 bytes templates/main/docs/index.md | 28 ++++ templates/main/mkdocs.yml | 25 ++++ templates/specimen/docs/index.md | 20 +++ templates/specimen/docs/reference.md | 1 + templates/specimen/mkdocs.yml | 54 +++++++ templates/specimen/src/calculator.py | 105 ++++++++++++++ 12 files changed, 527 insertions(+) create mode 100644 .github/workflows/gallery.yml create mode 100644 Makefile create mode 100644 build_gallery.py create mode 100644 requirements.txt create mode 100644 templates/main/docs/assets/logo.png create mode 100644 templates/main/docs/index.md create mode 100644 templates/main/mkdocs.yml create mode 100644 templates/specimen/docs/index.md create mode 100644 templates/specimen/docs/reference.md create mode 100644 templates/specimen/mkdocs.yml create mode 100644 templates/specimen/src/calculator.py diff --git a/.github/workflows/gallery.yml b/.github/workflows/gallery.yml new file mode 100644 index 0000000..feeca81 --- /dev/null +++ b/.github/workflows/gallery.yml @@ -0,0 +1,61 @@ +name: Gallery + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - uses: actions/cache@v3 + name: Cache/restore pip data + with: + path: ~/.cache/pip + key: pip + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Cache/restore Playwright browsers + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright/ + key: playwright + + - name: Install Playwright dependencies + run: shot-scraper install + + - name: Build everything + run: python build_gallery.py + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: gallery + + deploy: + if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch + needs: build + concurrency: + group: "pages" + permissions: + pages: write + id-token: write + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} diff --git a/.gitignore b/.gitignore index 5bee470..468af00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# Specific to this project +/logs/ +/themes/ +/gallery/ +/docs/ +/mkdocs.yml + # IntelliJ target/ .idea/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b2dc71 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: build-gallery +build-gallery: + python -m venv .venv + .venv/bin/pip install -r requirements.txt + .venv/bin/playwright install + .venv/bin/python build_gallery.py + +.PHONY: serve-gallery +serve-gallery: + cd gallery; python -m http.server diff --git a/build_gallery.py b/build_gallery.py new file mode 100644 index 0000000..50e0a7d --- /dev/null +++ b/build_gallery.py @@ -0,0 +1,209 @@ +import argparse +import os +import shutil +import subprocess +import sys +import venv +from dataclasses import dataclass +from multiprocessing import Pool +from pathlib import Path + +import mkdocs_get_deps +import yaml +from jinja2 import Environment +from shot_scraper.cli import cli as shot_scraper +from tqdm import tqdm + + +@dataclass +class Theme: + name: str + mkdocs_id: str + url: str = "" + pypi_id: str = "" + builtin: bool = False + + +_builtin_themes = [ + Theme(name="MkDocs", mkdocs_id="mkdocs", builtin=True), + Theme(name="ReadTheDocs", mkdocs_id="readthedocs", builtin=True), +] + + +# Fetch themes from MkDocs catalog. +def get_themes() -> list[Theme]: + with open("projects.yaml") as file: + catalog = yaml.safe_load(file) + projects = catalog["projects"] + theming_category = [project for project in projects if project["category"] == "theming"] + themes = [] + for project in theming_category: + if mkdocs_theme := project.get("mkdocs_theme"): + if "github_id" in project: + url = f"https://github.com/{project['github_id']}" + elif "gitlab_id" in project: + url = f"https://gitlab.com/{project['gitlab_id']}" + else: + url = "" + pypi_id = project.get("pypi_id", f"git+{url}") + if isinstance(mkdocs_theme, str): + themes.append(Theme(name=project["name"], url=url, pypi_id=pypi_id, mkdocs_id=mkdocs_theme)) + else: + for theme in mkdocs_theme: + themes.append( + Theme(name=f"{project['name']} - {theme.title()}", url=url, pypi_id=pypi_id, mkdocs_id=theme) + ) + return _builtin_themes + sorted(themes, key=lambda theme: theme.name.lower()) + + +# Copy files and expand Jinja templates. +def _prepare_site(src_dir: Path, dest_dir: Path, themes: list[Theme], theme: Theme | None = None) -> None: + jinja = Environment(autoescape=False) + dest_dir.mkdir(parents=True, exist_ok=True) + + for src_path in src_dir.rglob("*"): + if not src_path.is_file(): + continue + dest_path = dest_dir.joinpath(src_path.relative_to(src_dir)) + dest_path.parent.mkdir(parents=True, exist_ok=True) + if src_path.suffix in (".md", ".yml"): + content = src_path.read_text() + content = jinja.from_string(content).render(themes=themes, theme=theme) + dest_path.write_text(content) + else: + shutil.copyfile(src_path, dest_path) + + +# Prepare each theme (docs directory and configuration file). +def prepare_themes(themes: list[Theme]) -> None: + specimen_dir = Path("templates", "specimen") + for theme in themes: + # Copy specific directory, or default to specimen. + theme_dir = Path("themes", theme.mkdocs_id) + theme_conf_dir = Path("templates", "themes", theme.mkdocs_id) + if not theme_conf_dir.exists(): + theme_conf_dir = specimen_dir + shutil.copytree(theme_conf_dir, theme_dir, dirs_exist_ok=True) + + _prepare_site(specimen_dir, theme_dir, themes, theme=theme) + + +# Prepare the main documentation site. +def prepare_main(themes: list[Theme]) -> None: + _prepare_site(Path("templates", "main"), Path("."), themes) + + +# Create virtualenvs and install dependencies. +def install_deps(theme: Theme) -> None: + theme_dir = Path("themes", theme.mkdocs_id) + venv_dir = theme_dir / ".venv" + if not venv_dir.exists(): + venv.create(venv_dir, with_pip=True) + deps = mkdocs_get_deps.get_deps(config_file=theme_dir / "mkdocs.yml") + subprocess.run( + [venv_dir / "bin" / "pip", "install", *deps], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + +# Build theme sites. +def build_themes(themes: list[Theme]) -> None: + parser = argparse.ArgumentParser(prog="build_gallery.py") + parser.add_argument( + "-D", + "--no-deps", + dest="install_deps", + action="store_false", + default=True, + help="Don't install Python dependencies.", + ) + parser.add_argument( + "-T", + "--no-themes", + dest="build_themes", + action="store_false", + default=True, + help="Don't rebuild each theme site.", + ) + parser.add_argument( + "-S", + "--no-shots", + dest="take_screenshots", + action="store_false", + default=True, + help="Don't take screenshots of each theme.", + ) + + opts = parser.parse_args() + + if not opts.install_deps: + print("Skipping dependencies installation") + else: + print("Preparing environments") + + with Pool(len(os.sched_getaffinity(0))) as pool: + tuple(tqdm(pool.imap(install_deps, themes), total=len(themes))) + + if not opts.build_themes: + print("Skipping themes building") + else: + logs_dir = Path("logs") + logs_dir.mkdir(exist_ok=True) + shutil.rmtree(Path("gallery", "themes"), ignore_errors=True) + Path("gallery", "themes").mkdir(parents=True) + + def _build_theme(theme: Theme) -> None: + theme_dir = Path("themes", theme.mkdocs_id).absolute() + dest_dir = Path("gallery", "themes", theme.mkdocs_id).absolute() + print(f"Building {theme.name}") + with logs_dir.joinpath(f"{theme.mkdocs_id}.txt").open("w") as logs_file: + try: + subprocess.run( + [theme_dir.joinpath(".venv", "bin", "mkdocs"), "build", "-d", dest_dir], + stdout=logs_file, + stderr=logs_file, + check=True, + text=True, + cwd=theme_dir, + ) + except subprocess.CalledProcessError: + print("FAILED!") + + for theme in themes: + _build_theme(theme) + + if not opts.take_screenshots: + print("Skipping screenshots") + else: + print("Taking screenshots") + Path("docs", "assets", "img").mkdir(parents=True, exist_ok=True) + for theme in tqdm(themes): + try: + shot_scraper( + [f"gallery/themes/{theme.mkdocs_id}/index.html", "-o", f"docs/assets/img/{theme.mkdocs_id}.png"] + ) + except SystemExit: + pass + except Exception as error: + print(error) + + +# Build main documentation site. +def build_main() -> None: + print("Building gallery's main site") + subprocess.run([sys.executable, "-mmkdocs", "build", "--dirty"], check=True) + + +# Run everything. +def main() -> None: + themes = get_themes() + prepare_themes(themes) + prepare_main(themes) + build_themes(themes) + build_main() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ad2c95d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +black +jinja2 +mkdocs-get-deps +mkdocs-material +pyyaml +shot-scraper +tqdm diff --git a/templates/main/docs/assets/logo.png b/templates/main/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7151383059b8d8b88fbade458bf5e3a661fa597a GIT binary patch literal 726 zcmV;{0xA88P) zB#I(YsZ>4+g7E+1)bFK-wJF3fOu4wYSf0)&Ig7u~9WUo96h%!u9?wsLAmAmUmJlqe z)vD|K{Jgs~;LgHsxBuw(`>$a7pyHd&=G)`rV*!QuhX{wmUtt=dC6rF5GxPcU_21{+ zZZ}|A7CN(=Aj>j{Mx#KIB*1apduTGu-PP4q=U?wiFc?&}&FSf>HWp3O-(ec{R-wIL zrS@K+9tUs0(C2_rYPA}O$K!_cF$@F6Vi8Fquri_4W1k6|IQw7n)=;39{MjZq%#+)Br_M zAd|^}cDoH?u^1Q(2HN>jsRaCf|JFpwvJ9rvsdkKEm<&yZZC$%uuKP}>^SITGB3PEq ztlgn;I2`v*rxS^A9LJ|VpYNL>2q$ZIXw+%sd0snCrBYf~2FGzTtK|uyP)OVGt=H=x zXqxWo +article img { + -webkit-filter: drop-shadow(0px 16px 10px rgba(100,100,100,0.6)); + -moz-filter: drop-shadow(0px 16px 10px rgba(100,100,100,0.6)); + -ms-filter: drop-shadow(0px 16px 10px rgba(100,100,100,0.6)); + -o-filter: drop-shadow(0px 16px 10px rgba(100,100,100,0.6)); + filter: drop-shadow(0px 16px 10px rgba(100,100,100,0.6)); +} + + +{% for builtin in [true, false] %} +{% if builtin %}## Built-in themes{% else %}## Third-party themes{% endif %} + +{% for theme in themes if theme.builtin == builtin %} +### {{theme.name}} + +[![{{theme.name}}](assets/img/{{theme.mkdocs_id}}.png)](themes/{{theme.mkdocs_id}}){ title="Click to browse!" } + +--- +{% endfor %} +{% endfor %} diff --git a/templates/main/mkdocs.yml b/templates/main/mkdocs.yml new file mode 100644 index 0000000..9f1f6c2 --- /dev/null +++ b/templates/main/mkdocs.yml @@ -0,0 +1,25 @@ +site_name: Gallery +site_url: https://mkdocs.github.io/catalog +site_dir: gallery + +theme: + name: material + logo: assets/logo.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: blue + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: blue + toggle: + icon: material/brightness-4 + name: Switch to light mode + +markdown_extensions: + - attr_list + - toc: + permalink: true diff --git a/templates/specimen/docs/index.md b/templates/specimen/docs/index.md new file mode 100644 index 0000000..6c5eb3b --- /dev/null +++ b/templates/specimen/docs/index.md @@ -0,0 +1,20 @@ +# Welcome to {{ theme.name }} + +This page is built with the {% if theme.url %}[{{ theme.name }}]({{ theme.url }}){% else %}{{ theme.name }}{% endif %} theme, +and demonstrates how a few Markdown extensions and MkDocs plugins +will look within this theme. + +{% if theme.pypi_id %} +To install the theme: + +```bash +pip install {{ theme.pypi_id }} +``` +{% endif %} + +To build your docs with this theme: + +```yaml +# mkdocs.yml +theme: {{ theme.mkdocs_id }} +``` diff --git a/templates/specimen/docs/reference.md b/templates/specimen/docs/reference.md new file mode 100644 index 0000000..9760db9 --- /dev/null +++ b/templates/specimen/docs/reference.md @@ -0,0 +1 @@ +::: calculator diff --git a/templates/specimen/mkdocs.yml b/templates/specimen/mkdocs.yml new file mode 100644 index 0000000..f50bbb2 --- /dev/null +++ b/templates/specimen/mkdocs.yml @@ -0,0 +1,54 @@ +site_name: {{ theme.name }} +theme: {{ theme.mkdocs_id }} + +markdown_extensions: +- attr_list +- admonition +- footnotes +- pymdownx.blocks.admonition +- pymdownx.blocks.details +- pymdownx.blocks.tab +- pymdownx.emoji +- pymdownx.keys +- pymdownx.magiclink +- pymdownx.snippets: + check_paths: true +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format +- pymdownx.tasklist: + custom_checkbox: true + +plugins: +- search +- mkdocstrings: + handlers: + python: + paths: [src] + import: [https://docs.python.org/3/objects.inv] + options: + docstring_options: + ignore_init_summary: true + docstring_section_style: list + heading_level: 1 + inherited_members: true + merge_init_into_class: true + separate_signature: true + show_root_heading: true + show_root_full_path: false + show_source: false + show_signature_annotations: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: true + +nav: +- Gallery: ../../ +- Python package docs: reference.md +- Themes: +{%- for theme in themes %} + - "{{ theme.name }}": ../{{ theme.mkdocs_id }} +{%- endfor %} diff --git a/templates/specimen/src/calculator.py b/templates/specimen/src/calculator.py new file mode 100644 index 0000000..5e0d497 --- /dev/null +++ b/templates/specimen/src/calculator.py @@ -0,0 +1,105 @@ +"""Provide several sample math calculations. + +This module allows the user to make mathematical calculations. + +Examples: + >>> from calculator import calculations + >>> calculations.add(2, 4) + 6.0 + >>> calculations.multiply(2.0, 4.0) + 8.0 + >>> from calculator.calculations import divide + >>> divide(4.0, 2) + 2.0 + +The module contains the following functions: + +- `add(a, b)` - Returns the sum of two numbers. +- `subtract(a, b)` - Returns the difference of two numbers. +- `multiply(a, b)` - Returns the product of two numbers. +- `divide(a, b)` - Returns the quotient of two numbers. +""" + +from __future__ import annotations + + +def add(a: int | float, b: int | float) -> float: + """Compute and return the sum of two numbers. + + Examples: + >>> add(4.0, 2.0) + 6.0 + >>> add(4, 2) + 6.0 + + Parameters: + a: A number representing the first addend in the addition. + b: A number representing the second addend in the addition. + + Returns: + A number representing the arithmetic sum of `a` and `b`. + """ + return float(a + b) + +def subtract(a: int | float, b: int | float) -> float: + """Calculate the difference of two numbers. + + Examples: + >>> subtract(4.0, 2.0) + 2.0 + >>> subtract(4, 2) + 2.0 + + Parameters: + a: A number representing the minuend in the subtraction. + b: A number representing the subtrahend in the subtraction. + + Returns: + A number representing the difference between `a` and `b`. + """ + return float(a - b) + +def multiply(a: int | float, b: int | float) -> float: + """Compute and return the product of two numbers. + + Examples: + >>> multiply(4.0, 2.0) + 8.0 + >>> multiply(4, 2) + 8.0 + + Parameters: + a: A number representing the multiplicand in the multiplication. + b: A number representing the multiplier in the multiplication. + + Returns: + A number representing the product of `a` and `b`. + """ + return float(a * b) + +def divide(a: int | float, b: int | float) -> float: + """Compute and return the quotient of two numbers. + + Examples: + >>> divide(4.0, 2.0) + 2.0 + >>> divide(4, 2) + 2.0 + >>> divide(4, 0) + Traceback (most recent call last): + ... + ZeroDivisionError: division by zero + + Parameters: + a: A number representing the dividend in the division. + b: A number representing the divisor in the division. + + Returns: + A number representing the quotient of `a` and `b`. + + Raises: + ZeroDivisionError: An error occurs when the divisor is `0`. + """ + if b == 0: + raise ZeroDivisionError("division by zero") + return float(a / b) \ No newline at end of file