{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "510ed03d-0c3b-4caf-910d-e6eaec08fd63", "metadata": {}, "outputs": [], "source": [ "#|default_exp quarto" ] }, { "cell_type": "markdown", "id": "f0f931c2", "metadata": {}, "source": [ "# quarto\n", "\n", "> Install and interact with Quarto from nbdev" ] }, { "cell_type": "code", "execution_count": null, "id": "6a35c7c4-748f-4c82-a9bf-c780a8d83e90", "metadata": {}, "outputs": [], "source": [ "#|export\n", "from __future__ import annotations\n", "import warnings\n", "\n", "from nbdev.config import *\n", "from nbdev.doclinks import *\n", "\n", "from fastcore.utils import *\n", "from fastcore.script import call_parse\n", "from fastcore.shutil import rmtree,move\n", "from fastcore.meta import delegates\n", "\n", "from os import system\n", "import subprocess,sys,shutil,ast" ] }, { "cell_type": "code", "execution_count": null, "id": "1e623a3d-3e77-44c6-adf3-4768b78328c5", "metadata": {}, "outputs": [], "source": [ "#|hide\n", "from fastcore.test import *" ] }, { "cell_type": "code", "execution_count": null, "id": "aae2d2be-ad03-4536-bf70-c4575f39cea3", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _sprun(cmd):\n", " try: subprocess.check_output(cmd, shell=True)\n", " except subprocess.CalledProcessError as cpe: sys.exit(cpe.returncode)" ] }, { "cell_type": "markdown", "id": "d2d44156", "metadata": {}, "source": [ "## Install" ] }, { "cell_type": "code", "execution_count": null, "id": "75e4b6a1", "metadata": {}, "outputs": [], "source": [ "#|export\n", "BASE_QUARTO_URL='https://www.quarto.org/download/latest/'\n", "\n", "def _install_linux():\n", " system(f'curl -LO {BASE_QUARTO_URL}quarto-linux-amd64.deb')\n", " system('sudo dpkg -i *64.deb && rm *64.deb')\n", " \n", "def _install_mac():\n", " system(f'curl -LO {BASE_QUARTO_URL}quarto-macos.pkg')\n", " system('sudo installer -pkg quarto-macos.pkg -target /')\n", "\n", "@call_parse\n", "def install_quarto():\n", " \"Install latest Quarto on macOS or Linux, prints instructions for Windows\"\n", " if sys.platform not in ('darwin','linux'):\n", " return print('Please visit https://quarto.org/docs/get-started/ to install quarto')\n", " print(\"Installing or upgrading quarto -- this requires root access.\")\n", " system('sudo touch .installing')\n", " try:\n", " installing = Path('.installing')\n", " if not installing.exists(): return print(\"Cancelled. Please download and install Quarto from quarto.org.\")\n", " if 'darwin' in sys.platform: _install_mac()\n", " elif 'linux' in sys.platform: _install_linux()\n", " finally: system('sudo rm -f .installing')" ] }, { "cell_type": "code", "execution_count": null, "id": "7580d48f-e10e-4bb0-937e-90645b5dfd08", "metadata": {}, "outputs": [], "source": [ "#| export\n", "@call_parse\n", "def install():\n", " \"Install Quarto and the current library\"\n", " install_quarto.__wrapped__()\n", " d = get_config().path('lib_path')\n", " if (d/'__init__.py').exists(): system(f'pip install -e \"{d.parent}[dev]\"')" ] }, { "cell_type": "markdown", "id": "bf0cbccc", "metadata": {}, "source": [ "## Sidebar" ] }, { "cell_type": "code", "execution_count": null, "id": "5192dfae", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _doc_paths(path:str=None, doc_path:str=None):\n", " cfg = get_config()\n", " cfg_path = cfg.config_path\n", " path = cfg.path('nbs_path') if not path else Path(path)\n", " doc_path = cfg.path('doc_path') if not doc_path else Path(doc_path)\n", " tmp_doc_path = path/f\"{cfg['doc_path']}\"\n", " return cfg,cfg_path,path,doc_path,tmp_doc_path" ] }, { "cell_type": "code", "execution_count": null, "id": "25e4b6a7", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _f(a,b): return Path(a),b\n", "def _pre(p,b=True): return ' ' * (len(p.parts)) + ('- ' if b else ' ')\n", "def _sort(a):\n", " x,y = a\n", " if y.startswith('index.'): return x,'00'\n", " return a" ] }, { "cell_type": "code", "execution_count": null, "id": "f57c511a", "metadata": {}, "outputs": [], "source": [ "#|export\n", "_def_file_re = '\\.(?:ipynb|qmd|html)$'" ] }, { "cell_type": "code", "execution_count": null, "id": "9a15b749", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@delegates(nbglob_cli)\n", "def _nbglob_docs(\n", " path:str=None, # Path to notebooks\n", " file_glob:str=None, # Only include files matching glob \n", " file_re:str=_def_file_re, # Only include files matching regex\n", " **kwargs):\n", " return nbglob(path, file_glob=file_glob, file_re=file_re, **kwargs)" ] }, { "cell_type": "code", "execution_count": null, "id": "d879c7e1", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "@delegates(_nbglob_docs)\n", "def nbdev_sidebar(\n", " path:str=None, # Path to notebooks\n", " printit:bool=False, # Print YAML for debugging\n", " force:bool=False, # Create sidebar even if settings.ini custom_sidebar=False\n", " skip_folder_re:str='(?:^[_.]|^www$)', # Skip folders matching regex\n", " **kwargs):\n", " \"Create sidebar.yml\"\n", " if not force and str2bool(get_config().custom_sidebar): return\n", " path = get_config().path('nbs_path') if not path else Path(path)\n", " files = nbglob(path, func=_f, skip_folder_re=skip_folder_re, **kwargs).sorted(key=_sort)\n", " lastd,res = Path(),[]\n", " for dabs,name in files:\n", " drel = dabs.relative_to(path)\n", " d = Path()\n", " for p in drel.parts:\n", " d /= p\n", " if d == lastd: continue\n", " title = re.sub('^\\d+_', '', d.name)\n", " res.append(_pre(d.parent) + f'section: {title}')\n", " res.append(_pre(d.parent, False) + 'contents:')\n", " lastd = d\n", " res.append(f'{_pre(d)}{d.joinpath(name)}')\n", "\n", " yml_path = path/'sidebar.yml'\n", " yml = \"website:\\n sidebar:\\n contents:\\n\"\n", " yml += '\\n'.join(f' {o}' for o in res)\n", " if printit: return print(yml)\n", " yml_path.write_text(yml)" ] }, { "cell_type": "code", "execution_count": null, "id": "8a7c2db2", "metadata": {}, "outputs": [], "source": [ "# nbdev_sidebar(printit=True, force=True)" ] }, { "cell_type": "markdown", "id": "ad323b10", "metadata": {}, "source": [ "## Render docs" ] }, { "cell_type": "code", "execution_count": null, "id": "9080eebd", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _render_readme(cfg_path, path, chk_time):\n", " idx_path = path/get_config().readme_nb\n", " if not idx_path.exists(): return\n", " readme_path = cfg_path/'README.md'\n", " if chk_time and readme_path.exists() and readme_path.stat().st_mtime>=idx_path.stat().st_mtime: return\n", "\n", " yml_path = path/'sidebar.yml'\n", " moved=False\n", " if yml_path.exists():\n", " # move out of the way to avoid rendering whole website\n", " yml_path.rename(path/'sidebar.yml.bak')\n", " moved=True\n", " try:\n", " _sprun(f'cd \"{path}\" && quarto render \"{idx_path}\" -o README.md -t gfm --no-execute')\n", " finally:\n", " if moved: (path/'sidebar.yml.bak').rename(yml_path)" ] }, { "cell_type": "code", "execution_count": null, "id": "43bdf85a-3053-4736-ac06-f73cb820c419", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "def nbdev_readme(\n", " path:str=None, # Path to notebooks\n", " doc_path:str=None, # Path to output docs\n", " chk_time:bool=False): # Only build if out of date\n", " \"Render README.md from index.ipynb\"\n", " cfg,cfg_path,path,doc_path,tmp_doc_path = _doc_paths(path, doc_path)\n", " _render_readme(cfg_path, path, chk_time)\n", " if (tmp_doc_path/'README.md').exists():\n", " _rdm = cfg_path/'README.md'\n", " _rdmi = tmp_doc_path/(Path(get_config().readme_nb).stem + '_files')\n", " if _rdm.exists(): _rdm.unlink() # py37 doesn't have arg missing_ok so have to check first\n", " move(tmp_doc_path/'README.md', cfg_path) # README.md is temporarily in nbs/_docs\n", " if _rdmi.exists(): move(_rdmi, cfg_path) # Move Supporting files for README" ] }, { "cell_type": "code", "execution_count": null, "id": "ec701707", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "def prepare():\n", " \"Export, test, and clean notebooks, and render README if needed\"\n", " import nbdev.test\n", " import nbdev.clean\n", " nbdev_export.__wrapped__()\n", " nbdev.test.nbdev_test.__wrapped__()\n", " nbdev.clean.nbdev_clean.__wrapped__()\n", " nbdev_readme.__wrapped__(chk_time=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "ba297c19", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _ensure_quarto():\n", " if shutil.which('quarto'): return\n", " print(\"Quarto is not installed. We will download and install it for you.\")\n", " install.__wrapped__()" ] }, { "cell_type": "code", "execution_count": null, "id": "aabf2f15", "metadata": {}, "outputs": [], "source": [ "#|export\n", "_quarto_yml=\"\"\"ipynb-filters: [nbdev_filter]\n", "\n", "project:\n", " type: website\n", " output-dir: {doc_path}\n", " preview:\n", " port: 3000\n", " browser: false\n", "\n", "format:\n", " html:\n", " theme: cosmo\n", " css: styles.css\n", " toc: true\n", " toc-depth: 4\n", "\n", "website:\n", " title: \"{title}\"\n", " site-url: \"{doc_host}{doc_baseurl}\"\n", " description: \"{description}\"\n", " twitter-card: true\n", " open-graph: true\n", " reader-mode: true\n", " repo-branch: {branch}\n", " repo-url: \"{git_url}\"\n", " repo-actions: [issue]\n", " navbar:\n", " background: primary\n", " search: true\n", " right:\n", " - icon: github\n", " href: \"{git_url}\"\n", " sidebar:\n", " style: \"floating\"\n", "\n", "metadata-files: \n", " - sidebar.yml\n", " - custom.yml\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": null, "id": "38124450", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def refresh_quarto_yml():\n", " \"Generate `_quarto.yml` from `settings.ini`.\"\n", " cfg = get_config()\n", " p = cfg.path('nbs_path')/'_quarto.yml'\n", " vals = {k:cfg.get(k) for k in ['doc_path', 'title', 'description', 'branch', 'git_url', 'doc_host', 'doc_baseurl']}\n", " # Do not build _quarto_yml if custom_quarto_yml is set to True\n", " if str2bool(get_config().custom_quarto_yml): return\n", " if 'title' not in vals: vals['title'] = vals['lib_name']\n", " yml=_quarto_yml.format(**vals)\n", " p.write_text(yml)" ] }, { "cell_type": "code", "execution_count": null, "id": "c453b391", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _is_qpy(path:Path):\n", " \"Is `path` a py script starting with frontmatter?\"\n", " path = Path(path)\n", " if not path.suffix=='.py': return\n", " try: p = ast.parse(path.read_text())\n", " except: return\n", " if not p.body: return\n", " a = p.body[0]\n", " if isinstance(a, ast.Expr) and isinstance(a.value, ast.Constant):\n", " v = a.value.value.strip().splitlines()\n", " return v[0]=='---' and v[-1]=='---'" ] }, { "cell_type": "code", "execution_count": null, "id": "b32303b9", "metadata": {}, "outputs": [], "source": [ "#|export\n", "def _exec_py(fname):\n", " \"Exec a python script and warn on error\"\n", " try: subprocess.check_output('python ' + fname, shell=True)\n", " except subprocess.CalledProcessError as cpe: warn(str(cpe))" ] }, { "cell_type": "code", "execution_count": null, "id": "a2132cb9-358c-40be-8c9f-33747ae8c886", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "@delegates(_nbglob_docs)\n", "def nbdev_quarto(\n", " path:str=None, # Path to notebooks\n", " doc_path:str=None, # Path to output docs\n", " preview:bool=False, # Preview the site instead of building it\n", " file_glob:str=None, # Only include files matching glob \n", " port:int=3000, # The port on which to run preview\n", " **kwargs):\n", " \"Create Quarto docs and README.md\"\n", " _ensure_quarto()\n", " import nbdev.doclinks\n", " nbdev.doclinks._build_modidx(skip_exists=True)\n", " cfg,cfg_path,path,doc_path,tmp_doc_path = _doc_paths(path, doc_path)\n", " refresh_quarto_yml()\n", " nbdev_sidebar.__wrapped__(path, file_glob=file_glob, **kwargs)\n", " pys = globtastic(path, file_glob='*.py', **kwargs).filter(_is_qpy)\n", " for py in pys: _exec_py(py)\n", " if preview: os.system(f'cd \"{path}\" && quarto preview --port {port}')\n", " else: \n", " nbdev_readme.__wrapped__(path, doc_path) # README must be generated BEFORE render because of how index.html is generated by Quarto\n", " _sprun(f'cd \"{path}\" && quarto render --no-cache')\n", " if tmp_doc_path.parent != cfg_path: # move docs folder to root of repo if it doesn't exist there\n", " rmtree(doc_path, ignore_errors=True)\n", " move(tmp_doc_path, cfg_path)" ] }, { "cell_type": "code", "execution_count": null, "id": "f5ddbc96", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "@delegates(nbdev_quarto, but=['preview'])\n", "def preview(\n", " path:str=None, # Path to notebooks\n", " **kwargs):\n", " \"Preview docs locally\"\n", " os.environ['QUARTO_PREVIEW']='1'\n", " nbdev_quarto.__wrapped__(path, preview=True, **kwargs)" ] }, { "cell_type": "code", "execution_count": null, "id": "a5c29641", "metadata": {}, "outputs": [], "source": [ "#|export\n", "@call_parse\n", "@delegates(nbdev_quarto)\n", "def deploy(\n", " path:str=None, # Path to notebooks\n", " skip_build:bool=False, # Don't build docs first\n", " **kwargs):\n", " \"Deploy docs to GitHub Pages\"\n", " if not skip_build: nbdev_quarto.__wrapped__(path, **kwargs)\n", " try: from ghp_import import ghp_import\n", " except: return warnings.warn('Please install ghp-import with `pip install ghp-import`')\n", " ghp_import(get_config().path('doc_path'), push=True, stderr=True, no_history=True)" ] }, { "cell_type": "markdown", "id": "aa35b010", "metadata": {}, "source": [ "## Export -" ] }, { "cell_type": "code", "execution_count": null, "id": "3d8031ce", "metadata": {}, "outputs": [], "source": [ "#|hide\n", "import nbdev; nbdev.nbdev_export()" ] }, { "cell_type": "code", "execution_count": null, "id": "12588a26-43a6-42c4-bacd-896293c871ab", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }