├── nbs ├── .nojekyll ├── explanations │ ├── .nodoc │ ├── .notest │ ├── images │ │ └── github-actions-pages.png │ ├── index.qmd │ ├── why_nbdev.ipynb │ └── config.ipynb ├── tutorials │ ├── .nodoc │ ├── .notest │ ├── images │ │ ├── github-repo-empty.png │ │ ├── jupyter-home-page.png │ │ ├── github-actions-pages.png │ │ ├── github-enable-pages.png │ │ ├── marie-curie-notebook.jpg │ │ ├── github-actions-initial.png │ │ ├── github-create-new-repo.png │ │ ├── jupyter-blank-terminal.png │ │ ├── jupyter-friendly-conflict.png │ │ ├── jupyter-unreadable-notebook.png │ │ └── nbdev-hello-world-site-initial.png │ ├── index.qmd │ ├── docs_only.ipynb │ ├── renderscript.qmd.py │ ├── qmd_intro.qmd │ └── pre_commit.ipynb ├── CNAME ├── .gitignore ├── favicon.png ├── images │ ├── bom.png │ ├── card.png │ ├── favicon.png │ ├── david-berg.jpeg │ ├── overstory.png │ ├── chris-lattner.png │ ├── erik-gaasedelen.jpeg │ ├── fernando-pérez.jpeg │ ├── roxanna-pourzand.jpeg │ ├── hugo-bowne-anderson.jpeg │ ├── testing.svg │ ├── netflix.svg │ ├── docs.svg │ ├── vscode.svg │ ├── amd.svg │ ├── packaging.svg │ ├── git.svg │ ├── lyft.svg │ ├── circles.svg.py │ ├── transform.svg │ ├── outerbounds.svg │ ├── novetta.svg │ └── jupyter.svg ├── api │ ├── images │ │ └── token.png │ ├── index.qmd │ ├── 17_serve.ipynb │ ├── 15_qmd.ipynb │ ├── 06_sync.ipynb │ ├── 04_export.ipynb │ └── 09_frontmatter.ipynb ├── blog │ ├── posts │ │ ├── 2022-07-28-nbdev2 │ │ │ ├── patch.png │ │ │ ├── lang_rank.png │ │ │ ├── logo_lyft.png │ │ │ ├── logo_ob.png │ │ │ ├── nb_quarto.png │ │ │ ├── logo_netflix.png │ │ │ └── logo_transform.png │ │ ├── 2022-11-07-spaces │ │ │ ├── final_app.png │ │ │ ├── blog_cover.jpeg │ │ │ ├── create_space.png │ │ │ ├── gradio_local.png │ │ │ └── app.py │ │ └── 2022-08-25-jupyter-git │ │ │ ├── friendly-conflict.png │ │ │ └── unreadable-notebook.png │ └── index.qmd ├── custom.yml ├── nbdev.yml ├── styles.css ├── _quarto.yml ├── index.css └── index.qmd.py ├── tests ├── .nodoc ├── .notest ├── notest │ ├── .notest │ └── nb_ignore.ipynb ├── .gitignore ├── 2022-09-06-homeschooling.md ├── black.ipynb ├── minimal.ipynb ├── 00_some.thing.ipynb ├── metadata.ipynb ├── example.ipynb.broken ├── old_directives.ipynb ├── 2020-01-14-test-markdown-post.md ├── directives.ipynb ├── showdoc_test.ipynb └── 01_everything.ipynb ├── nbdev ├── __init__.py ├── imports.py ├── serve_drv.py ├── extract_attachments.py ├── frontmatter.py ├── export.py ├── sync.py ├── serve.py ├── qmd.py ├── cli.py ├── merge.py ├── test.py ├── process.py └── migrate.py ├── MANIFEST.in ├── TODO.md ├── .github ├── workflows │ ├── deploy.yaml │ ├── test.yaml │ └── deploy-manual.yaml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .pre-commit-hooks.yaml ├── CONTRIBUTING.md ├── settings.ini ├── .gitignore ├── setup.py └── CODE_OF_CONDUCT.md /nbs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.nodoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.notest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbs/explanations/.nodoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbs/tutorials/.nodoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbs/tutorials/.notest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/notest/.notest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbs/CNAME: -------------------------------------------------------------------------------- 1 | nbdev.fast.ai 2 | -------------------------------------------------------------------------------- /nbs/explanations/.notest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | _docs_test_files/ 2 | -------------------------------------------------------------------------------- /nbs/.gitignore: -------------------------------------------------------------------------------- 1 | site_libs/ 2 | docs/ 3 | /.quarto/ 4 | -------------------------------------------------------------------------------- /nbs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/favicon.png -------------------------------------------------------------------------------- /nbs/images/bom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/bom.png -------------------------------------------------------------------------------- /nbs/images/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/card.png -------------------------------------------------------------------------------- /nbs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/favicon.png -------------------------------------------------------------------------------- /nbs/api/images/token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/api/images/token.png -------------------------------------------------------------------------------- /nbs/images/david-berg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/david-berg.jpeg -------------------------------------------------------------------------------- /nbs/images/overstory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/overstory.png -------------------------------------------------------------------------------- /nbs/images/chris-lattner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/chris-lattner.png -------------------------------------------------------------------------------- /nbs/images/erik-gaasedelen.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/erik-gaasedelen.jpeg -------------------------------------------------------------------------------- /nbs/images/fernando-pérez.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/fernando-pérez.jpeg -------------------------------------------------------------------------------- /nbs/images/roxanna-pourzand.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/roxanna-pourzand.jpeg -------------------------------------------------------------------------------- /nbs/images/hugo-bowne-anderson.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/images/hugo-bowne-anderson.jpeg -------------------------------------------------------------------------------- /nbdev/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.3.12" 2 | 3 | from .doclinks import nbdev_export 4 | from .showdoc import show_doc 5 | 6 | -------------------------------------------------------------------------------- /nbdev/imports.py: -------------------------------------------------------------------------------- 1 | from fastcore.imports import * 2 | from fastcore.foundation import * 3 | from fastcore.utils import * 4 | 5 | -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/patch.png -------------------------------------------------------------------------------- /nbs/tutorials/images/github-repo-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/github-repo-empty.png -------------------------------------------------------------------------------- /nbs/tutorials/images/jupyter-home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/jupyter-home-page.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include settings.ini 2 | include LICENSE 3 | include CONTRIBUTING.md 4 | include README.md 5 | recursive-exclude * __pycache__ 6 | -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/lang_rank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/lang_rank.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/logo_lyft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/logo_lyft.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/logo_ob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/logo_ob.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/nb_quarto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/nb_quarto.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-11-07-spaces/final_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-11-07-spaces/final_app.png -------------------------------------------------------------------------------- /nbs/tutorials/images/github-actions-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/github-actions-pages.png -------------------------------------------------------------------------------- /nbs/tutorials/images/github-enable-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/github-enable-pages.png -------------------------------------------------------------------------------- /nbs/tutorials/images/marie-curie-notebook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/marie-curie-notebook.jpg -------------------------------------------------------------------------------- /nbs/blog/posts/2022-11-07-spaces/blog_cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-11-07-spaces/blog_cover.jpeg -------------------------------------------------------------------------------- /nbs/explanations/images/github-actions-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/explanations/images/github-actions-pages.png -------------------------------------------------------------------------------- /nbs/tutorials/images/github-actions-initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/github-actions-initial.png -------------------------------------------------------------------------------- /nbs/tutorials/images/github-create-new-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/github-create-new-repo.png -------------------------------------------------------------------------------- /nbs/tutorials/images/jupyter-blank-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/jupyter-blank-terminal.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/logo_netflix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/logo_netflix.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-07-28-nbdev2/logo_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-07-28-nbdev2/logo_transform.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-11-07-spaces/create_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-11-07-spaces/create_space.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-11-07-spaces/gradio_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-11-07-spaces/gradio_local.png -------------------------------------------------------------------------------- /nbs/tutorials/images/jupyter-friendly-conflict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/jupyter-friendly-conflict.png -------------------------------------------------------------------------------- /nbs/tutorials/images/jupyter-unreadable-notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/jupyter-unreadable-notebook.png -------------------------------------------------------------------------------- /nbs/tutorials/images/nbdev-hello-world-site-initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/tutorials/images/nbdev-hello-world-site-initial.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-08-25-jupyter-git/friendly-conflict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-08-25-jupyter-git/friendly-conflict.png -------------------------------------------------------------------------------- /nbs/blog/posts/2022-08-25-jupyter-git/unreadable-notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/nbdev/master/nbs/blog/posts/2022-08-25-jupyter-git/unreadable-notebook.png -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## Docs export 2 | 3 | - `show_doc` changes: 4 | - support `#default_cls_lvl` 5 | - pick header level for functions/methods 6 | - implement stuff from nbdev export2html, e.g.(?): 7 | - `remove_widget_state` 8 | - fastcore conversion scripts + instuctions 9 | 10 | -------------------------------------------------------------------------------- /nbs/custom.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | output-dir: _docs 4 | 5 | website: 6 | title: "nbdev" 7 | site-url: "https://nbdev.fast.ai/" 8 | description: "Create delightful software with Jupyter Notebooks" 9 | repo-url: "https://github.com/fastai/nbdev" 10 | 11 | -------------------------------------------------------------------------------- /nbs/nbdev.yml: -------------------------------------------------------------------------------- 1 | project: 2 | output-dir: _docs 3 | 4 | website: 5 | title: "nbdev" 6 | site-url: "https://nbdev.fast.ai/" 7 | description: "Create delightful software with Jupyter Notebooks" 8 | repo-branch: master 9 | repo-url: "https://github.com/fastai/nbdev" 10 | -------------------------------------------------------------------------------- /nbs/tutorials/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | title: Tutorials 4 | listing: 5 | fields: [title, description] 6 | type: table 7 | sort-ui: false 8 | filter-ui: false 9 | --- 10 | 11 | Click through to any of these tutorials to get started with nbdev's features. 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: [master] 5 | workflow_dispatch: 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: fastai/workflows/quarto-ghp@master 11 | # with: {pre: 1} 12 | -------------------------------------------------------------------------------- /nbs/blog/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: nbdev Blog 3 | subtitle: News, tips, and commentary about all things nbdev 4 | listing: 5 | sort: "date desc" 6 | contents: "posts" 7 | sort-ui: false 8 | filter-ui: false 9 | categories: true 10 | feed: true 11 | page-layout: full 12 | --- -------------------------------------------------------------------------------- /nbs/explanations/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | title: Explanations 4 | listing: 5 | fields: [title, description] 6 | type: table 7 | sort-ui: false 8 | filter-ui: false 9 | --- 10 | 11 | These explanations provide background information on how nbdev is designed and how it works. They are designed to help people who want to more deeply understand the nbdev system. 12 | 13 | -------------------------------------------------------------------------------- /tests/2022-09-06-homeschooling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Essential Work-From-Home Advice: Cheap and Easy Ergonomic Setups" 3 | summary: "You can permanently damage your back, neck, and wrists from working without an ergonomic setup. Learn how to create one for less at home." 4 | image: '/images/ergonomic1-short.jpg' 5 | author: 'Rachel Thomas' 6 | tags: advice health 7 | --- 8 | 9 | Lorem ipsum 10 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - &hook 2 | id: nbdev_clean 3 | name: nbdev_clean 4 | entry: nbdev_clean 5 | description: "Clean metadata from notebooks to avoid merge conflicts" 6 | language: python 7 | always_run: true 8 | pass_filenames: false 9 | 10 | - <<: *hook 11 | id: nbdev_export 12 | name: nbdev_export 13 | entry: nbdev_export 14 | description: "Export notebooks to modules and build modidx" 15 | -------------------------------------------------------------------------------- /nbs/api/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | order: 9 3 | title: API 4 | listing: 5 | fields: [title, description] 6 | type: table 7 | sort-ui: false 8 | filter-ui: false 9 | --- 10 | 11 | This section contains API details for each of nbdev's python submodules. This reference documentation is mainly useful for people looking to customise or build on top of nbdev, or wanting detailed information about how nbdev works. 12 | 13 | -------------------------------------------------------------------------------- /nbs/images/testing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/black.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b9cc4e2f-fa2a-4e25-b078-98bc84ecbb0b", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "j = [1,\n", 11 | " 2,\n", 12 | " 3\n", 13 | "]" 14 | ] 15 | } 16 | ], 17 | "metadata": { 18 | "kernelspec": { 19 | "display_name": "Python 3 (ipykernel)", 20 | "language": "python", 21 | "name": "python3" 22 | } 23 | }, 24 | "nbformat": 4, 25 | "nbformat_minor": 5 26 | } 27 | -------------------------------------------------------------------------------- /tests/notest/nb_ignore.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3832a663-51f6-47db-846c-156086a0fc0d", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook should not be tested due to a sibling file called `.notest`" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "3de2ee33-9ee9-4376-a602-d8ae1e6ca4e7", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "assert 123 == 593" 19 | ] 20 | } 21 | ], 22 | "metadata": { 23 | "kernelspec": { 24 | "display_name": "Python 3 (ipykernel)", 25 | "language": "python", 26 | "name": "python3" 27 | } 28 | }, 29 | "nbformat": 4, 30 | "nbformat_minor": 5 31 | } 32 | -------------------------------------------------------------------------------- /nbs/blog/posts/2022-11-07-spaces/app.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: app.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['iface', 'size'] 5 | 6 | # %% app.ipynb 6 7 | import gradio as gr 8 | from fastcore.net import urljson, HTTPError 9 | 10 | # %% app.ipynb 8 11 | def size(repo:str): 12 | "Returns the size in GB of a HuggingFace Dataset." 13 | url = f'https://huggingface.co/api/datasets/{repo}' 14 | try: resp = urljson(f'{url}/treesize/main') 15 | except HTTPError: return f'Did not find repo: {url}' 16 | gb = resp['size'] / 1e9 17 | return f'{gb:.2f} GB' 18 | 19 | # %% app.ipynb 12 20 | iface = gr.Interface(fn=size, inputs=gr.Text(value="tglcourse/CelebA-faces-cropped-128"), outputs="text") 21 | iface.launch(height=450, width=500) 22 | -------------------------------------------------------------------------------- /nbdev/serve_drv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from execnb.nbio import read_nb,write_nb 3 | from io import StringIO 4 | from contextlib import redirect_stdout 5 | 6 | def exec_scr(src, dst, md): 7 | f = StringIO() 8 | g = {} 9 | with redirect_stdout(f): exec(compile(src.read_text(), src, 'exec'), g) 10 | res = "" 11 | if md: res += "---\n" + md + "\n---\n\n" 12 | dst.write_text(res + f.getvalue()) 13 | 14 | def exec_nb(src, dst, cb): 15 | nb = read_nb(src) 16 | cb()(nb) 17 | write_nb(nb, dst) 18 | 19 | def main(o): 20 | src,dst,x = o 21 | os.environ["IN_TEST"] = "1" 22 | if src.suffix=='.ipynb': exec_nb(src, dst, x) 23 | elif src.suffix=='.py': exec_scr(src, dst, x) 24 | else: raise Exception(src) 25 | del os.environ["IN_TEST"] 26 | 27 | -------------------------------------------------------------------------------- /tests/minimal.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## A minimal notebook" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "data": { 17 | "text/plain": [ 18 | "2" 19 | ] 20 | }, 21 | "execution_count": null, 22 | "metadata": {}, 23 | "output_type": "execute_result" 24 | } 25 | ], 26 | "source": [ 27 | "# Do some arithmetic\n", 28 | "1+1" 29 | ] 30 | } 31 | ], 32 | "metadata": { 33 | "kernelspec": { 34 | "display_name": "Python 3 (ipykernel)", 35 | "language": "python", 36 | "name": "python3" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /tests/00_some.thing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook is used to demonstrate exporting to an existing module. See the notebooks in `nbs` for how it's used." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "#|hide\n", 17 | "#|default_exp some.thing" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "#|export\n", 27 | "a=1" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "kernelspec": { 33 | "display_name": "Python 3 (ipykernel)", 34 | "language": "python", 35 | "name": "python3" 36 | } 37 | }, 38 | "nbformat": 4, 39 | "nbformat_minor": 4 40 | } 41 | -------------------------------------------------------------------------------- /nbs/images/netflix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nbs/images/docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | os: [ubuntu, macos] 14 | version: ["3.7", "3.8", "3.9", "3.10"] 15 | runs-on: ${{ matrix.os }}-latest 16 | steps: 17 | - uses: fastai/workflows/nbdev-ci@master 18 | with: 19 | version: ${{ matrix.version }} 20 | pre: 1 21 | - name: test docs build 22 | if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.version == '3.9' && matrix.os == 'ubuntu' }} 23 | run: | 24 | set -ux 25 | wget -q $(curl https://latest.fast.ai/pre/quarto-dev/quarto-cli/linux-amd64.deb) 26 | sudo dpkg -i quarto*.deb 27 | nbdev_docs 28 | if [ -f "_docs/index.html" ]; then 29 | echo "docs built successfully." 30 | else 31 | echo "index page not found in rendered docs." 32 | ls -la 33 | ls -la _docs 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /nbs/images/vscode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nbs/styles.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .cell > .sourceCode { 6 | margin-bottom: 0; 7 | } 8 | 9 | .cell-output > pre { 10 | margin-bottom: 0; 11 | } 12 | 13 | .cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { 14 | margin-left: 0.8rem; 15 | margin-top: 0; 16 | background: none; 17 | border-left: 2px solid lightsalmon; 18 | border-top-left-radius: 0; 19 | border-top-right-radius: 0; 20 | } 21 | 22 | .cell-output > .sourceCode { 23 | border: none; 24 | } 25 | 26 | .cell-output > .sourceCode { 27 | background: none; 28 | margin-top: 0; 29 | } 30 | 31 | div.description { 32 | padding-left: 2px; 33 | padding-top: 5px; 34 | font-style: italic; 35 | font-size: 135%; 36 | opacity: 70%; 37 | } 38 | 39 | /* show_doc signature */ 40 | blockquote > pre { 41 | font-size: 14px; 42 | } 43 | 44 | .table { 45 | font-size: 16px; 46 | /* disable striped tables */ 47 | --bs-table-striped-bg: var(--bs-table-bg); 48 | } 49 | 50 | .quarto-figure-center > figure > figcaption { 51 | text-align: center; 52 | } 53 | 54 | .figure-caption { 55 | font-size: 75%; 56 | font-style: italic; 57 | } -------------------------------------------------------------------------------- /nbs/images/amd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy-manual.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | deploy: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-python@v3 10 | - name: Install Dependencies 11 | shell: bash 12 | run: | 13 | python -m pip install --upgrade pip 14 | pip install -Uq git+https://github.com/fastai/ghapi.git # you need this for enabling pages 15 | pip install -Uq git+https://github.com/fastai/fastcore.git 16 | pip install -Uq git+https://github.com/fastai/execnb.git 17 | pip install -e ".[dev]" 18 | wget -nv https://www.quarto.org/download/latest/quarto-linux-amd64.deb 19 | sudo dpkg -i quarto*.deb 20 | nbdev_docs 21 | - name: Deploy to GitHub Pages 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ github.token }} 25 | force_orphan: true 26 | publish_dir: ${{ env.NBDEV_DOCS }} 27 | user_name: github-actions[bot] 28 | user_email: 41898282+github-actions[bot]@users.noreply.github.com 29 | 30 | -------------------------------------------------------------------------------- /nbs/images/packaging.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nbs/images/git.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/metadata.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## A notebook with metadata" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "hide_input": true, 15 | "my_extra_cell_key": "foo", 16 | "my_removed_cell_key": "foo", 17 | "meta": 37 18 | }, 19 | "outputs": [ 20 | { 21 | "data": { 22 | "text/plain": [ 23 | "2" 24 | ] 25 | }, 26 | "execution_count": 1, 27 | "metadata": {}, 28 | "output_type": "execute_result" 29 | } 30 | ], 31 | "source": [ 32 | "# A cell with metadata\n", 33 | "1+1" 34 | ] 35 | } 36 | ], 37 | "metadata": { 38 | "kernelspec": { 39 | "display_name": "Python 3 (ipykernel)", 40 | "language": "python", 41 | "name": "python3" 42 | }, 43 | "language_info": { 44 | "codemirror_mode": { 45 | "name": "ipython", 46 | "version": 3 47 | }, 48 | "file_extension": ".py", 49 | "mimetype": "text/x-python", 50 | "name": "python", 51 | "nbconvert_exporter": "python", 52 | "pygments_lexer": "ipython3", 53 | "version": "3.7.13" 54 | }, 55 | "my_extra_key": "foo", 56 | "my_removed_key": "foo", 57 | "jekyll": "some_meta", 58 | "meta": 37 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 4 62 | } 63 | -------------------------------------------------------------------------------- /tests/example.ipynb.broken: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 6, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": [ 11 | "3" 12 | ] 13 | }, 14 | "execution_count": 6, 15 | "metadata": {}, 16 | "output_type": "execute_result" 17 | } 18 | ], 19 | "source": [ 20 | <<<<<<< HEAD 21 | "z=3\n", 22 | ======= 23 | "z=2\n", 24 | >>>>>>> a7ec1b0bfb8e23b05fd0a2e6cafcb41cd0fb1c35 25 | "z" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 7, 31 | "execution_count": 5, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "6" 38 | ] 39 | }, 40 | <<<<<<< HEAD 41 | "execution_count": 7, 42 | ======= 43 | "execution_count": 5, 44 | >>>>>>> a7ec1b0bfb8e23b05fd0a2e6cafcb41cd0fb1c35 45 | "metadata": {}, 46 | "output_type": "execute_result" 47 | } 48 | ], 49 | "source": [ 50 | "x=3\n", 51 | "y=3\n", 52 | "x+y" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [] 61 | } 62 | ], 63 | "metadata": { 64 | "kernelspec": { 65 | "display_name": "Python 3", 66 | "language": "python", 67 | "name": "python3" 68 | } 69 | }, 70 | "nbformat": 4, 71 | "nbformat_minor": 2 72 | } 73 | -------------------------------------------------------------------------------- /nbs/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | preview: 4 | port: 3000 5 | browser: false 6 | 7 | format: 8 | html: 9 | theme: cosmo 10 | css: styles.css 11 | toc: true 12 | toc-depth: 4 13 | 14 | website: 15 | twitter-card: true 16 | open-graph: true 17 | repo-actions: [issue] 18 | sidebar: 19 | style: floating 20 | contents: 21 | - auto: "/*.ipynb" 22 | - section: Tutorials 23 | contents: tutorials/* 24 | - section: Explanations 25 | contents: explanations/* 26 | - section: API 27 | contents: api/* 28 | favicon: favicon.png 29 | navbar: 30 | background: primary 31 | search: true 32 | collapse-below: lg 33 | left: 34 | - text: "Get Started" 35 | href: getting_started.ipynb 36 | - text: "Tutorial" 37 | href: tutorials/tutorial.ipynb 38 | - text: "Blog" 39 | href: blog/index.qmd 40 | - text: "Help" 41 | menu: 42 | - text: "Report an Issue" 43 | icon: bug 44 | href: https://github.com/fastai/nbdev/issues 45 | - text: "Fast.ai Forum" 46 | icon: chat-right-text 47 | href: https://forums.fast.ai/ 48 | - text: "FAQ" 49 | icon: question-circle 50 | href: getting_started.ipynb#faq 51 | right: 52 | - icon: github 53 | href: "https://github.com/fastai/nbdev" 54 | - icon: twitter 55 | href: https://twitter.com/fastdotai 56 | aria-label: Fast.ai Twitter 57 | 58 | metadata-files: [nbdev.yml] 59 | 60 | -------------------------------------------------------------------------------- /tests/old_directives.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "23fe7353-8bac-4ac3-a7df-d80ff3186e0f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#hide\n", 11 | " # export" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "d4419c73-e4df-40d2-a6a9-8f9299f5f832", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "#hide\n", 22 | " # export\n", 23 | "def there_is_code(): ..." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "id": "82d2db47-24d7-43ba-9a5a-d61af1a06b12", 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "a = 'no directives'" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "183e982d-7f38-417b-b774-ccadd6b76e2e", 39 | "metadata": {}, 40 | "source": [ 41 | "some md" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "id": "20b94777-1b99-4a0f-bf76-766343470047", 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "#eval: false\n", 52 | "assert 1 == 2" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "d4b7b3db-6831-46b7-aa3f-4e6cc0bc0a24", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "# notest\n", 63 | "assert 1 == 2" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "3f49b654-cda4-49f1-8763-1724e18b1def", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "not_a_directive='#export'" 74 | ] 75 | } 76 | ], 77 | "metadata": { 78 | "kernelspec": { 79 | "display_name": "Python 3 (ipykernel)", 80 | "language": "python", 81 | "name": "python3" 82 | } 83 | }, 84 | "nbformat": 4, 85 | "nbformat_minor": 5 86 | } 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a minimal reproducible example to help us improve 4 | labels: ["bug"] 5 | 6 | --- 7 | 8 | # Provide a minimally reproducible example 9 | 10 | To maximize the chances of your issue being fixed, you should share a minimally reproducible example that allows us to reproduce your error. A good way to do this is to setup a new nbdev project which containts the minimal amount of code that reproduces your error. 11 | 12 | If we are not able to reproduce your error, we may close your issue. 13 | 14 | ## Some background on minimal reproducible examples [^1] 15 | 16 | The first benefit of a public reproduction is to prove that the problem is not caused by your environment or by a setting you left out of your description, thinking it wasn't relevant. If there were any doubts about whether you'd found a genuine problem before, they are usually removed once a reproduction is made. 17 | 18 | Next, when a reproduction has minimal config and code, it can often let us narrow down or even identify the root cause, suggest workarounds, etc. This means we can often help you from code inspection alone. 19 | 20 | Finally, by making the code/dependencies minimal, it usually makes the problem feasible to step through using a debugging if code inspection wasn't sufficient. Production repositories or non-minimal reproductions are often very difficult to debug because break points get triggered dozens or hundreds or times. 21 | 22 | The basic idea of a minimal reproduction is to use the least amount of both code and config to trigger missing or wrong behavior. A minimal reproduction helps the developers see where the bug or missing feature is, and allows us to verify that the new code meets the requirements. 23 | 24 | ## Where to host your mimimal reproducible example 25 | 26 | A new, **public** GitHub repo is the best place to host your minimal reproducible example. 27 | 28 | [^1]: the below guidance was adapted [from this language](https://github.com/renovatebot/renovate/blob/main/docs/development/minimal-reproductions.md) 29 | -------------------------------------------------------------------------------- /nbs/images/lyft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo_standard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /nbdev/extract_attachments.py: -------------------------------------------------------------------------------- 1 | """ A preprocessor that extracts all of the attachments from the notebook file. 2 | The extracted attachments are returned in the 'resources' dictionary. 3 | 4 | Based on the ExtractOutputsProcessor in nbconvert... the license for nbconvert is: 5 | 6 | # Licensing terms 7 | 8 | This project is licensed under the terms of the Modified BSD License 9 | (also known as New or Revised or 3-Clause BSD), as follows: 10 | 11 | - Copyright (c) 2001-2015, IPython Development Team 12 | - Copyright (c) 2015-, Jupyter Development Team 13 | 14 | All rights reserved. """ 15 | 16 | import sys,os 17 | from binascii import a2b_base64 18 | from traitlets import Unicode, Set 19 | from nbconvert.preprocessors.base import Preprocessor 20 | 21 | class ExtractAttachmentsPreprocessor(Preprocessor): 22 | "Extracts all of the outputs from the notebook file." 23 | output_filename_template = Unicode( "attach_{cell_index}_{name}").tag(config=True) 24 | extract_output_types = Set( {'image/png', 'image/jpeg', 'image/svg+xml', 'application/pdf'}).tag(config=True) 25 | 26 | def preprocess_cell(self, cell, resources, cell_index): 27 | output_files_dir = resources.get('output_files_dir', None) 28 | if not isinstance(resources['outputs'], dict): resources['outputs'] = {} 29 | 30 | for name, attach in cell.get("attachments", {}).items(): 31 | for mime, data in attach.items(): 32 | if mime not in self.extract_output_types: continue 33 | # Binary files are base64-encoded, SVG is already XML 34 | if mime in {'image/png', 'image/jpeg', 'application/pdf'}: data = a2b_base64(data) 35 | elif sys.platform == 'win32': data = data.replace('\n', '\r\n').encode("UTF-8") 36 | else: data = data.encode("UTF-8") 37 | filename = self.output_filename_template.format( cell_index=cell_index, name=name) 38 | if output_files_dir is not None: filename = os.path.join(output_files_dir, filename) 39 | if name.endswith(".gif") and mime == "image/png": filename = filename.replace(".gif", ".png") 40 | resources['outputs'][filename] = data 41 | attach_str = "attachment:"+name 42 | if attach_str in cell.source: cell.source = cell.source.replace(attach_str, filename) 43 | 44 | return cell, resources 45 | 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## How to get started 4 | 5 | Before anything else, please install the git hooks that run automatic scripts during each commit and merge to strip the notebooks of superfluous metadata (and avoid merge conflicts). After cloning the repository, run the following command inside it: 6 | ``` 7 | nbdev_install_hooks 8 | ``` 9 | 10 | ## Did you find a bug? 11 | 12 | * Ensure the bug was not already reported by searching on GitHub under Issues. 13 | * If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 14 | * Be sure to add the complete error messages. 15 | 16 | #### Did you write a patch that fixes a bug? 17 | 18 | * Open a new GitHub pull request with the patch. 19 | * Ensure that your PR includes a test that fails without your patch, and pass with it. 20 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 21 | 22 | ## PR submission guidelines 23 | 24 | * Keep each PR focused. While it's more convenient, do not combine several unrelated fixes together. Create as many branches as needing to keep each PR focused. 25 | * Do not mix style changes/fixes with "functional" changes. It's very difficult to review such PRs and it most likely get rejected. 26 | * Do not add/remove vertical whitespace. Preserve the original style of the file you edit as much as you can. 27 | * Do not turn an already submitted PR into your development playground. If after you submitted PR, you discovered that more work is needed - close the PR, do the required work and then submit a new PR. Otherwise each of your commits requires attention from maintainers of the project. 28 | * If, however, you submitted a PR and received a request for changes, you should proceed with commits inside that PR, so that the maintainer can see the incremental fixes and won't need to review the whole PR again. In the exception case where you realize it'll take many many commits to complete the requests, then it's probably best to close the PR, do the work and then submit it again. Use common sense where you'd choose one way over another. 29 | 30 | ## Do you want to contribute to the documentation? 31 | 32 | * Docs are automatically created from the notebooks in the nbs folder. 33 | 34 | -------------------------------------------------------------------------------- /nbdev/frontmatter.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/09_frontmatter.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['FrontmatterProc'] 5 | 6 | # %% ../nbs/api/09_frontmatter.ipynb 2 7 | from .imports import * 8 | from .process import * 9 | from .doclinks import _nbpath2html 10 | 11 | from execnb.nbio import * 12 | from fastcore.imports import * 13 | import yaml 14 | 15 | # %% ../nbs/api/09_frontmatter.ipynb 5 16 | _RE_FM_BASE=r'''^---\s* 17 | (.*?\S+.*?) 18 | ---\s*''' 19 | 20 | _re_fm_nb = re.compile(_RE_FM_BASE+'$', flags=re.DOTALL) 21 | _re_fm_md = re.compile(_RE_FM_BASE, flags=re.DOTALL) 22 | 23 | def _fm2dict(s:str, nb=True): 24 | "Load YAML frontmatter into a `dict`" 25 | re_fm = _re_fm_nb if nb else _re_fm_md 26 | match = re_fm.search(s.strip()) 27 | return yaml.safe_load(match.group(1)) if match else {} 28 | 29 | def _md2dict(s:str): 30 | "Convert H1 formatted markdown cell to frontmatter dict" 31 | if '#' not in s: return {} 32 | m = re.search(r'^#\s+(\S.*?)\s*$', s, flags=re.MULTILINE) 33 | if not m: return {} 34 | res = {'title': m.group(1)} 35 | m = re.search(r'^>\s+(\S.*?)\s*$', s, flags=re.MULTILINE) 36 | if m: res['description'] = m.group(1) 37 | r = re.findall(r'^-\s+(\S.*:.*\S)\s*$', s, flags=re.MULTILINE) 38 | if r: 39 | try: res.update(yaml.safe_load('\n'.join(r))) 40 | except Exception as e: warn(f'Failed to create YAML dict for:\n{r}\n\n{e}\n') 41 | return res 42 | 43 | # %% ../nbs/api/09_frontmatter.ipynb 6 44 | def _dict2fm(d): return f'---\n{yaml.dump(d)}\n---\n\n' 45 | def _insertfm(nb, fm): nb.cells.insert(0, mk_cell(_dict2fm(fm), 'raw')) 46 | 47 | class FrontmatterProc(Processor): 48 | "A YAML and formatted-markdown frontmatter processor" 49 | def begin(self): self.fm = getattr(self.nb, 'frontmatter_', {}) 50 | 51 | def _update(self, f, cell): 52 | s = cell.get('source') 53 | if not s: return 54 | d = f(s) 55 | if not d: return 56 | self.fm.update(d) 57 | cell.source = None 58 | 59 | def cell(self, cell): 60 | if cell.cell_type=='raw': self._update(_fm2dict, cell) 61 | elif cell.cell_type=='markdown' and 'title' not in self.fm: self._update(_md2dict, cell) 62 | 63 | def end(self): 64 | self.nb.frontmatter_ = self.fm 65 | if not self.fm: return 66 | self.fm.update({'output-file': _nbpath2html(Path(self.nb.path_)).name}) 67 | _insertfm(self.nb, self.fm) 68 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | lib_name = nbdev 3 | repo = nbdev 4 | description = Create delightful software with Jupyter Notebooks 5 | copyright = 2020 onwards, Jeremy Howard 6 | keywords = nbdev fastai jupyter notebook export 7 | user = fastai 8 | author = Jeremy Howard and Hamel Husain 9 | author_email = j@fast.ai 10 | branch = master 11 | min_python = 3.7 12 | version = 2.3.12 13 | audience = Developers 14 | language = English 15 | custom_sidebar = True 16 | license = apache2 17 | status = 2 18 | requirements = fastcore>=1.5.27 execnb>=0.1.4 astunparse ghapi>=1.0.3 watchdog asttokens 19 | pip_requirements = PyYAML 20 | conda_requirements = pyyaml 21 | conda_user = fastai 22 | dev_requirements = nbdev-numpy nbdev-stdlib pandas matplotlib black svg.py jupyter 23 | console_scripts = nbdev_create_config=nbdev.config:nbdev_create_config 24 | nbdev_update=nbdev.sync:nbdev_update 25 | nbdev_export=nbdev.doclinks:nbdev_export 26 | nbdev_fix=nbdev.merge:nbdev_fix 27 | nbdev_merge=nbdev.merge:nbdev_merge 28 | nbdev_trust=nbdev.clean:nbdev_trust 29 | nbdev_clean=nbdev.clean:nbdev_clean 30 | nbdev_install_hooks=nbdev.clean:nbdev_install_hooks 31 | nbdev_filter=nbdev.cli:nbdev_filter 32 | nbdev_sidebar=nbdev.quarto:nbdev_sidebar 33 | nbdev_test=nbdev.test:nbdev_test 34 | nbdev_new=nbdev.cli:nbdev_new 35 | nbdev_migrate=nbdev.migrate:nbdev_migrate 36 | nbdev_install_quarto=nbdev.quarto:install_quarto 37 | nbdev_install=nbdev.quarto:install 38 | nbdev_docs=nbdev.quarto:nbdev_docs 39 | nbdev_preview=nbdev.quarto:nbdev_preview 40 | nbdev_prepare=nbdev.quarto:prepare 41 | nbdev_readme=nbdev.quarto:nbdev_readme 42 | nbdev_release_gh=nbdev.release:release_gh 43 | nbdev_release_git=nbdev.release:release_git 44 | nbdev_changelog=nbdev.release:changelog 45 | nbdev_pypi=nbdev.release:release_pypi 46 | nbdev_conda=nbdev.release:release_conda 47 | nbdev_release_both=nbdev.release:release_both 48 | nbdev_bump_version=nbdev.release:nbdev_bump_version 49 | nbdev_proc_nbs=nbdev.quarto:nbdev_proc_nbs 50 | nbdev_help=nbdev.cli:chelp 51 | tst_flags = notest 52 | nbs_path = nbs 53 | doc_path = _docs 54 | recursive = True 55 | doc_host = https://nbdev.fast.ai 56 | doc_baseurl = / 57 | git_url = https://github.com/fastai/nbdev 58 | lib_path = nbdev 59 | title = nbdev 60 | black_formatting = False 61 | readme_nb = getting_started.ipynb 62 | allowed_metadata_keys = 63 | allowed_cell_metadata_keys = 64 | jupyter_hooks = True 65 | clean_ids = False 66 | clear_all = False 67 | put_version_in_init = True 68 | 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _publish.yml 2 | _quarto.yml 3 | _proc/ 4 | tests/settings.ini 5 | idx/ 6 | .install 7 | conda/ 8 | _site/ 9 | _docs/ 10 | test_settings.ini 11 | 12 | *.bak 13 | .gitattributes 14 | .last_checked 15 | .gitconfig 16 | *.bak 17 | *.log 18 | *~ 19 | ~* 20 | _tmp* 21 | tmp* 22 | tags 23 | 24 | # Byte-compiled / optimized / DLL files 25 | __pycache__/ 26 | *.py[cod] 27 | *$py.class 28 | 29 | # C extensions 30 | *.so 31 | 32 | # Distribution / packaging 33 | .Python 34 | env/ 35 | build/ 36 | develop-eggs/ 37 | dist/ 38 | downloads/ 39 | eggs/ 40 | .eggs/ 41 | lib/ 42 | lib64/ 43 | parts/ 44 | sdist/ 45 | var/ 46 | wheels/ 47 | *.egg-info/ 48 | .installed.cfg 49 | *.egg 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage.xml 69 | *.cover 70 | .hypothesis/ 71 | 72 | # Translations 73 | *.mo 74 | *.pot 75 | 76 | # Django stuff: 77 | *.log 78 | local_settings.py 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # celery beat schedule file 100 | celerybeat-schedule 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # dotenv 106 | .env 107 | 108 | # virtualenv 109 | .venv 110 | venv/ 111 | ENV/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | 126 | .vscode 127 | *.swp 128 | 129 | # osx generated files 130 | .DS_Store 131 | .DS_Store? 132 | .Trashes 133 | ehthumbs.db 134 | Thumbs.db 135 | .idea 136 | 137 | # pytest 138 | .pytest_cache 139 | 140 | # tools/trust-doc-nbs 141 | docs_src/.last_checked 142 | 143 | # symlinks to fastai 144 | docs_src/fastai 145 | tools/fastai 146 | 147 | # link checker 148 | checklink/cookies.txt 149 | 150 | # .gitconfig is now autogenerated 151 | .gitconfig 152 | 153 | -------------------------------------------------------------------------------- /tests/2020-01-14-test-markdown-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | toc: true 4 | layout: post 5 | description: A minimal example of using markdown with fastpages. 6 | categories: [markdown] 7 | title: An Example Markdown Post 8 | 9 | 10 | --- 11 | 12 | # Example Markdown Post 13 | 14 | ## Basic setup 15 | 16 | Jekyll requires blog post files to be named according to the following format: 17 | 18 | `YEAR-MONTH-DAY-filename.md` 19 | 20 | Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `filename` is whatever file name you choose, to remind yourself what this post is about. `.md` is the file extension for markdown files. 21 | 22 | The first line of the file should start with a single hash character, then a space, then your title. This is how you create a "*level 1 heading*" in markdown. Then you can create level 2, 3, etc headings as you wish but repeating the hash character, such as you see in the line `## File names` above. 23 | 24 | ## Basic formatting 25 | 26 | You can use *italics*, **bold**, `code font text`, and create [links](https://www.markdownguide.org/cheat-sheet/). Here's a footnote [^1]. Here's a horizontal rule: 27 | 28 | --- 29 | 30 | ## Lists 31 | 32 | Here's a list: 33 | 34 | - item 1 35 | - item 2 36 | 37 | And a numbered list: 38 | 39 | 1. item 1 40 | 1. item 2 41 | 42 | ## Boxes and stuff 43 | 44 | > This is a quotation 45 | 46 | {% include alert.html text="You can include alert boxes" %} 47 | 48 | ...and... 49 | 50 | {% include info.html text="You can include info boxes" %} 51 | 52 | ## Images 53 | 54 | ![]({{ site.baseurl }}/images/logo.png "fast.ai's logo") 55 | 56 | ## Code 57 | 58 | You can format text and code per usual 59 | 60 | General preformatted text: 61 | 62 | # Do a thing 63 | do_thing() 64 | 65 | Python code and output: 66 | 67 | ```python 68 | # Prints '2' 69 | print(1+1) 70 | ``` 71 | 72 | 2 73 | 74 | Formatting text as shell commands: 75 | 76 | ```shell 77 | echo "hello world" 78 | ./some_script.sh --option "value" 79 | wget https://example.com/cat_photo1.png 80 | ``` 81 | 82 | Formatting text as YAML: 83 | 84 | ```yaml 85 | key: value 86 | - another_key: "another value" 87 | ``` 88 | 89 | 90 | ## Tables 91 | 92 | | Column 1 | Column 2 | 93 | |-|-| 94 | | A thing | Another thing | 95 | 96 | 97 | ## Tweetcards 98 | 99 | {% twitter https://twitter.com/jakevdp/status/1204765621767901185?s=20 %} 100 | 101 | 102 | ## Footnotes 103 | 104 | 105 | 106 | [^1]: This is the footnote. 107 | 108 | -------------------------------------------------------------------------------- /nbdev/export.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/04_export.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['ExportModuleProc', 'black_format', 'nb_export'] 5 | 6 | # %% ../nbs/api/04_export.ipynb 2 7 | from .config import * 8 | from .maker import * 9 | from .imports import * 10 | from .process import * 11 | 12 | from fastcore.script import * 13 | from fastcore.basics import * 14 | from fastcore.imports import * 15 | 16 | from collections import defaultdict 17 | 18 | # %% ../nbs/api/04_export.ipynb 4 19 | class ExportModuleProc: 20 | "A processor which exports code to a module" 21 | def begin(self): self.modules,self.in_all = defaultdict(L),defaultdict(L) 22 | def _default_exp_(self, cell, exp_to): self.default_exp = exp_to 23 | def _exporti_(self, cell, exp_to=None): self.modules[ifnone(exp_to, '#')].append(cell) 24 | def _export_(self, cell, exp_to=None): 25 | self._exporti_(cell, exp_to) 26 | self.in_all[ifnone(exp_to, '#')].append(cell) 27 | _exports_=_export_ 28 | 29 | # %% ../nbs/api/04_export.ipynb 7 30 | def black_format(cell, # Cell to format 31 | force=False): # Turn black formatting on regardless of settings.ini 32 | "Processor to format code with `black`" 33 | try: cfg = get_config() 34 | except FileNotFoundError: return 35 | if (not cfg.black_formatting and not force) or cell.cell_type != 'code': return 36 | try: import black 37 | except: raise ImportError("You must install black: `pip install black` if you wish to use black formatting with nbdev") 38 | else: 39 | _format_str = partial(black.format_str, mode = black.Mode()) 40 | try: cell.source = _format_str(cell.source).strip() 41 | except: pass 42 | 43 | # %% ../nbs/api/04_export.ipynb 9 44 | def nb_export(nbname, lib_path=None, procs=black_format, debug=False, mod_maker=ModuleMaker, name=None): 45 | "Create module(s) from notebook" 46 | if lib_path is None: lib_path = get_config().lib_path 47 | exp = ExportModuleProc() 48 | nb = NBProcessor(nbname, [exp]+L(procs), debug=debug) 49 | nb.process() 50 | for mod,cells in exp.modules.items(): 51 | all_cells = exp.in_all[mod] 52 | nm = ifnone(name, getattr(exp, 'default_exp', None) if mod=='#' else mod) 53 | if not nm: 54 | warn(f"Notebook '{nbname}' uses `#|export` without `#|default_exp` cell.\n" 55 | "Note nbdev2 no longer supports nbdev1 syntax. Run `nbdev_migrate` to upgrade.\n" 56 | "See https://nbdev.fast.ai/getting_started.html for more information.") 57 | return 58 | mm = mod_maker(dest=lib_path, name=nm, nb_path=nbname, is_new=bool(name) or mod=='#') 59 | mm.make(cells, all_cells, lib_path=lib_path) 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools,shlex 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'), 18 | 'mit': ('MIT License', 'OSI Approved :: MIT License'), 19 | 'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'), 20 | 'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'), 21 | 'agpl3': ('GNU Affero General Public License v3', 'OSI Approved :: GNU Affero General Public License (AGPLv3)'), 22 | 'bsd3': ('BSD License', 'OSI Approved :: BSD License'), 23 | } 24 | statuses = [ '0 - Pre-Planning', '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 25 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 26 | py_versions = '3.7 3.8 3.9 3.10'.split() 27 | 28 | requirements = shlex.split(cfg.get('requirements', '')) 29 | if cfg.get('pip_requirements'): requirements += shlex.split(cfg.get('pip_requirements', '')) 30 | min_python = cfg['min_python'] 31 | lic = licenses.get(cfg['license'].lower(), (cfg['license'], None)) 32 | dev_requirements = (cfg.get('dev_requirements') or '').split() 33 | project_urls = {} 34 | if cfg.get('doc_host'): project_urls["Documentation"] = cfg['doc_host'] + cfg.get('doc_baseurl', '') 35 | 36 | setuptools.setup( 37 | name = cfg['lib_name'], 38 | license = lic[0], 39 | classifiers = [ 40 | 'Development Status :: ' + statuses[int(cfg['status'])], 41 | 'Intended Audience :: ' + cfg['audience'].title(), 42 | 'Natural Language :: ' + cfg['language'].title(), 43 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []), 44 | url = cfg['git_url'], 45 | packages = setuptools.find_packages(), 46 | include_package_data = True, 47 | install_requires = requirements, 48 | extras_require={ 'dev': dev_requirements }, 49 | dependency_links = cfg.get('dep_links','').split(), 50 | python_requires = '>=' + cfg['min_python'], 51 | long_description = open('README.md', encoding="utf8").read(), 52 | long_description_content_type = 'text/markdown', 53 | zip_safe = False, 54 | entry_points = { 55 | 'console_scripts': cfg.get('console_scripts','').split(), 56 | 'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'] 57 | }, 58 | project_urls = project_urls, 59 | **setup_cfg) 60 | 61 | -------------------------------------------------------------------------------- /nbs/images/circles.svg.py: -------------------------------------------------------------------------------- 1 | """--- 2 | ---""" 3 | 4 | # Original copyright 2022 Gram, MIT license; https://github.com/orsinium-labs/generative-art; Changes copyright Jeremy Howard 5 | 6 | import math,svg 7 | from random import randint,random,seed 8 | from typing import NamedTuple 9 | 10 | class Point(NamedTuple): 11 | x: float 12 | y: float 13 | def distance_to(self, p): 14 | dx = abs(self.x - p.x) 15 | dy = abs(self.y - p.y) 16 | return math.sqrt(dx ** 2 + dy ** 2) 17 | 18 | 19 | class Circle(NamedTuple): 20 | c: Point 21 | r: float 22 | def contains(self, point): return self.c.distance_to(point) <= self.r 23 | def min_distance(self, other): return abs(self.r - self.c.distance_to(other)) 24 | def render(self, stroke= "none", fill= "none"): 25 | return svg.Circle( cx=self.c.x, cy=self.c.y, r=self.r, stroke=stroke, stroke_width=1, fill=fill,) 26 | 27 | 28 | class Generator: 29 | size=800; min_shift=20; max_shift=60; min_width=20; min_circles=200; max_circles=300; min_radius=4 30 | def generate(self): return svg.SVG( width=self.size, height=self.size, xmlns='http://www.w3.org/2000/svg', elements=list(self.iter_elements()),) 31 | def color(self): return f'hsl({randint(0, 360)}, {randint(75, 100)}%, 50%)' 32 | 33 | def __init__(self): 34 | outer_r = self.size//2 35 | outer_center = Point(outer_r, outer_r) 36 | self.outer = Circle(c=outer_center, r=outer_r) 37 | 38 | def inner(self): 39 | inner_x = self.outer.r + randint(self.min_shift, self.max_shift) 40 | inner_y = self.outer.r + randint(self.min_shift, self.max_shift) 41 | inner_center = Point(inner_x, inner_y) 42 | r = self.outer.min_distance(inner_center) - self.min_width 43 | return Circle(c=inner_center, r=r) 44 | 45 | def iter_elements(self): 46 | assert self.inner().r < self.outer.r 47 | yield self.outer.render(fill="white") 48 | circles_count = randint(self.min_circles, self.max_circles) 49 | min_distance = math.ceil(self.inner().min_distance(Point(self.outer.r, self.outer.r))) 50 | circles = [self.inner()] 51 | for _ in range(circles_count): 52 | for _ in range(40): 53 | circle = self.get_random_circle(min_distance, circles) 54 | if circle is not None: 55 | circles.append(circle) 56 | yield circle.render(stroke=self.color()) 57 | break 58 | 59 | def get_random_circle(self, min_distance: int, circles: list): 60 | distance = randint(min_distance, math.floor(self.outer.r)) 61 | angle = random() * math.pi * 2 62 | cx = self.outer.r + math.cos(angle) * distance 63 | cy = self.outer.r + math.sin(angle) * distance 64 | center = Point(cx, cy) 65 | for other in circles: 66 | if other.contains(center): return None 67 | r = self.outer.min_distance(center) 68 | for other in circles: r = min(r, other.min_distance(center)) 69 | if r < self.min_radius: return None 70 | return Circle(c=center, r=r) 71 | 72 | seed(42) 73 | print(Generator().generate()) 74 | -------------------------------------------------------------------------------- /nbdev/sync.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/06_sync.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['absolute_import', 'nbdev_update'] 5 | 6 | # %% ../nbs/api/06_sync.ipynb 3 7 | from .imports import * 8 | from .config import * 9 | from .maker import * 10 | from .process import * 11 | from .process import _partition_cell 12 | from .export import * 13 | from .doclinks import _iter_py_cells 14 | 15 | from execnb.nbio import * 16 | from fastcore.script import * 17 | from fastcore.xtras import * 18 | 19 | import ast 20 | from importlib import import_module 21 | 22 | # %% ../nbs/api/06_sync.ipynb 5 23 | def absolute_import(name, fname, level): 24 | "Unwarps a relative import in `name` according to `fname`" 25 | if not level: return name 26 | mods = fname.split(os.path.sep) 27 | if not name: return '.'.join(mods) 28 | return '.'.join(mods[:len(mods)-level+1]) + f".{name}" 29 | 30 | # %% ../nbs/api/06_sync.ipynb 7 31 | @functools.lru_cache(maxsize=None) 32 | def _mod_files(): 33 | midx = import_module(f'{get_config().lib_path.name}._modidx') 34 | return L(files for mod in midx.d['syms'].values() for _,files in mod.values()).unique() 35 | 36 | # %% ../nbs/api/06_sync.ipynb 8 37 | _re_import = re.compile("from\s+\S+\s+import\s+\S") 38 | 39 | # %% ../nbs/api/06_sync.ipynb 10 40 | def _to_absolute(code, py_path, lib_dir): 41 | if not _re_import.search(code): return code 42 | res = update_import(code, ast.parse(code).body, str(py_path.relative_to(lib_dir).parent), absolute_import) 43 | return ''.join(res) if res else code 44 | 45 | # %% ../nbs/api/06_sync.ipynb 11 46 | def _update_nb(nb_path, cells, lib_dir): 47 | "Update notebook `nb_path` with contents from `cells`" 48 | nbp = NBProcessor(nb_path, ExportModuleProc(), rm_directives=False) 49 | nbp.process() 50 | for cell in cells: 51 | assert cell.nb_path == nb_path 52 | nbcell = nbp.nb.cells[cell.idx] 53 | dirs,_ = _partition_cell(nbcell, 'python') 54 | nbcell.source = ''.join(dirs) + _to_absolute(cell.code, cell.py_path, lib_dir) 55 | write_nb(nbp.nb, nb_path) 56 | 57 | # %% ../nbs/api/06_sync.ipynb 12 58 | def _update_mod(py_path, lib_dir): 59 | "Propagate changes from cells in module `py_path` to corresponding notebooks" 60 | py_cells = L(_iter_py_cells(py_path)).filter(lambda o: o.nb != 'auto') 61 | for nb_path,cells in groupby(py_cells, 'nb_path').items(): _update_nb(nb_path, cells, lib_dir) 62 | 63 | # %% ../nbs/api/06_sync.ipynb 14 64 | @call_parse 65 | def nbdev_update(fname:str=None): # A Python file name to update 66 | "Propagate change in modules matching `fname` to notebooks that created them" 67 | if fname and fname.endswith('.ipynb'): raise ValueError("`nbdev_update` operates on .py files. If you wish to convert notebooks instead, see `nbdev_export`.") 68 | if os.environ.get('IN_TEST',0): return 69 | cfg = get_config() 70 | fname = Path(fname or cfg.lib_path) 71 | lib_dir = cfg.lib_path.parent 72 | files = globtastic(fname, file_glob='*.py', skip_folder_re='^[_.]').filter(lambda x: str(Path(x).absolute().relative_to(lib_dir) in _mod_files())) 73 | files.map(_update_mod, lib_dir=lib_dir) 74 | -------------------------------------------------------------------------------- /nbs/explanations/why_nbdev.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ffd2b608-1808-4661-b29d-c01800ccac6d", 6 | "metadata": {}, 7 | "source": [ 8 | "# Why nbdev\n", 9 | "\n", 10 | "> Why we develop software with nbdev\n", 11 | "\n", 12 | "- order: 4" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "169c521b-1343-4ade-b978-47d7507e5b9a", 18 | "metadata": {}, 19 | "source": [ 20 | "Popular Python documentation standards such as [numpy docstrings](https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard) and [sphinx](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html) facilitate documentation of source code with docstrings, which are limited in their expressiveness and functionality. Notebooks, on the other hand, offer a richer medium for authoring documentation (with markdown and code cells) compared to docstrings, and unlock new ways of documenting your code that are otherwhise infeasible.\n", 21 | "\n", 22 | "Furthermore, traditional testing frameworks such as [pytest](https://docs.pytest.org/) and [unittest](https://docs.python.org/3/library/unittest.html) encourage writing tests in a separate context that is disjointed from the associated source code and documentation. With nbdev, you write tests in the same context as your source code and documentation, such that tests can optionally become part of the narrative within your documentation. \n", 23 | "\n", 24 | "Since nbdev allows your colleagues and users to view the tests, code and documentation in a single context with a REPL that invites experimentation, the way you author code, documentation and tests in nbdev are very different than traditional software development workflows." 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "3087ce55-ed59-4251-9dd2-a1a2a1968f98", 30 | "metadata": {}, 31 | "source": [ 32 | "In [Notebook Best Practices](/tutorials/best_practices.ipynb#putting-it-all-together-an-annotated-example) we compare a function coded, tested, and documented in nbdev versus ordinary .py files. Here are a few of the challenges we faced with other approaches that led us to using nbdev. In .py files:\n", 33 | "\n", 34 | "1. Docstrings repeat information that is already contained in the function signature, such as names of parameters, default values, and types\n", 35 | "2. Examples are manually entered. This requires the author to copy and paste both the code and outputs. Furthermore, the author must manually re-enter or change these examples anytime the code changes, which is an error-prone process\n", 36 | "3. Examples are limited to short code snippets and cannot contain rich output like plots or graphics\n", 37 | "4. Examples cannot have prose interleaved with code except for code comments\n", 38 | "5. Readers of your code must copy and paste contents of the docstring if they wish to reproduce the examples." 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | } 48 | }, 49 | "nbformat": 4, 50 | "nbformat_minor": 5 51 | } 52 | -------------------------------------------------------------------------------- /nbdev/serve.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/17_serve.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['proc_nbs'] 5 | 6 | # %% ../nbs/api/17_serve.ipynb 2 7 | import ast,subprocess,threading,sys 8 | from shutil import rmtree,copy2 9 | 10 | from fastcore.utils import * 11 | from fastcore.parallel import parallel 12 | from fastcore.script import call_parse 13 | from fastcore.meta import delegates 14 | 15 | from .config import get_config 16 | from .doclinks import nbglob_cli,nbglob 17 | from .processors import FilterDefaults 18 | import nbdev.serve_drv 19 | 20 | # %% ../nbs/api/17_serve.ipynb 4 21 | def _is_qpy(path:Path): 22 | "Is `path` a py script starting with frontmatter?" 23 | path = Path(path) 24 | if not path.suffix=='.py': return 25 | p = ast.parse(path.read_text()) 26 | # try: p = ast.parse(path.read_text()) 27 | # except: return 28 | if not p.body: return 29 | a = p.body[0] 30 | if isinstance(a, ast.Expr) and isinstance(a.value, ast.Constant): 31 | v = a.value.value.strip() 32 | vl = v.splitlines() 33 | if vl[0]=='---' and vl[-1]=='---': return '\n'.join(vl[1:-1]) 34 | 35 | # %% ../nbs/api/17_serve.ipynb 5 36 | def _proc_file(s, cache, path, mtime=None): 37 | skips = ('_proc', '_docs', '_site') 38 | if not s.is_file() or any(o[0]=='.' or o in skips for o in s.parts): return 39 | d = cache/s.relative_to(path) 40 | if s.suffix=='.py': d = d.with_suffix('') 41 | if d.exists(): 42 | dtime = d.stat().st_mtime 43 | if mtime: dtime = max(dtime, mtime) 44 | if s.stat().st_mtime<=dtime: return 45 | 46 | d.parent.mkdir(parents=True, exist_ok=True) 47 | if s.suffix=='.ipynb': return s,d,FilterDefaults 48 | md = _is_qpy(s) 49 | if md is not None: return s,d,md.strip() 50 | else: copy2(s,d) 51 | 52 | # %% ../nbs/api/17_serve.ipynb 7 53 | @delegates(nbglob_cli) 54 | def proc_nbs( 55 | path:str='', # Path to notebooks 56 | n_workers:int=defaults.cpus, # Number of workers 57 | force:bool=False, # Ignore cache and build all 58 | file_glob:str='', # Only include files matching glob 59 | file_re:str='', # Only include files matching glob 60 | **kwargs): 61 | "Process notebooks in `path` for docs rendering" 62 | cfg = get_config() 63 | cache = cfg.config_path/'_proc' 64 | path = Path(path or cfg.nbs_path) 65 | files = nbglob(path, func=Path, file_glob='', file_re='', **kwargs) 66 | if (path/'_quarto.yml').exists(): files.append(path/'_quarto.yml') 67 | if (path/'_extensions').exists(): files.extend(nbglob(path/'_extensions', func=Path, file_glob='', file_re='', skip_file_re='^[.]')) 68 | 69 | # If settings.ini or filter script newer than cache folder modified, delete cache 70 | chk_mtime = max(cfg.config_file.stat().st_mtime, Path(__file__).stat().st_mtime) 71 | cache.mkdir(parents=True, exist_ok=True) 72 | cache_mtime = cache.stat().st_mtime 73 | if force or (cache.exists and cache_mtime How to create nbdev powered docs without a library!\n", 11 | "- order: 9" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "504ef133-7e39-4ef8-a9d5-35a9ca6fbb9b", 17 | "metadata": {}, 18 | "source": [ 19 | "## Background\n", 20 | "\n", 21 | "While nbdev is great for authoring software, you may wish to utilize the power of nbdev for the purposes of documenting existing code, or **use various utilities of nbdev without having to write a python library**. For example, you can use the following features of nbdev without creating a python package:\n", 22 | "\n", 23 | "- Custom [nbdev directives](../explanations/directives.ipynb) such as [`#|hide_line`](../explanations/directives.ipynb#hide_line).\n", 24 | "- Testing with `nbdev_test`.\n", 25 | "- Automated entity linking with [doclinks](best_practices.ipynb#reference-related-symbols-with-doclinks).\n", 26 | "- Rendering API documentation with [docments and show_doc](best_practices.ipynb#document-parameters-with-docments).\n", 27 | "\n", 28 | "## Setup\n", 29 | "\n", 30 | "To setup a documentation only site, you can follow these steps:\n", 31 | "\n", 32 | "1. Create a nbdev repo the usual way, using `nbdev_new`\n", 33 | "2. Remove library files\n", 34 | "\n", 35 | "```bash\n", 36 | "rm setup.py .github/workflows/test.yaml nbs/00_core.ipynb\n", 37 | "```\n", 38 | "\n", 39 | "3. Remove your library folder (this will be the `lib_path` field in `settings.ini`):\n", 40 | "\n", 41 | "```bash\n", 42 | "rm -rf \n", 43 | "```\n", 44 | "\n", 45 | "## Usage\n", 46 | "\n", 47 | "After setting up your project, you can use various nbdev utilities per usual:\n", 48 | "\n", 49 | "- `nbdev_preview` for previewing your site\n", 50 | "- `nbdev_test` for testing your docs locally\n", 51 | "- Custom [nbdev directives](../explanations/directives.ipynb) will be available to you (but you must be careful not to use irrelevant ones like `#|export`).\n", 52 | "- If you created your nbdev docs site on GitHub, GitHub Actions will publish your docs for you automatically [as described here](../explanations/docs.ipynb#Deploying-Docs-With-GitHub-Actions).\n", 53 | "- You can publish your docs on other platforms as described [here](../explanations/docs.ipynb#Deploying-Your-Docs-On-Other-Platforms)." 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "id": "6c7c8ede-f3af-417d-8aa6-7658b5052288", 59 | "metadata": {}, 60 | "source": [ 61 | "## Demo\n", 62 | "\n", 63 | "A minimal example of a documentation-only site is located [here](https://github.com/hamelsmu/nolib_nbdev)." 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "60e597c1-9950-4927-ae5a-79fcc46497bd", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [] 73 | } 74 | ], 75 | "metadata": { 76 | "kernelspec": { 77 | "display_name": "Python 3 (ipykernel)", 78 | "language": "python", 79 | "name": "python3" 80 | } 81 | }, 82 | "nbformat": 4, 83 | "nbformat_minor": 5 84 | } 85 | -------------------------------------------------------------------------------- /tests/directives.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "23fe7353-8bac-4ac3-a7df-d80ff3186e0f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#|hide\n", 11 | " #| export" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "82d2db47-24d7-43ba-9a5a-d61af1a06b12", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "a = 'no directives'" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "id": "183e982d-7f38-417b-b774-ccadd6b76e2e", 27 | "metadata": {}, 28 | "source": [ 29 | "some md" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "id": "20b94777-1b99-4a0f-bf76-766343470047", 36 | "metadata": {}, 37 | "outputs": [ 38 | { 39 | "ename": "AssertionError", 40 | "evalue": "", 41 | "output_type": "error", 42 | "traceback": [ 43 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 44 | "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", 45 | "\u001b[0;32m/var/folders/jj/xl1rktcs6mn7ms6b8vvx8k4r0000gn/T/ipykernel_38804/2756147105.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m#|eval: false\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 46 | "\u001b[0;31mAssertionError\u001b[0m: " 47 | ] 48 | } 49 | ], 50 | "source": [ 51 | "#|eval: false\n", 52 | "assert 1 == 2" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "d4b7b3db-6831-46b7-aa3f-4e6cc0bc0a24", 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "ename": "AssertionError", 63 | "evalue": "", 64 | "output_type": "error", 65 | "traceback": [ 66 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 67 | "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", 68 | "\u001b[0;32m/var/folders/jj/xl1rktcs6mn7ms6b8vvx8k4r0000gn/T/ipykernel_38804/441260613.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m#| notest\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 69 | "\u001b[0;31mAssertionError\u001b[0m: " 70 | ] 71 | } 72 | ], 73 | "source": [ 74 | "#| notest\n", 75 | "assert 1 == 2" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "id": "be48928b-20cf-4c18-be86-a6350a80e443", 81 | "metadata": {}, 82 | "source": [ 83 | "- more md" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "id": "c87f288d-b021-4104-b436-1b7aab1a9e42", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [] 93 | } 94 | ], 95 | "metadata": { 96 | "kernelspec": { 97 | "display_name": "Python 3 (ipykernel)", 98 | "language": "python", 99 | "name": "python3" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 5 104 | } 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | Examples of unacceptable behavior by participants include: 13 | 14 | * The use of sexualized language or imagery and unwelcome sexual attention or 15 | advances 16 | * Trolling, insulting/derogatory comments, and personal or political attacks 17 | * Public or private harassment 18 | * Publishing others' private information, such as a physical or electronic 19 | address, without explicit permission 20 | 21 | These examples of unacceptable behaviour are requirements; we will not allow them 22 | in any fast.ai project, including this one. 23 | 24 | ## Our Standards 25 | 26 | Examples of behavior that contributes to creating a positive environment 27 | include: 28 | 29 | * Using welcoming and inclusive language 30 | * Being respectful of differing viewpoints and experiences 31 | * Gracefully accepting constructive criticism 32 | * Focusing on what is best for the community 33 | * Showing empathy towards other community members 34 | 35 | These examples are shown only to help you participate effectively -- they are not 36 | requirements, just requests and guidance. 37 | 38 | ## Our Responsibilities 39 | 40 | Project maintainers are responsible for clarifying the standards of acceptable 41 | behavior and are expected to take appropriate and fair corrective action in 42 | response to any instances of unacceptable behavior. 43 | 44 | Project maintainers have the right and responsibility to remove, edit, or 45 | reject comments, commits, code, wiki edits, issues, and other contributions 46 | that are not aligned to this Code of Conduct, or to ban temporarily or 47 | permanently any contributor for other behaviors that they deem inappropriate, 48 | threatening, offensive, or harmful. 49 | 50 | ## Scope 51 | 52 | This Code of Conduct applies both within project spaces and in public spaces 53 | when an individual is representing the project or its community. Examples of 54 | representing a project or community include using an official project e-mail 55 | address, posting via an official social media account or acting as an appointed 56 | representative at an online or offline event. Representation of a project may be 57 | further defined and clarified by project maintainers. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing or otherwise unacceptable behavior may be 62 | reported by contacting the project team at info@fast.ai. All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | -------------------------------------------------------------------------------- /tests/showdoc_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "c2ac41fe-6c3c-48c1-9531-490927859346", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from nbdev.showdoc import show_doc\n", 11 | "from fastcore.utils import patch\n", 12 | "from fastcore.meta import delegates" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "f67bf2b2-88cf-446b-9241-f1351b129739", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "#|export\n", 23 | "class Foo:\n", 24 | " def __init__(self,\n", 25 | " a, # A thing\n", 26 | " b:int=None): # Another thing\n", 27 | " ..." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "id": "1f970abb-dfd0-41f0-9610-7e8b8ecdd552", 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "data": { 38 | "text/markdown": [ 39 | "---\n", 40 | "\n", 41 | "### Foo\n", 42 | "\n", 43 | "> Foo (a, b:int=None)\n", 44 | "\n", 45 | "Initialize self. See help(type(self)) for accurate signature.\n", 46 | "\n", 47 | "| | **Type** | **Default** | **Details** |\n", 48 | "| -- | -------- | ----------- | ----------- |\n", 49 | "| a | | | A thing |\n", 50 | "| b | int | None | Another thing |" 51 | ], 52 | "text/plain": [ 53 | "" 54 | ] 55 | }, 56 | "execution_count": null, 57 | "metadata": {}, 58 | "output_type": "execute_result" 59 | } 60 | ], 61 | "source": [ 62 | "show_doc(Foo)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "49cdb1d8-579f-4d3a-a93e-e52a35430810", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "#|export\n", 73 | "@patch\n", 74 | "def a_method(self:Foo, a:list,b:dict,c):\n", 75 | " \"This is a method\"\n", 76 | " ..." 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "id": "f8be1e1a", 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "#|export\n", 87 | "@delegates()\n", 88 | "class B(Foo):\n", 89 | " def __init__(self, c, d:str=\"a\", **kwargs): ..." 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "id": "87d056c5", 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "data": { 100 | "text/markdown": [ 101 | "---\n", 102 | "\n", 103 | "### B\n", 104 | "\n", 105 | "> B (c, d:str='a', b:int=None)\n", 106 | "\n", 107 | "Initialize self. See help(type(self)) for accurate signature." 108 | ], 109 | "text/plain": [ 110 | "" 111 | ] 112 | }, 113 | "execution_count": null, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "show_doc(B)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "id": "e3c091c2", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [] 129 | } 130 | ], 131 | "metadata": { 132 | "kernelspec": { 133 | "display_name": "Python 3 (ipykernel)", 134 | "language": "python", 135 | "name": "python3" 136 | } 137 | }, 138 | "nbformat": 4, 139 | "nbformat_minor": 5 140 | } 141 | -------------------------------------------------------------------------------- /nbs/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #33373F; 3 | --background-2: #41454E; 4 | --foreground: white; 5 | --foreground-2: #484d54; 6 | --content-width: 1000px; 7 | } 8 | 9 | html { 10 | font-family: Inter, SF Pro Display, -apple-system, sans-serif; 11 | } 12 | 13 | body { 14 | background: var(--background); 15 | color: var(--foreground); 16 | text-align: center; 17 | } 18 | 19 | #title-block-header { 20 | display: none; 21 | } 22 | 23 | .navbar { 24 | background: var(--background); 25 | text-align: left; 26 | font-size: 16px; 27 | } 28 | 29 | .title, .navbar-title { 30 | font-weight: bold; 31 | padding: 4px 8px; 32 | border-radius: 8px; 33 | margin-right: 4px; 34 | } 35 | 36 | .navbar-title { 37 | background: linear-gradient(240.01deg, #4B555B 15.29%, rgba(67, 78, 84, 0) 138.75%); 38 | } 39 | 40 | .title { 41 | background: linear-gradient(240.01deg, #78858C 15.29%, rgba(105, 117, 123, 0) 138.75%); 42 | } 43 | 44 | .content-block { 45 | margin-left: 30px; 46 | margin-right: 30px; 47 | overflow-x: hidden; 48 | } 49 | 50 | @media(min-width: 900px) { 51 | .content-block { 52 | margin-left: 50px; 53 | margin-right: 50px; 54 | } 55 | } 56 | 57 | @media(min-width: 1200px) { 58 | .content-block { 59 | max-width: var(--content-width); 60 | margin-left: auto; 61 | margin-right: auto; 62 | } 63 | } 64 | 65 | .hero-banner { 66 | padding-top: 20px; 67 | padding-bottom: 10px; 68 | margin-bottom: 30px; 69 | display: flex; 70 | flex-direction: column; 71 | align-items: center; 72 | } 73 | 74 | .hero-banner h1 { 75 | font-size: 56px; 76 | font-weight: bold; 77 | margin-top: 10px; 78 | margin-bottom: 40px; 79 | } 80 | 81 | .hero-banner h2 { 82 | border: none; 83 | font-size: 40px; 84 | font-weight: bold; 85 | margin-top: 10px; 86 | margin-bottom: 40px; 87 | } 88 | 89 | .hero-banner h3 { 90 | color: var(--foreground); 91 | font-size: 20px; 92 | max-width: 90%; 93 | margin-left: auto; 94 | margin-right: auto; 95 | margin-top: 0; 96 | margin-bottom: 40px; 97 | } 98 | 99 | @media(min-width: 900px) { 100 | .hero-banner h3 { 101 | max-width: 60%; 102 | } 103 | } 104 | 105 | @media(min-width: 1200px) { 106 | .hero-banner h3 { 107 | max-width: 75%; 108 | } 109 | } 110 | 111 | .mid-content { 112 | width: 100%; 113 | padding: 0; 114 | padding-top: 80px !important; 115 | padding-bottom: 80px; 116 | background: linear-gradient(235.87deg, #656A75 12.29%, #33373F 107.53%); 117 | overflow-x: hidden; 118 | } 119 | 120 | .testimonial { 121 | background: rgba(51, 55, 63, 0.4); 122 | border-radius: 12px; 123 | padding: 30px; 124 | text-align: left; 125 | } 126 | 127 | .testimonial p { 128 | margin: 0; 129 | padding: 0; 130 | } 131 | 132 | .testimonial img { 133 | margin: 0; 134 | padding: 0; 135 | display: inline; 136 | float: left; 137 | width: 50px; 138 | margin-right: 20px; 139 | border-radius: 8px; 140 | } 141 | 142 | .testimonial h1 { 143 | font-size: 16px; 144 | font-weight: bold; 145 | padding: 0; 146 | margin: 0; 147 | margin-bottom: 4px; 148 | } 149 | 150 | .testimonial h2 { 151 | border: none; 152 | font-size: 16px; 153 | padding: 0; 154 | margin: 0; 155 | color: #A2A8B4; 156 | } 157 | 158 | .testimonial h3 { 159 | margin: 0; 160 | margin-top: 30px; 161 | font-size: 16px; 162 | } 163 | 164 | .feature { 165 | font-size: 16px; 166 | height: 210px; 167 | background: var(--background-2); 168 | border-radius: 12px; 169 | padding: 30px; 170 | text-align: left; 171 | display: flex; 172 | flex-direction: column; 173 | } 174 | 175 | .feature img { 176 | margin-bottom: 20px; 177 | width: 48px; 178 | } 179 | 180 | .footer { 181 | color: #A2A8B4; 182 | font-size: 16px; 183 | margin-top: 40px; 184 | padding-top: 30px !important; 185 | border-top: 1px solid var(--foreground-2); 186 | } 187 | 188 | .btn-action-primary { 189 | color: #F9FAFB; 190 | background: #009AF1; 191 | font-weight: 700; 192 | line-height: 20px; /* identical to box height */ 193 | } 194 | 195 | .btn-action-primary:hover, .btn-action-primary:active, .btn-action-primary:focus { 196 | background: #35acf0; 197 | } 198 | 199 | .btn-action { 200 | min-width: 165px; 201 | border-radius: 8px; 202 | padding: 20px 48px; 203 | border: none; 204 | } 205 | 206 | /* Below are needed to make .ipynb equivalent to .qmd */ 207 | 208 | .figure { 209 | display: inline; 210 | } 211 | 212 | .quarto-figure { 213 | margin: 0; 214 | } 215 | -------------------------------------------------------------------------------- /nbs/images/transform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /nbdev/cli.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/13_cli.ipynb. 2 | 3 | # %% ../nbs/api/13_cli.ipynb 2 4 | from __future__ import annotations 5 | import warnings 6 | 7 | from .config import * 8 | from .process import * 9 | from .processors import * 10 | from .doclinks import * 11 | from .test import * 12 | from .clean import * 13 | from .quarto import nbdev_readme, refresh_quarto_yml 14 | from .frontmatter import FrontmatterProc 15 | 16 | from execnb.nbio import * 17 | from fastcore.meta import * 18 | from fastcore.utils import * 19 | from fastcore.script import * 20 | from fastcore.style import S 21 | from fastcore.shutil import rmtree,move 22 | 23 | from urllib.error import HTTPError 24 | from contextlib import redirect_stdout 25 | import os, tarfile, sys 26 | 27 | # %% auto 0 28 | __all__ = ['nbdev_filter', 'extract_tgz', 'nbdev_new', 'chelp'] 29 | 30 | # %% ../nbs/api/13_cli.ipynb 5 31 | @call_parse 32 | def nbdev_filter( 33 | nb_txt:str=None, # Notebook text (uses stdin if not provided) 34 | fname:str=None, # Notebook to read (uses `nb_txt` if not provided) 35 | printit:bool_arg=True, # Print to stdout? 36 | ): 37 | "A notebook filter for Quarto" 38 | os.environ["IN_TEST"] = "1" 39 | try: filt = globals()[get_config().get('exporter', 'FilterDefaults')]() 40 | except FileNotFoundError: filt = FilterDefaults() 41 | if fname: nb_txt = Path(fname).read_text() 42 | elif not nb_txt: nb_txt = sys.stdin.read() 43 | nb = dict2nb(loads(nb_txt)) 44 | if printit: 45 | with open(os.devnull, 'w') as dn: 46 | with redirect_stdout(dn): filt(nb) 47 | else: filt(nb) 48 | res = nb2str(nb) 49 | del os.environ["IN_TEST"] 50 | if printit: print(res, flush=True) 51 | else: return res 52 | 53 | # %% ../nbs/api/13_cli.ipynb 8 54 | def extract_tgz(url, dest='.'): 55 | from fastcore.net import urlopen 56 | with urlopen(url) as u: tarfile.open(mode='r:gz', fileobj=u).extractall(dest) 57 | 58 | # %% ../nbs/api/13_cli.ipynb 9 59 | def _render_nb(fn, cfg): 60 | "Render templated values like `{{lib_name}}` in notebook at `fn` from `cfg`" 61 | txt = fn.read_text() 62 | txt = txt.replace('from your_lib.core', f'from {cfg.lib_path}.core') # for compatibility with old templates 63 | for k,v in cfg.d.items(): txt = txt.replace('{{'+k+'}}', v) 64 | fn.write_text(txt) 65 | 66 | # %% ../nbs/api/13_cli.ipynb 10 67 | def _update_repo_meta(cfg): 68 | "Enable gh pages and update the homepage and description in your GitHub repo." 69 | token=os.getenv('GITHUB_TOKEN') 70 | if token: 71 | from ghapi.core import GhApi 72 | api = GhApi(owner=cfg.user, repo=cfg.repo, token=token) 73 | try: api.repos.update(homepage=f'{cfg.doc_host}{cfg.doc_baseurl}', description=cfg.description) 74 | except HTTPError:print(f"Could not update the description & URL on the repo: {cfg.user}/{cfg.repo} using $GITHUB_TOKEN.\n" 75 | "Use a token with the correction permissions or perform these steps manually.") 76 | 77 | # %% ../nbs/api/13_cli.ipynb 11 78 | @call_parse 79 | @delegates(nbdev_create_config) 80 | def nbdev_new(**kwargs): 81 | "Create an nbdev project." 82 | from ghapi.core import GhApi 83 | nbdev_create_config.__wrapped__(**kwargs) 84 | cfg = get_config() 85 | _update_repo_meta(cfg) 86 | 87 | path = Path() 88 | with warnings.catch_warnings(): 89 | warnings.simplefilter('ignore', UserWarning) 90 | tag = GhApi(gh_host='https://api.github.com', authenticate=False).repos.get_latest_release('fastai', 'nbdev-template').tag_name 91 | url = f"https://github.com/fastai/nbdev-template/archive/{tag}.tar.gz" 92 | extract_tgz(url) 93 | tmpl_path = path/f'nbdev-template-{tag}' 94 | 95 | cfg.nbs_path.mkdir(exist_ok=True) 96 | nbexists = bool(first(cfg.nbs_path.glob('*.ipynb'))) 97 | _nbs_path_sufs = ('.ipynb','.css') 98 | for o in tmpl_path.ls(): 99 | p = cfg.nbs_path if o.suffix in _nbs_path_sufs else path 100 | if o.name == '_quarto.yml': continue 101 | if o.name == 'index.ipynb': _render_nb(o, cfg) 102 | if o.name == '00_core.ipynb' and not nbexists: move(o, p) 103 | elif not (path/o.name).exists(): move(o, p) 104 | rmtree(tmpl_path) 105 | 106 | refresh_quarto_yml() 107 | 108 | nbdev_export.__wrapped__() 109 | nbdev_readme.__wrapped__() 110 | 111 | # %% ../nbs/api/13_cli.ipynb 15 112 | @call_parse 113 | def chelp(): 114 | "Show help for all console scripts" 115 | from fastcore.xtras import console_help 116 | console_help('nbdev') 117 | -------------------------------------------------------------------------------- /nbdev/merge.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/07_merge.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['conf_re', 'unpatch', 'nbdev_fix', 'nbdev_merge'] 5 | 6 | # %% ../nbs/api/07_merge.ipynb 2 7 | from .imports import * 8 | from .config import * 9 | from .export import * 10 | from .sync import * 11 | 12 | from execnb.nbio import * 13 | from fastcore.script import * 14 | from fastcore import shutil 15 | 16 | import subprocess 17 | from difflib import SequenceMatcher 18 | 19 | # %% ../nbs/api/07_merge.ipynb 16 20 | _BEG,_MID,_END = '<'*7,'='*7,'>'*7 21 | conf_re = re.compile(rf'^{_BEG}\s+(\S+)\n(.*?)^{_MID}\n(.*?)^{_END}\s+([\S ]+)\n', re.MULTILINE|re.DOTALL) 22 | 23 | def _unpatch_f(before, cb1, cb2, c, r): 24 | if cb1 is not None and cb1 != cb2: raise Exception(f'Branch mismatch: {cb1}/{cb2}') 25 | r.append(before) 26 | r.append(c) 27 | return cb2 28 | 29 | # %% ../nbs/api/07_merge.ipynb 17 30 | def unpatch(s:str): 31 | "Takes a string with conflict markers and returns the two original files, and their branch names" 32 | *main,last = conf_re.split(s) 33 | r1,r2,c1b,c2b = [],[],None,None 34 | for before,c1_branch,c1,c2,c2_branch in chunked(main, 5): 35 | c1b = _unpatch_f(before, c1b, c1_branch, c1, r1) 36 | c2b = _unpatch_f(before, c2b, c2_branch, c2, r2) 37 | return ''.join(r1+[last]), ''.join(r2+[last]), c1b, c2b 38 | 39 | # %% ../nbs/api/07_merge.ipynb 22 40 | def _make_md(code): return [dict(source=f'`{code}`', cell_type="markdown", metadata={})] 41 | def _make_conflict(a,b, branch1, branch2): 42 | return _make_md(f'{_BEG} {branch1}') + a+_make_md(_MID)+b + _make_md(f'{_END} {branch2}') 43 | 44 | def _merge_cells(a, b, brancha, branchb, theirs): 45 | matches = SequenceMatcher(None, a, b).get_matching_blocks() 46 | res,prev_sa,prev_sb,conflict = [],0,0,False 47 | for sa,sb,sz in matches: 48 | ca,cb = a[prev_sa:sa],b[prev_sb:sb] 49 | if ca or cb: 50 | res += _make_conflict(ca, cb, brancha, branchb) 51 | conflict = True 52 | if sz: res += b[sb:sb+sz] if theirs else a[sa:sa+sz] 53 | prev_sa,prev_sb = sa+sz,sb+sz 54 | return res,conflict 55 | 56 | # %% ../nbs/api/07_merge.ipynb 23 57 | @call_parse 58 | def nbdev_fix(nbname:str, # Notebook filename to fix 59 | outname:str=None, # Filename of output notebook (defaults to `nbname`) 60 | nobackup:bool_arg=True, # Do not backup `nbname` to `nbname`.bak if `outname` not provided 61 | theirs:bool=False, # Use their outputs and metadata instead of ours 62 | noprint:bool=False): # Do not print info about whether conflicts are found 63 | "Create working notebook from conflicted notebook `nbname`" 64 | nbname = Path(nbname) 65 | if not nobackup and not outname: shutil.copy(nbname, nbname.with_suffix('.ipynb.bak')) 66 | nbtxt = nbname.read_text() 67 | a,b,branch1,branch2 = unpatch(nbtxt) 68 | ac,bc = dict2nb(loads(a)),dict2nb(loads(b)) 69 | dest = bc if theirs else ac 70 | cells,conflict = _merge_cells(ac.cells, bc.cells, branch1, branch2, theirs=theirs) 71 | dest.cells = cells 72 | write_nb(dest, ifnone(outname, nbname)) 73 | if not noprint: 74 | if conflict: print("One or more conflict remains in the notebook, please inspect manually.") 75 | else: print("Successfully merged conflicts!") 76 | return conflict 77 | 78 | # %% ../nbs/api/07_merge.ipynb 27 79 | def _git_branch_merge(): 80 | try: return only(v for k,v in os.environ.items() if k.startswith('GITHEAD')) 81 | except ValueError: return 82 | 83 | # %% ../nbs/api/07_merge.ipynb 28 84 | def _git_rebase_head(): 85 | for d in ('apply','merge'): 86 | d = Path(f'.git/rebase-{d}') 87 | if d.is_dir(): 88 | cmt = (d/'orig-head').read_text() 89 | msg = run(f'git show-branch --no-name {cmt}') 90 | return f'{cmt[:7]} ({msg})' 91 | 92 | # %% ../nbs/api/07_merge.ipynb 29 93 | def _git_merge_file(base, ours, theirs): 94 | "`git merge-file` with expected labels depending on if a `merge` or `rebase` is in-progress" 95 | l_theirs = _git_rebase_head() or _git_branch_merge() or 'THEIRS' 96 | cmd = f"git merge-file -L HEAD -L BASE -L '{l_theirs}' {ours} {base} {theirs}" 97 | return subprocess.run(cmd, shell=True, capture_output=True, text=True) 98 | 99 | # %% ../nbs/api/07_merge.ipynb 30 100 | @call_parse 101 | def nbdev_merge(base:str, ours:str, theirs:str, path:str): 102 | "Git merge driver for notebooks" 103 | if not _git_merge_file(base, ours, theirs).returncode: return 104 | theirs = str2bool(os.environ.get('THEIRS', False)) 105 | return nbdev_fix.__wrapped__(ours, theirs=theirs) 106 | -------------------------------------------------------------------------------- /nbdev/test.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/12_test.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['test_nb', 'nbdev_test'] 5 | 6 | # %% ../nbs/api/12_test.ipynb 2 7 | import time,os,sys,traceback,contextlib, inspect 8 | from fastcore.basics import * 9 | from fastcore.imports import * 10 | from fastcore.foundation import * 11 | from fastcore.parallel import * 12 | from fastcore.script import * 13 | from fastcore.meta import delegates 14 | 15 | from .config import * 16 | from .doclinks import * 17 | from .process import NBProcessor, nb_lang 18 | from .frontmatter import FrontmatterProc 19 | 20 | from execnb.nbio import * 21 | from execnb.shell import * 22 | 23 | # %% ../nbs/api/12_test.ipynb 3 24 | def test_nb(fn, # file name of notebook to test 25 | skip_flags=None, # list of flags marking cells to skip 26 | force_flags=None, # list of flags marking cells to always run 27 | do_print=False, # print completion? 28 | showerr=True, # print errors to stderr? 29 | basepath=None): # path to add to sys.path 30 | "Execute tests in notebook in `fn` except those with `skip_flags`" 31 | if basepath: sys.path.insert(0, str(basepath)) 32 | if not IN_NOTEBOOK: os.environ["IN_TEST"] = '1' 33 | flags=set(L(skip_flags)) - set(L(force_flags)) 34 | nb = NBProcessor(fn, procs=FrontmatterProc, process=True).nb 35 | fm = getattr(nb, 'frontmatter_', {}) 36 | if str2bool(fm.get('skip_exec', False)) or nb_lang(nb) != 'python': return True, 0 37 | 38 | def _no_eval(cell): 39 | if cell.cell_type != 'code': return True 40 | if 'nbdev_export'+'(' in cell.source: return True 41 | direc = getattr(cell, 'directives_', {}) or {} 42 | if direc.get('eval:', [''])[0].lower() == 'false': return True 43 | return flags & direc.keys() 44 | 45 | start = time.time() 46 | k = CaptureShell(fn) 47 | if do_print: print(f'Starting {fn}') 48 | try: 49 | with working_directory(fn.parent): 50 | k.run_all(nb, exc_stop=True, preproc=_no_eval) 51 | res = True 52 | except: 53 | if showerr: sys.stderr.write(k.prettytb(fname=fn)+'\n') 54 | res=False 55 | if do_print: print(f'- Completed {fn}') 56 | return res,time.time()-start 57 | 58 | # %% ../nbs/api/12_test.ipynb 8 59 | def _keep_file(p:Path, # filename for which to check for `indicator_fname` 60 | ignore_fname:str # filename that will result in siblings being ignored 61 | ) -> bool: 62 | "Returns False if `indicator_fname` is a sibling to `fname` else True" 63 | if p.exists(): return not bool(p.parent.ls().attrgot('name').filter(lambda x: x == ignore_fname)) 64 | else: True 65 | 66 | # %% ../nbs/api/12_test.ipynb 10 67 | @call_parse 68 | @delegates(nbglob_cli) 69 | def nbdev_test( 70 | path:str=None, # A notebook name or glob to test 71 | flags:str='', # Space separated list of test flags to run that are normally ignored 72 | n_workers:int=None, # Number of workers 73 | timing:bool=False, # Time each notebook to see which are slow 74 | do_print:bool=False, # Print start and end of each notebook 75 | pause:float=0.01, # Pause time (in seconds) between notebooks to avoid race conditions 76 | ignore_fname:str='.notest', # Filename that will result in siblings being ignored 77 | **kwargs): 78 | "Test in parallel notebooks matching `path`, passing along `flags`" 79 | skip_flags = get_config().tst_flags.split() 80 | force_flags = flags.split() 81 | files = nbglob(path, as_path=True, **kwargs) 82 | files = [f.absolute() for f in sorted(files) if _keep_file(f, ignore_fname)] 83 | if len(files)==0: return print('No files were eligible for testing') 84 | 85 | if n_workers is None: n_workers = 0 if len(files)==1 else min(num_cpus(), 8) 86 | if IN_NOTEBOOK: kw = {'method':'spawn'} if os.name=='nt' else {'method':'forkserver'} 87 | else: kw = {} 88 | with working_directory(get_config().nbs_path): 89 | results = parallel(test_nb, files, skip_flags=skip_flags, force_flags=force_flags, n_workers=n_workers, 90 | basepath=get_config().config_path, pause=pause, do_print=do_print, **kw) 91 | passed,times = zip(*results) 92 | if all(passed): print("Success.") 93 | else: 94 | _fence = '='*50 95 | failed = '\n\t'.join(f.name for p,f in zip(passed,files) if not p) 96 | sys.stderr.write(f"\nnbdev Tests Failed On The Following Notebooks:\n{_fence}\n\t{failed}\n") 97 | sys.exit(1) 98 | if timing: 99 | for i,t in sorted(enumerate(times), key=lambda o:o[1], reverse=True): print(f"{files[i].name}: {int(t)} secs") 100 | -------------------------------------------------------------------------------- /nbs/tutorials/renderscript.qmd.py: -------------------------------------------------------------------------------- 1 | """--- 2 | title: RenderScripts 3 | description: Introduction to RenderScripts -- create web pages using python 4 | order: 3 5 | ---""" 6 | 7 | from nbdev import qmd 8 | def im(fn, width, **kw): return qmd.img(f"/images/{fn}", width=f"{width}%", **kw) 9 | 10 | print("""*RenderScripts* are regular Python scripts, except that: 11 | 12 | - Rather than just having the extension `.py`, they have an extension like `.qmd.py` 13 | - They contain a module docstring containing [frontmatter](https://quarto.org/docs/tools/jupyter-lab.html#yaml-front-matter) (i.e three hyphens on a line, then some yaml, then another three hyphens on a line). 14 | 15 | These scripts are run when your site is rendered. Anything that they print to stdout becomes a new file in your site. The name of the file is the same as the name of the .py script, but without the `.py` extension. For instance, the page you're reading right now page is created by a script called `renderscript.qmd.py`, which you'll [find here](https://github.com/fastai/nbdev/blob/master/nbs/tutorials/renderscript.qmd.py). 16 | 17 | Hot/live reloading even works with these .py scripts -- so as soon as you save the script, you'll see the new output in your web browser. 18 | 19 | This approach can be particularly helpful for generating data-driven documents. For instance, consider this table, containing a list of the people with testimonials on nbdev's home page: 20 | 21 | """) 22 | 23 | testimonials = [ 24 | ('chris-lattner.png', 'Chris Lattner', 'Inventor of Swift and LLVM'), 25 | ('fernando-pérez.jpeg', 'Fernando Pérez', 'Creator of Jupyter'), 26 | ('david-berg.jpeg', 'David Berg', 'Software Engineer, Netflix'), 27 | ('erik-gaasedelen.jpeg', 'Erik Gaasedelen', 'Software Engineer, Lyft'), 28 | ('roxanna-pourzand.jpeg', 'Roxanna Pourzand', 'Product Manager, Transform'), 29 | ('hugo-bowne-anderson.jpeg', 'Hugo Bowne-Anderson', 'Head of Developer Relations, Outerbounds') 30 | ] 31 | print(qmd.tbl_row(['','Name','Position'])) 32 | print(qmd.tbl_sep([1,3,4])) 33 | for fname,name,position in testimonials: 34 | print(qmd.tbl_row([im(fname, 60), name, position])) 35 | 36 | print(""" 37 | 38 | When creating a table like this, it can be tricky to ensure that markdown is correct and consistent for every row. It can be easier and more maintainable to programatically generate it. The table above is generated from the following python list: 39 | 40 | ::: {.column-screen-inset-right} 41 | ```python 42 | testimonials = [ 43 | ('chris-lattner.png', 'Chris Lattner', 'Inventor of Swift and LLVM'), 44 | ('fernando-pérez.jpeg', 'Fernando Pérez', 'Creator of Jupyter'), 45 | ('david-berg.jpeg', 'David Berg', 'Software Engineer, Netflix'), 46 | ('erik-gaasedelen.jpeg', 'Erik Gaasedelen', 'Software Engineer, Lyft'), 47 | ('roxanna-pourzand.jpeg', 'Roxanna Pourzand', 'Product Manager, Transform'), 48 | ('hugo-bowne-anderson.jpeg', 'Hugo Bowne-Anderson', 'Head of Developer Relations, Outerbounds') 49 | ] 50 | ``` 51 | ::: 52 | 53 | To produce the table from this python list, the following four lines of code are used: 54 | 55 | ```python 56 | print(qmd.tbl_row(['','Name','Position'])) 57 | print(qmd.tbl_sep([1,3,4])) 58 | for fname,name,position in testimonials: 59 | print(qmd.tbl_row([im(fname, 60), name, position])) 60 | ``` 61 | 62 | [`tbl_hdr`](https://nbdev.fast.ai/api/qmd.html#tbl_hdr) and [`tbl_row`](https://nbdev.fast.ai/api/qmd.html#tbl_row) are two functions imported from the module `nbdev.qmd`. [`nbdev.qmd`](https://nbdev.fast.ai/api/qmd.html) is a small module that has some convenient functions for creating `.qmd` documents, such as the table creation functions used above. You can see more examples of their use in [index.qmd.py](https://github.com/fastai/nbdev/blob/master/nbs/index.qmd.py), which is the RenderScript which creates the [nbdev home page](https://nbdev.fast.ai). The nbdev home page is a more idiomatic example of how to use RenderScripts than the current page's source code -- we're only using RenderScript for the current page to provide a more simple example. In practice, we find that RenderScripts are best used for pages containing a lot of data-driven content, reusable components, and so forth. 63 | 64 | You can use RenderScripts to create any kind of file. For instance, the SVG below is created dynamically using [this script](https://github.com/fastai/nbdev/blob/master/nbs/images/circles.svg.py): 65 | 66 | """) 67 | 68 | print(im('circles.svg', 40)) 69 | 70 | print(""" 71 | Once you've run `nbdev_preview` or `nbdev_docs` you'll find your rendered document in the `_proc` directory, along with all of your processed notebooks. This can be helpful for debugging. You can also simply call your script directly from the shell (e.g. `python renderscript.qmd.py`) to view the printed output.""") 72 | 73 | -------------------------------------------------------------------------------- /nbs/images/outerbounds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /nbs/images/novetta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /nbs/tutorials/qmd_intro.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Qmd Documents 3 | description: Introduction to qmd -- markdown on steroids 4 | order: 3 5 | execute: 6 | echo: false 7 | output: asis 8 | --- 9 | 10 | *Qmd documents* are [Markdown]() documents, but with loads of extra functionality provided by [Quarto](https://quarto.org/) and [Pandoc](https://pandoc.org/). nbdev uses Quarto to render its pages (with some extra functionality), and Quarto uses Pandoc to render its pages (with some extra functionality). Every markdown cell in an nbdev notebook is treated as qmd, and nbdev can publish plain qmd text files, and qmd [RenderScripts](/tutorials/renderscript.html). Therefore, it's a good idea to be familiar with the main features of qmd. 11 | 12 | Just like with RenderScripts, you can use hot/live reloading with plain qmd text files -- so as soon as you save the file, you'll see the new output in your web browser (assuming you've got `nbdev_preview` running). 13 | 14 | ## Computations 15 | 16 | You can generate data-driven documents using qmd files. For instance, consider this table (also shown in the RenderScript tutorial for comparison), containing a list of the people with testimonials on nbdev's home page: 17 | 18 | ```{python} 19 | from nbdev import qmd 20 | def im(fn, width, **kw): return qmd.img(f"/images/{fn}", width=f"{width}%", **kw) 21 | 22 | testimonials = [ 23 | ('chris-lattner.png', 'Chris Lattner', 'Inventor of Swift and LLVM'), 24 | ('fernando-pérez.jpeg', 'Fernando Pérez', 'Creator of Jupyter'), 25 | ('david-berg.jpeg', 'David Berg', 'Software Engineer, Netflix'), 26 | ('erik-gaasedelen.jpeg', 'Erik Gaasedelen', 'Software Engineer, Lyft'), 27 | ('roxanna-pourzand.jpeg', 'Roxanna Pourzand', 'Product Manager, Transform'), 28 | ('hugo-bowne-anderson.jpeg', 'Hugo Bowne-Anderson', 'Head of Developer Relations, Outerbounds') 29 | ] 30 | print(qmd.tbl_row(['','Name','Position'])) 31 | print(qmd.tbl_sep([1,3,4])) 32 | for fname,name,position in testimonials: 33 | print(qmd.tbl_row([im(fname, 60), name, position])) 34 | ``` 35 | 36 | The table above is generated using an embedded [qmd computation block](https://quarto.org/docs/computations/python.html) from the following python list: 37 | 38 | ::: {.column-screen-inset-right} 39 | ```python 40 | testimonials = [ 41 | ('chris-lattner.png', 'Chris Lattner', 'Inventor of Swift and LLVM'), 42 | ('fernando-pérez.jpeg', 'Fernando Pérez', 'Creator of Jupyter'), 43 | ('david-berg.jpeg', 'David Berg', 'Software Engineer, Netflix'), 44 | ('erik-gaasedelen.jpeg', 'Erik Gaasedelen', 'Software Engineer, Lyft'), 45 | ('roxanna-pourzand.jpeg', 'Roxanna Pourzand', 'Product Manager, Transform'), 46 | ('hugo-bowne-anderson.jpeg', 'Hugo Bowne-Anderson', 'Head of Developer Relations, Outerbounds') 47 | ] 48 | ``` 49 | ::: 50 | 51 | Just like in the RenderScript example, to produce the table from this python list, the following four lines of code are used: 52 | 53 | ```python 54 | print(qmd.tbl_row(['','Name','Position'])) 55 | print(qmd.tbl_sep([1,3,4])) 56 | for fname,name,position in testimonials: 57 | print(qmd.tbl_row([im(fname, 60), name, position])) 58 | ``` 59 | 60 | For data-driven documents such as this one, we add the following to the YAML frontmatter, which hides the code used to produce outputs, and also does not add any extra formatting to outputs: 61 | 62 | ``` 63 | --- 64 | execute: 65 | echo: false 66 | output: asis 67 | --- 68 | ``` 69 | 70 | Compare the source code of the RenderScript example and of the current page to see how computations are used in RenderScripts compared to plain qmd text files. We find that we like to use Notebooks for most pages we build, since they've got so much helpful functionality (such as pasting images directly into cells). We use RenderScripts for complex web pages like the nbdev home page, and qmd files for pages that are mainly markdown and don't need any notebook functionality. 71 | 72 | ## Formatting 73 | 74 | In addition to the [standard markdown formatting](https://quarto.org/docs/authoring/markdown-basics.html), Quarto qmd adds many additional features. Look at the full quarto docs to see everything it can do -- we'll just highlight a few of our favorites here. 75 | 76 | ### Divs and classes 77 | 78 | You can create HTML [divs](https://quarto.org/docs/authoring/markdown-basics.html#divs-and-spans), by surrounding lines with `:::`. Divs can include classes by placing `{.classname}` after the opening `:::`. Here's an example: 79 | 80 | ``` markdown 81 | ::: {.border} 82 | This content can be styled with a border 83 | ::: 84 | ``` 85 | 86 | This is how that's rendered: 87 | 88 | ::: {.border} 89 | This content can be styled with a border 90 | ::: 91 | 92 | You might be wondering where that `border` class comes from... Quarto comes with support for [Bootstrap 5 and Bootswatch themes](https://quarto.org/docs/output-formats/html-themes.html) so there's lots of classes available you can use in your documents. Remember, all notebook markdown cells are also considered qmd, and can also use all the formatting tricks discussed in this section. 93 | 94 | ### Callouts 95 | 96 | A special kind of block you can use is the [callout block](https://quarto.org/docs/authoring/callouts.html). Here's an example: 97 | 98 | ```markdown 99 | :::{.callout-note} 100 | Note that there are five types of callouts, including: 101 | `note`, `warning`, `important`, `tip`, and `caution`. 102 | ::: 103 | ``` 104 | 105 | ...and here's how it's rendered: 106 | 107 | :::{.callout-note} 108 | Note that there are five types of callouts, including: `note`, `warning`, `important`, `tip`, and `caution`. 109 | ::: 110 | 111 | ### Images 112 | 113 | You can add images (quarto calls them [figures](https://quarto.org/docs/authoring/figures.html) to your document, along with captions, and you can even arrange them into layouts. Here's an example: 114 | 115 | ```markdown 116 | ::: {layout-ncol=3} 117 | ![Jupyter](/images/jupyter.svg) 118 | 119 | ![Vscode](/images/vscode.svg) 120 | 121 | ![Git](/images/git.svg) 122 | ::: 123 | ``` 124 | 125 | ::: {layout-ncol=3} 126 | ![Jupyter](/images/jupyter.svg){width=100px fig-align="left"} 127 | 128 | ![Vscode](/images/vscode.svg){width=100px fig-align="left"} 129 | 130 | ![Git](/images/git.svg){width=100px fig-align="left"} 131 | ::: 132 | 133 | -------------------------------------------------------------------------------- /nbdev/process.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/03_process.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['langs', 'nb_lang', 'first_code_ln', 'extract_directives', 'opt_set', 'instantiate', 'NBProcessor', 'Processor'] 5 | 6 | # %% ../nbs/api/03_process.ipynb 2 7 | from .config import * 8 | from .maker import * 9 | from .imports import * 10 | 11 | from execnb.nbio import * 12 | from fastcore.script import * 13 | from fastcore.imports import * 14 | 15 | from collections import defaultdict 16 | 17 | # %% ../nbs/api/03_process.ipynb 6 18 | # from https://github.com/quarto-dev/quarto-cli/blob/main/src/resources/jupyter/notebook.py 19 | langs = defaultdict( 20 | lambda: '#', r = "#", python = "#", julia = "#", scala = "//", matlab = "%", csharp = "//", fsharp = "//", 21 | c = ["/*","*/"], css = ["/*","*/"], sas = ["*",";"], powershell = "#", bash = "#", sql = "--", mysql = "--", psql = "--", 22 | lua = "--", cpp = "//", cc = "//", stan = "#", octave = "#", fortran = "!", fortran95 = "!", awk = "#", gawk = "#", stata = "*", 23 | java = "//", groovy = "//", sed = "#", perl = "#", ruby = "#", tikz = "%", javascript = "//", js = "//", d3 = "//", node = "//", 24 | sass = "//", coffee = "#", go = "//", asy = "//", haskell = "--", dot = "//", apl = "⍝") 25 | 26 | # %% ../nbs/api/03_process.ipynb 7 27 | def nb_lang(nb): return nested_attr(nb, 'metadata.kernelspec.language', 'python') 28 | 29 | # %% ../nbs/api/03_process.ipynb 9 30 | def _dir_pre(lang=None): return fr"\s*{langs[lang]}\s*\|" 31 | def _quarto_re(lang=None): return re.compile(_dir_pre(lang) + r'\s*[\w|-]+\s*:') 32 | 33 | # %% ../nbs/api/03_process.ipynb 11 34 | def _directive(s, lang='python'): 35 | s = re.sub('^'+_dir_pre(lang), f"{langs[lang]}|", s) 36 | if ':' in s: s = s.replace(':', ': ') 37 | s = (s.strip()[2:]).strip().split() 38 | if not s: return None 39 | direc,*args = s 40 | return direc,args 41 | 42 | # %% ../nbs/api/03_process.ipynb 12 43 | def _norm_quarto(s, lang='python'): 44 | "normalize quarto directives so they have a space after the colon" 45 | m = _quarto_re(lang).match(s) 46 | return m.group(0) + ' ' + _quarto_re(lang).sub('', s).lstrip() if m else s 47 | 48 | # %% ../nbs/api/03_process.ipynb 14 49 | _cell_mgc = re.compile(r"^\s*%%\w+") 50 | 51 | def first_code_ln(code_list, re_pattern=None, lang='python'): 52 | "get first line number where code occurs, where `code_list` is a list of code" 53 | if re_pattern is None: re_pattern = _dir_pre(lang) 54 | return first(i for i,o in enumerate(code_list) if o.strip() != '' and not re.match(re_pattern, o) and not _cell_mgc.match(o)) 55 | 56 | # %% ../nbs/api/03_process.ipynb 17 57 | def _partition_cell(cell, lang): 58 | if not cell.source: return [],[] 59 | lines = cell.source.splitlines(True) 60 | first_code = first_code_ln(lines, lang=lang) 61 | return lines[:first_code],lines[first_code:] 62 | 63 | # %% ../nbs/api/03_process.ipynb 18 64 | def extract_directives(cell, remove=True, lang='python'): 65 | "Take leading comment directives from lines of code in `ss`, remove `#|`, and split" 66 | dirs,code = _partition_cell(cell, lang) 67 | if not dirs: return {} 68 | if remove: 69 | # Leave Quarto directives and cell magic in place for later processing 70 | cell['source'] = ''.join([_norm_quarto(o, lang) for o in dirs if _quarto_re(lang).match(o) or _cell_mgc.match(o)] + code) 71 | return dict(L(_directive(s, lang) for s in dirs).filter()) 72 | 73 | # %% ../nbs/api/03_process.ipynb 22 74 | def opt_set(var, newval): 75 | "newval if newval else var" 76 | return newval if newval else var 77 | 78 | # %% ../nbs/api/03_process.ipynb 23 79 | def instantiate(x, **kwargs): 80 | "Instantiate `x` if it's a type" 81 | return x(**kwargs) if isinstance(x,type) else x 82 | 83 | def _mk_procs(procs, nb): return L(procs).map(instantiate, nb=nb) 84 | 85 | # %% ../nbs/api/03_process.ipynb 24 86 | def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_' 87 | 88 | # %% ../nbs/api/03_process.ipynb 25 89 | class NBProcessor: 90 | "Process cells and nbdev comments in a notebook" 91 | def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False): 92 | self.nb = read_nb(path) if nb is None else nb 93 | self.lang = nb_lang(self.nb) 94 | for cell in self.nb.cells: cell.directives_ = extract_directives(cell, remove=rm_directives, lang=self.lang) 95 | self.procs = _mk_procs(procs, nb=self.nb) 96 | self.debug,self.rm_directives = debug,rm_directives 97 | if process: self.process() 98 | 99 | def _process_cell(self, proc, cell): 100 | if not hasattr(cell,'source'): return 101 | if cell.cell_type=='code' and cell.directives_: 102 | # Option 1: `proc` is directive name with `_` suffix 103 | f = getattr(proc, '__name__', '-').rstrip('_') 104 | if f in cell.directives_: self._process_comment(proc, cell, f) 105 | 106 | # Option 2: `proc` contains a method named `_{directive}_` 107 | for cmd in cell.directives_: 108 | f = getattr(proc, f'_{cmd}_', None) 109 | if f: self._process_comment(f, cell, cmd) 110 | if callable(proc) and not _is_direc(proc): cell = opt_set(cell, proc(cell)) 111 | 112 | def _process_comment(self, proc, cell, cmd): 113 | args = cell.directives_[cmd] 114 | if self.debug: print(cmd, args, proc) 115 | return proc(cell, *args) 116 | 117 | def _proc(self, proc): 118 | if hasattr(proc,'begin'): proc.begin() 119 | for cell in self.nb.cells: self._process_cell(proc, cell) 120 | if hasattr(proc,'end'): proc.end() 121 | self.nb.cells = [c for c in self.nb.cells if c and getattr(c,'source',None) is not None] 122 | for i,cell in enumerate(self.nb.cells): cell.idx_ = i 123 | 124 | def process(self): 125 | "Process all cells with all processors" 126 | for proc in self.procs: self._proc(proc) 127 | 128 | # %% ../nbs/api/03_process.ipynb 35 129 | class Processor: 130 | "Base class for processors" 131 | def __init__(self, nb): self.nb = nb 132 | def cell(self, cell): pass 133 | def __call__(self, cell): return self.cell(cell) 134 | -------------------------------------------------------------------------------- /nbs/index.qmd.py: -------------------------------------------------------------------------------- 1 | """--- 2 | title: Home 3 | pagetitle: nbdev – Create delightful software with Jupyter Notebooks 4 | page-layout: custom 5 | section-divs: false 6 | css: index.css 7 | toc: false 8 | image: https://nbdev.fast.ai/images/card.png 9 | description: Write, test, document, and distribute software packages and technical articles — all in one place, your notebook. 10 | ---""" 11 | 12 | from fastcore.foundation import L 13 | from nbdev import qmd 14 | 15 | def img(fname, classes=None, **kwargs): return qmd.img(f"images/{fname}", classes=classes, **kwargs) 16 | def btn(txt, link): return qmd.btn(txt, link=link, classes=['btn-action-primary', 'btn-action', 'btn', 'btn-success', 'btn-lg']) 17 | def banner(txt, classes=None, style=None): return qmd.div(txt, L('hero-banner')+classes, style=style) 18 | 19 | features = L( 20 | ('docs', 'Beautiful technical documentation and scientific articles with Quarto'), 21 | ('testing', 'Out-of-the-box continuous integration with GitHub Actions'), 22 | ('packaging', 'Publish code to PyPI and conda, and prose to GitHub Pages'), 23 | ('vscode', 'Two-way sync with your favourite IDEs'), 24 | ('jupyter', 'Write prose, code, and tests in notebooks — no context-switching'), 25 | ('git', 'Git-friendly notebooks: human-readable merge conflicts; no unwanted metadata') 26 | ) 27 | 28 | testms = L( 29 | ('chris-lattner.png', 'Chris Lattner', 'Inventor of Swift and LLVM', 'I really do think [nbdev] is a huge step forward for programming environments.'), 30 | ('fernando-pérez.jpeg', 'Fernando Pérez', 'Creator of Jupyter', '[nbdev] should be celebrated and used a lot more — I have kept a tab with your original nbdev blog post open for months in Chrome because of how often I refer to it and point others to this work.'), 31 | ('david-berg.jpeg', 'David Berg', 'Software Engineer, Netflix', 'Prior to using nbdev, documentation was the most cumbersome aspect of our software development process… Using nbdev allows us to spend more time creating rich prose around the many code snippets guaranteeing the whole experience is robust.

nbdev has turned what was once a chore into a natural extension of the notebook-based testing we were already doing.'), 32 | ('erik-gaasedelen.jpeg', 'Erik Gaasedelen', 'Software Engineer, Lyft', 'I use this in production at my company. It’s an awesome tool… nbdev streamlines everything so I can write docs, tests, and code all in one place… The packaging is also really well thought out.

From my point of view it is close to a Pareto improvement over traditional Python library development.'), 33 | ('roxanna-pourzand.jpeg', 'Roxanna Pourzand', 'Product Manager, Transform', 'We’re so excited about using nbdev. Our product is technical so our resulting documentation includes a lot of code-based examples. Before nbdev, we had no way of maintaining our code examples and ensuring that it was up-to-date for both command inputs and outputs. It was all manual. With nbdev, we now have this under control in a sustainable way. Since we’ve deployed these docs, we also had a situation where we were able to identify a bug in one of our interfaces, which we found by seeing the error that was output in the documentation.'), 34 | ('hugo-bowne-anderson.jpeg', 'Hugo Bowne-Anderson', 'Head of Developer Relations, Outerbounds', 'Nbdev has transformed the way we write documentation. Gone are the days of worrying about broken code examples when our API changes or due to human errors associated with copying & pasting code into markdown files. The authoring experience of nbdev is also powerful, allowing us to write prose and live code in a unified interface, which allows more experimentation with technical content. On top of this, nbdev allows us to include unit tests in our documentation which mitigates the burden of maintaining the docs over time.') 35 | ) 36 | 37 | def industry(im, **kwargs): return qmd.div(img(im, **kwargs), ["g-col-12", "g-col-sm-6", "g-col-md-3"]) 38 | 39 | def testm(im, nm, detl, txt): 40 | return qmd.div(f"""{img(im, link=True)} 41 | 42 | # {nm} 43 | 44 | ## {detl} 45 | 46 | ### {txt}""", ["testimonial", "g-col-12", "g-col-md-6"]) 47 | 48 | expert_d = qmd.div('\n'.join(testms.starmap(testm)), ['content-block', 'grid', 'gap-4']) 49 | 50 | def feature(im, desc): return qmd.div(f"{img(im+'.svg')}\n\n{desc}\n", ['feature', 'g-col-12', 'g-col-sm-6', 'g-col-md-4']) 51 | 52 | feature_d = qmd.div('\n'.join(features.starmap(feature)), ['grid', 'gap-4'], style={"padding-bottom": "60px"}) 53 | 54 | def b(*args, **kwargs): print(banner (*args, **kwargs)) 55 | def d(*args, **kwargs): print(qmd.div(*args, **kwargs)) 56 | 57 | ### 58 | # Output section 59 | ### 60 | 61 | b(f"""# Create delightful software
with Jupyter Notebooks 62 | 63 | ### Write, test, document, and distribute software packages and technical articles — all in one place, your notebook. 64 | 65 | {btn('Get started', '/getting_started.ipynb')} 66 | 67 | {img('card.png', style={"margin-top": "40px", "margin-bottom": "40px"}, link=True)}""", "content-block") 68 | 69 | industries = '\n'.join([ 70 | industry('netflix.svg', height=26, relative=("top",1)), 71 | industry('transform.svg', height=26, relative=("bottom",1)), 72 | industry('outerbounds.svg', height=26, relative=("bottom",1)), 73 | industry('novetta.svg', height=30, relative=("top",1)), 74 | industry('amd.svg', height=22), 75 | industry('overstory.png', height=26), 76 | industry('bom.png', height=46, relative=("bottom",12)), 77 | industry('lyft.svg', height=34), 78 | ]) 79 | industries = qmd.div(industries, 'grid', style={"column-gap": "50px"}) 80 | 81 | b(f"""## Trusted in industry 82 | 83 | {industries}""", "mid-content") 84 | 85 | feature_h = banner(f"""## Interactive programming
without compromise 86 | 87 | ### Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.""") 88 | 89 | d(feature_h+feature_d, "content-block") 90 | 91 | expert_b = banner("## Here's what experts are saying") 92 | 93 | d(expert_b+expert_d, "mid-content") 94 | 95 | b(f"""## Get started in seconds 96 | 97 | {btn('Install nbdev', '/getting_started.ipynb')}""", 'content-block', style={"margin-top": "40px"}) 98 | 99 | -------------------------------------------------------------------------------- /nbs/images/jupyter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /nbs/api/17_serve.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "36769638", 6 | "metadata": {}, 7 | "source": [ 8 | "# serve\n", 9 | "\n", 10 | "> A parallel ipynb processor (experimental)\n", 11 | "- order: 17" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "f305fea5", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "#| default_exp serve" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "6899a335", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "#|export\n", 32 | "import ast,subprocess,threading,sys\n", 33 | "from shutil import rmtree,copy2\n", 34 | "\n", 35 | "from fastcore.utils import *\n", 36 | "from fastcore.parallel import parallel\n", 37 | "from fastcore.script import call_parse\n", 38 | "from fastcore.meta import delegates\n", 39 | "\n", 40 | "from nbdev.config import get_config\n", 41 | "from nbdev.doclinks import nbglob_cli,nbglob\n", 42 | "from nbdev.processors import FilterDefaults\n", 43 | "import nbdev.serve_drv" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "32c11e55", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "#|hide\n", 54 | "__file__ = 'serve.ipynb'" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "65766a33", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "#|export\n", 65 | "def _is_qpy(path:Path):\n", 66 | " \"Is `path` a py script starting with frontmatter?\"\n", 67 | " path = Path(path)\n", 68 | " if not path.suffix=='.py': return\n", 69 | " p = ast.parse(path.read_text())\n", 70 | "# try: p = ast.parse(path.read_text())\n", 71 | "# except: return\n", 72 | " if not p.body: return\n", 73 | " a = p.body[0]\n", 74 | " if isinstance(a, ast.Expr) and isinstance(a.value, ast.Constant):\n", 75 | " v = a.value.value.strip()\n", 76 | " vl = v.splitlines()\n", 77 | " if vl[0]=='---' and vl[-1]=='---': return '\\n'.join(vl[1:-1])" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "id": "abc3835a", 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "#|export\n", 88 | "def _proc_file(s, cache, path, mtime=None):\n", 89 | " skips = ('_proc', '_docs', '_site')\n", 90 | " if not s.is_file() or any(o[0]=='.' or o in skips for o in s.parts): return\n", 91 | " d = cache/s.relative_to(path)\n", 92 | " if s.suffix=='.py': d = d.with_suffix('')\n", 93 | " if d.exists():\n", 94 | " dtime = d.stat().st_mtime\n", 95 | " if mtime: dtime = max(dtime, mtime)\n", 96 | " if s.stat().st_mtime<=dtime: return\n", 97 | "\n", 98 | " d.parent.mkdir(parents=True, exist_ok=True)\n", 99 | " if s.suffix=='.ipynb': return s,d,FilterDefaults\n", 100 | " md = _is_qpy(s)\n", 101 | " if md is not None: return s,d,md.strip()\n", 102 | " else: copy2(s,d)" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "id": "8b3b08f7", 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "#|hide\n", 113 | "# __file__ = '../tutorials/circles.svg.py'\n", 114 | "# p = Path(__file__).resolve()\n", 115 | "# cfg = get_config()\n", 116 | "# cache = cfg.config_path/'_proc'\n", 117 | "# path = Path(cfg.nbs_path)\n", 118 | "# _proc_file(p, cache, path)" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "14463227", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "#|export\n", 129 | "@delegates(nbglob_cli)\n", 130 | "def proc_nbs(\n", 131 | " path:str='', # Path to notebooks\n", 132 | " n_workers:int=defaults.cpus, # Number of workers\n", 133 | " force:bool=False, # Ignore cache and build all\n", 134 | " file_glob:str='', # Only include files matching glob\n", 135 | " file_re:str='', # Only include files matching glob\n", 136 | " **kwargs):\n", 137 | " \"Process notebooks in `path` for docs rendering\"\n", 138 | " cfg = get_config()\n", 139 | " cache = cfg.config_path/'_proc'\n", 140 | " path = Path(path or cfg.nbs_path)\n", 141 | " files = nbglob(path, func=Path, file_glob='', file_re='', **kwargs)\n", 142 | " if (path/'_quarto.yml').exists(): files.append(path/'_quarto.yml')\n", 143 | " if (path/'_extensions').exists(): files.extend(nbglob(path/'_extensions', func=Path, file_glob='', file_re='', skip_file_re='^[.]'))\n", 144 | "\n", 145 | " # If settings.ini or filter script newer than cache folder modified, delete cache\n", 146 | " chk_mtime = max(cfg.config_file.stat().st_mtime, Path(__file__).stat().st_mtime)\n", 147 | " cache.mkdir(parents=True, exist_ok=True)\n", 148 | " cache_mtime = cache.stat().st_mtime\n", 149 | " if force or (cache.exists and cache_mtime Basic qmd generation helpers (experimental)\n", 11 | "- order: 15" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "2b548a67", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "#|default_exp qmd" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "6a35c7c4-748f-4c82-a9bf-c780a8d83e90", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "#|export\n", 32 | "from __future__ import annotations\n", 33 | "import sys,os,inspect\n", 34 | "\n", 35 | "from fastcore.utils import *\n", 36 | "from fastcore.meta import delegates" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "1e623a3d-3e77-44c6-adf3-4768b78328c5", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "#|hide\n", 47 | "from fastcore.test import *" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "id": "5a64f1f4", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "#| export\n", 58 | "def meta(md, # Markdown to add meta to\n", 59 | " classes=None, # List of CSS classes to add\n", 60 | " style=None, # Dict of CSS styles to add\n", 61 | " **kwargs): # Additional attributes to add to meta\n", 62 | " \"A metadata section for qmd div in `{}`\"\n", 63 | " if style: kwargs['style'] = \"; \".join(f'{k}: {v}' for k,v in style.items())\n", 64 | " props = ' '.join(f'{k}=\"{v}\"' for k,v in kwargs.items())\n", 65 | " classes = ' '.join('.'+c for c in L(classes))\n", 66 | " meta = []\n", 67 | " if classes: meta.append(classes)\n", 68 | " if props: meta.append(props)\n", 69 | " meta = ' '.join(meta)\n", 70 | " return md + (\"{\" + meta + \"}\" if meta else \"\")" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "id": "52637a70", 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "#| export\n", 81 | "def div(txt, # Markdown to add meta to\n", 82 | " classes=None, # List of CSS classes to add\n", 83 | " style=None, # Dict of CSS styles to add\n", 84 | " **kwargs):\n", 85 | " \"A qmd div with optional metadata section\"\n", 86 | " return meta(\"::: \", classes=classes, style=style, **kwargs) + f\"\\n\\n{txt}\\n\\n:::\\n\\n\"" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "id": "f9f499f4", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "#| export\n", 97 | "def img(fname, # Image to link to\n", 98 | " classes=None, # List of CSS classes to add\n", 99 | " style=None, # Dict of CSS styles to add\n", 100 | " height=None, # Height attribute\n", 101 | " relative=None, # Tuple of (position,px)\n", 102 | " link=False, # Hyperlink to this image\n", 103 | " **kwargs):\n", 104 | " \"A qmd image\"\n", 105 | " kwargs,style = kwargs or {}, style or {}\n", 106 | " if height: kwargs[\"height\"]= f\"{height}px\"\n", 107 | " if relative:\n", 108 | " pos,px = relative\n", 109 | " style[\"position\"] = \"relative\"\n", 110 | " style[pos] = f\"{px}px\"\n", 111 | " res = meta(f'![]({fname})', classes=classes, style=style, **kwargs)\n", 112 | " return f'[{res}]({fname})' if link else res" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "id": "16d7aa5f", 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "#| export\n", 123 | "def btn(txt, # Button text\n", 124 | " link, # Button link URL\n", 125 | " classes=None, # List of CSS classes to add\n", 126 | " style=None, # Dict of CSS styles to add\n", 127 | " **kwargs):\n", 128 | " \"A qmd button\"\n", 129 | " return meta(f'[{txt}]({link})', classes=classes, style=style, role=\"button\")" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "id": "e414ed33", 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "#| export\n", 140 | "def tbl_row(cols:list, # Auto-stringified columns to show in the row\n", 141 | " ):\n", 142 | " \"Create a markdown table row from `cols`\"\n", 143 | " return '|' + '|'.join(str(c or '') for c in cols) + '|'" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "id": "38f0641b", 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "#| export\n", 154 | "def tbl_sep(sizes:int|list=3 # List of column sizes, or single `int` if all sizes the same\n", 155 | " ):\n", 156 | " \"Create a markdown table separator with relative column size `sizes`\"\n", 157 | " if isinstance(sizes,int): sizes = [3]*sizes\n", 158 | " return tbl_row('-'*s for s in sizes)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "id": "a0d6553e-05ab-4db2-b739-aeee59a9ff31", 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "#| export\n", 169 | "def _install_nbdev():\n", 170 | " return div('''#### pip\n", 171 | "\n", 172 | "```sh\n", 173 | "pip install -U nbdev\n", 174 | "```\n", 175 | "\n", 176 | "#### conda\n", 177 | "\n", 178 | "```sh\n", 179 | "conda install -c fastai nbdev\n", 180 | "```\n", 181 | "''', ['panel-tabset'])" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "id": "aa35b010", 187 | "metadata": {}, 188 | "source": [ 189 | "## Export -" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "id": "3d8031ce", 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "#|hide\n", 200 | "import nbdev; nbdev.nbdev_export()" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "id": "12588a26-43a6-42c4-bacd-896293c871ab", 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [] 210 | } 211 | ], 212 | "metadata": { 213 | "kernelspec": { 214 | "display_name": "Python 3 (ipykernel)", 215 | "language": "python", 216 | "name": "python3" 217 | } 218 | }, 219 | "nbformat": 4, 220 | "nbformat_minor": 5 221 | } 222 | -------------------------------------------------------------------------------- /nbs/tutorials/pre_commit.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2f2a6752-fa19-4096-8bd2-5ca82e1a9ffb", 6 | "metadata": {}, 7 | "source": [ 8 | "# Pre-Commit Hooks\n", 9 | "\n", 10 | "> How to use nbdev's git pre-commit hooks\n", 11 | "- order: 6" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "6b6629a1-e679-48ec-8cd8-21d65d5422b2", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "#| hide\n", 22 | "from nbdev.qmd import _install_nbdev" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "f9c2fff2-db3a-4ba2-ba16-8a6b1d9aa9e1", 28 | "metadata": {}, 29 | "source": [ 30 | "We provide hooks for the [pre-commit framework](https://pre-commit.com/) to catch and fix uncleaned and unexported notebooks, locally, without having to wait for continuous integration pipelines to run. " 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "4411d7f9-5dc1-4ba8-b7a3-8e822ad21901", 36 | "metadata": {}, 37 | "source": [ 38 | "They might also be useful as an alternative to the [Jupyter clean hook](/tutorials/git_friendly_jupyter.ipynb#nbdev_clean-on-saving-notebooks-in-jupyter) if you're using a notebook editor that isn't yet supported (e.g. VSCode)." 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "id": "0c3c7192-892a-4ff9-acb0-a89fe8a2e4bc", 44 | "metadata": {}, 45 | "source": [ 46 | "## Install pre-commit" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "d161041d-7585-4177-beb2-06d2fb3919e9", 52 | "metadata": {}, 53 | "source": [ 54 | "...Install pre-commit (check [their latest instructions](https://pre-commit.com/#install) if you have any difficulty with these commands):\n", 55 | "\n", 56 | "::: {.panel-tabset}\n", 57 | "\n", 58 | "#### pip\n", 59 | "\n", 60 | "```sh\n", 61 | "pip install pre-commit\n", 62 | "```\n", 63 | "\n", 64 | "#### conda\n", 65 | "\n", 66 | "```sh\n", 67 | "conda install -c conda-forge pre-commit\n", 68 | "```\n", 69 | "\n", 70 | "#### homebrew (macOS)\n", 71 | "\n", 72 | "```sh\n", 73 | "brew install pre-commit\n", 74 | "```\n", 75 | "\n", 76 | ":::" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "e94e4d29-e137-4f08-b7ee-b6aa06dc49a9", 82 | "metadata": {}, 83 | "source": [ 84 | "## Configure pre-commit for your repo" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "ebd3c984-8419-4fa9-9c5a-abca1924f88d", 90 | "metadata": {}, 91 | "source": [ 92 | "Create a file named `.pre-commit-config.yaml` in the root of your repo, with the following contents:\n", 93 | "\n", 94 | "```yaml\n", 95 | "repos:\n", 96 | "- repo: https://github.com/fastai/nbdev\n", 97 | " rev: 2.2.10\n", 98 | " hooks:\n", 99 | " - id: nbdev_clean\n", 100 | " - id: nbdev_export\n", 101 | "```\n", 102 | "\n", 103 | "Include only the hook(s) you'd like to run, as well as any other [supported hooks](https://pre-commit.com/hooks.html)." 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "id": "16dab296-26f5-47a3-8230-e63ea403f320", 109 | "metadata": {}, 110 | "source": [ 111 | "::: {.callout-tip}\n", 112 | "If you expect all collaborators to use pre-commit, add the `.pre-commit-config.yaml` file to your repo. Otherwise, add it to your `.gitignore`.\n", 113 | ":::" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "id": "a801d92a-30f5-4c87-b57f-ad0e11b72c0e", 119 | "metadata": {}, 120 | "source": [ 121 | "Install pre-commit hooks into your repo:\n", 122 | "\n", 123 | "```sh\n", 124 | "pre-commit install\n", 125 | "```" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "id": "a4a7cd01-6106-48fb-96fe-26d0f0e71572", 131 | "metadata": {}, 132 | "source": [ 133 | "## Make a commit and enjoy pre-commit in action" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "4f04112c-38d3-4422-a819-bf08342214d4", 139 | "metadata": {}, 140 | "source": [ 141 | "When you do a `git commit` in a repo that has pre-commit hooks installed, your new workflow will be as follows:\n", 142 | "\n", 143 | "1. pre-commit runs each hook on your _staged_ changes (as in, changes that you `git add`ed)\n", 144 | "2. If a hook changes files -- for example, if a commited notebook wasn't cleaned -- pre-commit stops the commit, leaving those changes as _unstaged_\n", 145 | "3. You can now stage those changes and make any edits required to get pre-commit to pass\n", 146 | "4. Redo the `git commit`, and if it succeeds, your commit will be created." 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "id": "436f12f9-81e6-4c16-a407-c23a510e7327", 152 | "metadata": {}, 153 | "source": [ 154 | "Using it in practice isn't as complicated as it might sound. The best way to figure out if it works for you is to give it a try." 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "id": "dc9b349e-db45-42cd-9268-4f87f754ce51", 160 | "metadata": {}, 161 | "source": [ 162 | "## How to override pre-commit if you get stuck" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "id": "570e52db-3608-47cb-9a23-70b44b9b71ad", 168 | "metadata": {}, 169 | "source": [ 170 | "If you struggle to get pre-commit to pass a commit that you absolutely think is correct, you can [temporarily disable a hook](https://pre-commit.com/#temporarily-disabling-hooks) like this:\n", 171 | "\n", 172 | "```sh\n", 173 | "SKIP=hook git commit\n", 174 | "```\n", 175 | "\n", 176 | "...where `hook` refers to a valid hook in your configuration, for example, to disable the `nbdev_export` hook:\n", 177 | "\n", 178 | "```sh\n", 179 | "SKIP=nbdev_export git commit\n", 180 | "```\n", 181 | "\n", 182 | "You can also disable pre-commit entirely with the `--no-verify` flag:\n", 183 | "\n", 184 | "```sh\n", 185 | "git commit --no-verify\n", 186 | "```" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "id": "09492b76-359a-40a8-9c3d-a19d3d80bd19", 192 | "metadata": {}, 193 | "source": [ 194 | "Finally, if you decide it's not for you, you can completely remove pre-commit hooks from your repo with:\n", 195 | "\n", 196 | "```sh\n", 197 | "pre-commit uninstall\n", 198 | "```" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "id": "2607c475-3efc-4e9d-a956-e3be801ecbf7", 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [] 208 | } 209 | ], 210 | "metadata": { 211 | "kernelspec": { 212 | "display_name": "Python 3 (ipykernel)", 213 | "language": "python", 214 | "name": "python3" 215 | } 216 | }, 217 | "nbformat": 4, 218 | "nbformat_minor": 5 219 | } 220 | -------------------------------------------------------------------------------- /nbs/explanations/config.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Settings.ini\n", 8 | "\n", 9 | "> The nbdev configuration file\n", 10 | "- order: 1" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "#| hide\n", 20 | "from fastcore.utils import *\n", 21 | "from nbdev.config import *\n", 22 | "\n", 23 | "from nbdev.showdoc import *" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "All of nbdev's configuration is done through a file called `settings.ini` which lives in the root of your repo. It's in [ConfigParser](https://docs.python.org/3/library/configparser.html) format. For example, here's the first few lines of nbdev's settings.ini file" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [ 38 | { 39 | "name": "stdout", 40 | "output_type": "stream", 41 | "text": [ 42 | "[DEFAULT]\r\n", 43 | "lib_name = nbdev\r\n", 44 | "description = Create delightful software with Jupyter Notebooks\r\n", 45 | "copyright = 2020 onwards, Jeremy Howard\r\n", 46 | "keywords = nbdev fastai jupyter notebook export\r\n", 47 | "user = fastai\r\n", 48 | "author = Jeremy Howard and Hamel Husain\r\n", 49 | "author_email = j@fast.ai\r\n", 50 | "branch = master\r\n", 51 | "min_python = 3.7\r\n" 52 | ] 53 | } 54 | ], 55 | "source": [ 56 | "#| exec_doc\n", 57 | "#| echo: false\n", 58 | "!head ../../settings.ini" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "You can create this file with `nbdev_create_config` (in which case you pass the settings manually), or with `nbdev_new` (which sets it up automatically for you from your repo settings). Here are all of nbdev's settings (excluding the `path` and `cfg_name` parameters which decide where the config file is saved):" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "data": { 75 | "text/markdown": [ 76 | "| | **Type** | **Default** | **Details** |\n", 77 | "| -- | -------- | ----------- | ----------- |\n", 78 | "| user | str | | Repo username |\n", 79 | "| author | str | | Package author's name |\n", 80 | "| author_email | str | | Package author's email address |\n", 81 | "| description | str | | Short summary of the package |\n", 82 | "| path | str | . | Path to create config file |\n", 83 | "| cfg_name | str | settings.ini | Name of config file to create |\n", 84 | "| lib_name | str | None | Package name, defaults to local repo folder name passed to `apply_defaults` |\n", 85 | "| branch | str | master | Repo default branch passed to `apply_defaults` |\n", 86 | "| git_url | str | https://github.com/%(user)s/%(lib_name)s | Repo URL passed to `apply_defaults` |\n", 87 | "| custom_sidebar | bool_arg | False | Use a custom sidebar.yml? passed to `apply_defaults` |\n", 88 | "| nbs_path | str | . | Path to notebooks passed to `apply_defaults` |\n", 89 | "| lib_path | str | %(lib_name)s | Path to package root passed to `apply_defaults` |\n", 90 | "| doc_path | str | _docs | Path to rendered docs passed to `apply_defaults` |\n", 91 | "| tst_flags | str | | Test flags passed to `apply_defaults` |\n", 92 | "| version | str | 0.0.1 | Version of this release passed to `apply_defaults` |\n", 93 | "| doc_host | str | https://%(user)s.github.io | Hostname for docs passed to `apply_defaults` |\n", 94 | "| doc_baseurl | str | /%(lib_name)s | Base URL for docs passed to `apply_defaults` |\n", 95 | "| keywords | str | nbdev jupyter notebook python | Package keywords passed to `apply_defaults` |\n", 96 | "| license | str | apache2 | License for the package passed to `apply_defaults` |\n", 97 | "| copyright | str | None | Copyright for the package, defaults to '`current_year` onwards, `author`' passed to `apply_defaults` |\n", 98 | "| status | str | 3 | Development status PyPI classifier passed to `apply_defaults` |\n", 99 | "| min_python | str | 3.7 | Minimum Python version PyPI classifier passed to `apply_defaults` |\n", 100 | "| audience | str | Developers | Intended audience PyPI classifier passed to `apply_defaults` |\n", 101 | "| language | str | English | Language PyPI classifier passed to `apply_defaults` |\n", 102 | "| recursive | bool_arg | False | Include subfolders in notebook globs? passed to `apply_defaults` |\n", 103 | "| black_formatting | bool_arg | False | Format libraries with black? passed to `apply_defaults` |\n", 104 | "| readme_nb | str | index.ipynb | Notebook to export as repo readme passed to `apply_defaults` |\n", 105 | "| title | str | %(lib_name)s | Quarto website title passed to `apply_defaults` |\n", 106 | "| allowed_metadata_keys | str | | Preserve the list of keys in the main notebook metadata passed to `apply_defaults` |\n", 107 | "| allowed_cell_metadata_keys | str | | Preserve the list of keys in cell level metadata passed to `apply_defaults` |\n", 108 | "| jupyter_hooks | bool | True | Run Jupyter hooks? passed to `apply_defaults` |\n", 109 | "| clean_ids | bool | True | Remove ids from plaintext reprs? passed to `apply_defaults` |\n", 110 | "| custom_quarto_yml | bool | False | Use a custom _quarto.yml? passed to `apply_defaults` |" 111 | ], 112 | "text/plain": [ 113 | "" 114 | ] 115 | }, 116 | "execution_count": null, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "#| exec_doc\n", 123 | "#| echo: false\n", 124 | "DocmentTbl(nbdev_create_config)" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "You can customise nbdev for all repositories for your user with a `~/.config/nbdev/settings.ini` file." 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "In order for Git actions to run smoothly, add `requirements` and `dev_requirements` with required packages in `settings.ini`.\n", 139 | "\n", 140 | "see [here](https://github.com/fastai/nbdev/blob/master/settings.ini) as a reference." 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [] 149 | } 150 | ], 151 | "metadata": { 152 | "kernelspec": { 153 | "display_name": "Python 3 (ipykernel)", 154 | "language": "python", 155 | "name": "python3" 156 | } 157 | }, 158 | "nbformat": 4, 159 | "nbformat_minor": 4 160 | } 161 | -------------------------------------------------------------------------------- /tests/01_everything.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "raw", 5 | "metadata": {}, 6 | "source": [ 7 | "---\n", 8 | "title: Foo\n", 9 | "execute:\n", 10 | " echo: false\n", 11 | "---" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# A Title\n", 19 | "> A description" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "This notebook is used to demonstrate and test all the features of nbdev's export functionality. See the notebooks in `nbs` for how it's used." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "#|export\n", 36 | "from __future__ import print_function" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "#|export\n", 46 | "from __future__ import absolute_import\n", 47 | "from fastcore.utils import patch,patch_to" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "#|hide\n", 57 | "#|default_exp everything\n", 58 | "#|default_cls_lvl 3" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "Each symbol name below has >=2 parts, split on `_`. First part is a unique name. Second is `y` if it should be exported. Third is `nall` if it shouldn't appear in `__all__`." 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "import nbdev.x,y,nbdev.z\n", 75 | "from nbdev.x import *" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "#|export\n", 85 | "def a_y(): ..." 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "#|export\n", 95 | "def a_y(f): # should only appear once\n", 96 | " def ai_n(): ...\n", 97 | " class a2i_n(): ...\n", 98 | " return f" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "#|hide\n", 108 | "#|export\n", 109 | "def b_y(a:bool)->int: ..." 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "#|exporti\n", 119 | "#just another comment\n", 120 | "def c_y_nall(): ..." 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "#|export\n", 130 | "class d_y():\n", 131 | " def di_n(): ...\n", 132 | " class d2i_n(): ...\n", 133 | "\n", 134 | "@a_y\n", 135 | "async def e_y(): ..." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "#|export\n", 145 | "@patch\n", 146 | "def d3i_n(self:d_y): ..." 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "#|export\n", 156 | "@patch_to(d_y)\n", 157 | "def d4i_n(self): ..." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "#|export\n", 167 | "def _f_y_nall(): ..." 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "#|export some.thing\n", 177 | "def h_n(): ..." 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "def i_n(): ..." 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "def j_n(): ...\n", 196 | "#|export" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "#export is used in a full sentence here\n", 206 | "#therefore this is not exported\n", 207 | "def k_n(): ..." 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "#exporting\n", 217 | "def l_n(): ..." 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "#|export\n", 227 | "m_y = n_y = 1" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "#|export\n", 237 | "exec(\"o_y=1\")\n", 238 | "exec(\"p_y=1\")\n", 239 | "_all_ = [o_y, 'p_y']" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "#|export\n", 249 | "q_y,_r_n = (1,1)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "#|export\n", 259 | "a_y.test = 1" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": null, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "#|printme testing\n", 269 | "_tmp = \"Cell for testing processor subclass\"" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": null, 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "data": { 279 | "text/html": [ 280 | "a test\n" 281 | ], 282 | "text/plain": [ 283 | "" 284 | ] 285 | }, 286 | "metadata": {}, 287 | "output_type": "display_data" 288 | } 289 | ], 290 | "source": [ 291 | "%%html\n", 292 | "a test" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": null, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "name": "stdout", 302 | "output_type": "stream", 303 | "text": [ 304 | "\u001b[94mhello\n" 305 | ] 306 | } 307 | ], 308 | "source": [ 309 | "print('\\033[94mhello')" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": null, 315 | "metadata": {}, 316 | "outputs": [ 317 | { 318 | "name": "stdout", 319 | "output_type": "stream", 320 | "text": [ 321 | "do not print me!\n" 322 | ] 323 | } 324 | ], 325 | "source": [ 326 | "#|eval:false\n", 327 | "print(\"do not print me!\")" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": null, 333 | "metadata": {}, 334 | "outputs": [], 335 | "source": [] 336 | } 337 | ], 338 | "metadata": { 339 | "kernelspec": { 340 | "display_name": "Python 3 (ipykernel)", 341 | "language": "python", 342 | "name": "python3" 343 | } 344 | }, 345 | "nbformat": 4, 346 | "nbformat_minor": 4 347 | } 348 | -------------------------------------------------------------------------------- /nbdev/migrate.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/16_migrate.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['MigrateProc', 'fp_md_fm', 'migrate_nb', 'migrate_md', 'nbdev_migrate'] 5 | 6 | # %% ../nbs/api/16_migrate.ipynb 2 7 | from .process import * 8 | from .frontmatter import * 9 | from .frontmatter import _fm2dict, _re_fm_md, _dict2fm, _insertfm 10 | from .processors import * 11 | from .config import get_config, read_nb 12 | from .sync import write_nb 13 | from .showdoc import show_doc 14 | from fastcore.all import * 15 | import shutil 16 | 17 | # %% ../nbs/api/16_migrate.ipynb 5 18 | def _cat_slug(fmdict): 19 | "Get the partial slug from the category front matter." 20 | slug = '/'.join(fmdict.get('categories', '')) 21 | return '/' + slug if slug else '' 22 | 23 | # %% ../nbs/api/16_migrate.ipynb 7 24 | def _file_slug(fname): 25 | "Get the partial slug from the filename." 26 | p = Path(fname) 27 | dt = '/'+p.name[:10].replace('-', '/')+'/' 28 | return dt + p.stem[11:] 29 | 30 | # %% ../nbs/api/16_migrate.ipynb 9 31 | def _replace_fm(d:dict, # dictionary you wish to conditionally change 32 | k:str, # key to check 33 | val:str,# value to check if d[k] == v 34 | repl_dict:dict #dictionary that will be used as a replacement 35 | ): 36 | "replace key `k` in dict `d` if d[k] == val with `repl_dict`" 37 | if str(d.get(k, '')).lower().strip() == str(val.lower()).strip(): 38 | d.pop(k) 39 | d = merge(d, repl_dict) 40 | return d 41 | 42 | def _fp_fm(d): 43 | "create aliases for fastpages front matter to match Quarto front matter." 44 | d = _replace_fm(d, 'search_exclude', 'true', {'search':'false'}) 45 | d = _replace_fm(d, 'hide', 'true', {'draft': 'true'}) 46 | return d 47 | 48 | # %% ../nbs/api/16_migrate.ipynb 10 49 | def _fp_image(d): 50 | "Correct path of fastpages images to reference the local directory." 51 | prefix = 'images/copied_from_nb/' 52 | if d.get('image', '').startswith(prefix): d['image'] = d['image'].replace(prefix, '') 53 | return d 54 | 55 | # %% ../nbs/api/16_migrate.ipynb 11 56 | def _rm_quote(s): 57 | title = re.search('''"(.*?)"''', s) 58 | return title.group(1) if title else s 59 | 60 | def _is_jekyll_post(path): return bool(re.search(r'^\d{4}-\d{2}-\d{2}-', Path(path).name)) 61 | 62 | def _fp_convert(fm:dict, path:Path): 63 | "Make fastpages frontmatter Quarto complaint and add redirects." 64 | fs = _file_slug(path) 65 | if _is_jekyll_post(path): 66 | fm = compose(_fp_fm, _fp_image)(fm) 67 | if 'permalink' in fm: fm['aliases'] = [f"{fm['permalink'].strip()}"] 68 | else: fm['aliases'] = [f'{_cat_slug(fm) + fs}'] 69 | if not fm.get('date'): 70 | _,y,m,d,_ = fs.split('/') 71 | fm['date'] = f'{y}-{m}-{d}' 72 | 73 | if fm.get('summary') and not fm.get('description'): fm['description'] = fm['summary'] 74 | if fm.get('tags') and not fm.get('categories'): 75 | if isinstance(fm['tags'], str): fm['categories'] = fm['tags'].split() 76 | elif isinstance(fm['tags'], list): fm['categories'] = fm['tags'] 77 | for k in ['title', 'description']: 78 | if k in fm: fm[k] = _rm_quote(fm[k]) 79 | if fm.get('comments'): fm.pop('comments') #true by itself is not a valid value for comments https://quarto.org/docs/output-formats/html-basics.html#commenting, and the default is true 80 | return fm 81 | 82 | # %% ../nbs/api/16_migrate.ipynb 14 83 | class MigrateProc(Processor): 84 | "Migrate fastpages front matter in notebooks to a raw cell." 85 | def begin(self): 86 | self.nb.frontmatter_ = _fp_convert(self.nb.frontmatter_, self.nb.path_) 87 | if getattr(first(self.nb.cells), 'cell_type', None) == 'raw': del self.nb.cells[0] 88 | _insertfm(self.nb, self.nb.frontmatter_) 89 | 90 | # %% ../nbs/api/16_migrate.ipynb 21 91 | def fp_md_fm(path): 92 | "Make fastpages front matter in markdown files quarto compliant." 93 | p = Path(path) 94 | md = p.read_text() 95 | fm = _fm2dict(md, nb=False) 96 | if fm: 97 | fm = _fp_convert(fm, path) 98 | return _re_fm_md.sub(_dict2fm(fm), md) 99 | else: return md 100 | 101 | # %% ../nbs/api/16_migrate.ipynb 30 102 | _alias = merge({k:'code-fold: true' for k in ['collapse', 'collapse_input', 'collapse_hide']}, 103 | {'collapse_show':'code-fold: show', 'hide_input': 'echo: false', 'hide': 'include: false', 'hide_output': 'output: false'}) 104 | def _subv1(s): return _alias.get(s, s) 105 | 106 | # %% ../nbs/api/16_migrate.ipynb 31 107 | def _re_v1(): 108 | d = ['default_exp', 'export', 'exports', 'exporti', 'hide', 'hide_input', 'collapse_show', 'collapse', 109 | 'collapse_hide', 'collapse_input', 'hide_output', 'default_cls_lvl'] 110 | d += L(get_config().tst_flags).filter() 111 | d += [s.replace('_', '-') for s in d] # allow for hyphenated version of old directives 112 | _tmp = '|'.join(list(set(d))) 113 | return re.compile(f"^[ \f\v\t]*?(#)\s*({_tmp})(?!\S)", re.MULTILINE) 114 | 115 | def _repl_directives(code_str): 116 | def _fmt(x): return f"#| {_subv1(x[2].replace('-', '_').strip())}" 117 | return _re_v1().sub(_fmt, code_str) 118 | 119 | # %% ../nbs/api/16_migrate.ipynb 33 120 | def _repl_v1dir(cell): 121 | "Replace nbdev v1 with v2 directives." 122 | if cell.get('source') and cell.get('cell_type') == 'code': 123 | ss = cell['source'].splitlines() 124 | first_code = first_code_ln(ss, re_pattern=_re_v1()) 125 | if not first_code: first_code = len(ss) 126 | if not ss: pass 127 | else: cell['source'] = '\n'.join([_repl_directives(c) for c in ss[:first_code]] + ss[first_code:]) 128 | 129 | # %% ../nbs/api/16_migrate.ipynb 38 130 | _re_callout = re.compile(r'^>\s(Warning|Note|Important|Tip):(.*)', flags=re.MULTILINE) 131 | def _co(x): return ":::{.callout-"+x[1].lower()+"}\n\n" + f"{x[2].strip()}\n\n" + ":::" 132 | def _convert_callout(s): 133 | "Convert nbdev v1 to v2 callouts." 134 | return _re_callout.sub(_co, s) 135 | 136 | # %% ../nbs/api/16_migrate.ipynb 45 137 | _re_video = re.compile(r'^>\syoutube:(.*)', flags=re.MULTILINE) 138 | def _v(x): return "{{< " + f"video {x[1].strip()}" + " >}}" 139 | def _convert_video(s): 140 | "Replace nbdev v1 with v2 video embeds." 141 | return _re_video.sub(_v, s) 142 | 143 | # %% ../nbs/api/16_migrate.ipynb 49 144 | _shortcuts = compose(_convert_video, _convert_callout) 145 | 146 | def _repl_v1shortcuts(cell): 147 | "Replace nbdev v1 with v2 callouts." 148 | if cell.get('source') and cell.get('cell_type') == 'markdown': 149 | cell['source'] = _shortcuts(cell['source']) 150 | 151 | # %% ../nbs/api/16_migrate.ipynb 50 152 | def migrate_nb(path, overwrite=True): 153 | "Migrate Notebooks from nbdev v1 and fastpages." 154 | nbp = NBProcessor(path, procs=[FrontmatterProc, MigrateProc, _repl_v1shortcuts, _repl_v1dir], rm_directives=False) 155 | nbp.process() 156 | if overwrite: write_nb(nbp.nb, path) 157 | return nbp.nb 158 | 159 | # %% ../nbs/api/16_migrate.ipynb 51 160 | def migrate_md(path, overwrite=True): 161 | "Migrate Markdown Files from fastpages." 162 | txt = fp_md_fm(path) 163 | if overwrite: path.write_text(txt) 164 | return txt 165 | 166 | # %% ../nbs/api/16_migrate.ipynb 52 167 | @call_parse 168 | def nbdev_migrate( 169 | path:str=None, # A path or glob containing notebooks and markdown files to migrate 170 | no_skip:bool=False, # Do not skip directories beginning with an underscore 171 | ): 172 | "Convert all markdown and notebook files in `path` from v1 to v2" 173 | _skip_re = None if no_skip else '^[_.]' 174 | if path is None: path = get_config().nbs_path 175 | for f in globtastic(path, file_re='(.ipynb$)|(.md$)', skip_folder_re=_skip_re, func=Path): 176 | try: 177 | if f.name.endswith('.ipynb'): migrate_nb(f) 178 | if f.name.endswith('.md'): migrate_md(f) 179 | except Exception as e: raise Exception(f'Error in migrating file: {f}') from e 180 | -------------------------------------------------------------------------------- /nbs/api/06_sync.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#|default_exp sync" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# sync\n", 17 | "\n", 18 | "> Propagate small changes in the library back to notebooks\n", 19 | "- order: 6" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "The library is primarily developed in notebooks so any big changes should be made there. But sometimes, it's easier to fix small bugs or typos in the modules directly. `nbdev_update` is the function that will propagate those changes back to the corresponding notebooks. Note that you can't create new cells or reorder cells with that functionality, so your corrections should remain limited." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "#|export\n", 36 | "from nbdev.imports import *\n", 37 | "from nbdev.config import *\n", 38 | "from nbdev.maker import *\n", 39 | "from nbdev.process import *\n", 40 | "from nbdev.process import _partition_cell\n", 41 | "from nbdev.export import *\n", 42 | "from nbdev.doclinks import _iter_py_cells\n", 43 | "\n", 44 | "from execnb.nbio import *\n", 45 | "from fastcore.script import *\n", 46 | "from fastcore.xtras import *\n", 47 | "\n", 48 | "import ast\n", 49 | "from importlib import import_module" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "#|hide\n", 59 | "from fastcore.test import *" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "#|export\n", 69 | "def absolute_import(name, fname, level):\n", 70 | " \"Unwarps a relative import in `name` according to `fname`\"\n", 71 | " if not level: return name\n", 72 | " mods = fname.split(os.path.sep)\n", 73 | " if not name: return '.'.join(mods)\n", 74 | " return '.'.join(mods[:len(mods)-level+1]) + f\".{name}\"" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "test_eq(absolute_import('xyz', 'nbdev', 0), 'xyz')\n", 84 | "test_eq(absolute_import('', 'nbdev', 1), 'nbdev')\n", 85 | "test_eq(absolute_import(None, 'nbdev', 1), 'nbdev')\n", 86 | "test_eq(absolute_import('core', 'nbdev', 1), 'nbdev.core')\n", 87 | "test_eq(absolute_import('core', 'nbdev/vision', 2), 'nbdev.core') # from ..core import *\n", 88 | "test_eq(absolute_import('transform', 'nbdev/vision', 1), 'nbdev.vision.transform') # from .transform import *\n", 89 | "test_eq(absolute_import('notebook.core', 'nbdev/data', 2), 'nbdev.notebook.core') # from ..notebook.core import *" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "#|export\n", 99 | "@functools.lru_cache(maxsize=None)\n", 100 | "def _mod_files():\n", 101 | " midx = import_module(f'{get_config().lib_path.name}._modidx')\n", 102 | " return L(files for mod in midx.d['syms'].values() for _,files in mod.values()).unique()" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "#|export\n", 112 | "_re_import = re.compile(\"from\\s+\\S+\\s+import\\s+\\S\")" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "#|hide\n", 122 | "assert _re_import.match('from foo import bar')\n", 123 | "assert not _re_import.match('#from foo import bar')" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "#|export\n", 133 | "def _to_absolute(code, py_path, lib_dir):\n", 134 | " if not _re_import.search(code): return code\n", 135 | " res = update_import(code, ast.parse(code).body, str(py_path.relative_to(lib_dir).parent), absolute_import)\n", 136 | " return ''.join(res) if res else code" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "#|export\n", 146 | "def _update_nb(nb_path, cells, lib_dir):\n", 147 | " \"Update notebook `nb_path` with contents from `cells`\"\n", 148 | " nbp = NBProcessor(nb_path, ExportModuleProc(), rm_directives=False)\n", 149 | " nbp.process()\n", 150 | " for cell in cells:\n", 151 | " assert cell.nb_path == nb_path\n", 152 | " nbcell = nbp.nb.cells[cell.idx]\n", 153 | " dirs,_ = _partition_cell(nbcell, 'python')\n", 154 | " nbcell.source = ''.join(dirs) + _to_absolute(cell.code, cell.py_path, lib_dir)\n", 155 | " write_nb(nbp.nb, nb_path)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "#|export\n", 165 | "def _update_mod(py_path, lib_dir):\n", 166 | " \"Propagate changes from cells in module `py_path` to corresponding notebooks\"\n", 167 | " py_cells = L(_iter_py_cells(py_path)).filter(lambda o: o.nb != 'auto')\n", 168 | " for nb_path,cells in groupby(py_cells, 'nb_path').items(): _update_nb(nb_path, cells, lib_dir)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "#|hide\n", 178 | "assert min(_mod_files().map(lambda x: x.endswith('.py'))) is True" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": null, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "#|export\n", 188 | "@call_parse\n", 189 | "def nbdev_update(fname:str=None): # A Python file name to update\n", 190 | " \"Propagate change in modules matching `fname` to notebooks that created them\"\n", 191 | " if fname and fname.endswith('.ipynb'): raise ValueError(\"`nbdev_update` operates on .py files. If you wish to convert notebooks instead, see `nbdev_export`.\")\n", 192 | " if os.environ.get('IN_TEST',0): return\n", 193 | " cfg = get_config()\n", 194 | " fname = Path(fname or cfg.lib_path)\n", 195 | " lib_dir = cfg.lib_path.parent\n", 196 | " files = globtastic(fname, file_glob='*.py', skip_folder_re='^[_.]').filter(lambda x: str(Path(x).absolute().relative_to(lib_dir) in _mod_files()))\n", 197 | " files.map(_update_mod, lib_dir=lib_dir)" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "#|hide\n", 207 | "# nbdev_update(\"../nbdev/sync.py\")" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "metadata": {}, 213 | "source": [ 214 | "## Export -" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "#|hide\n", 224 | "#|eval: false\n", 225 | "from nbdev.doclinks import nbdev_export\n", 226 | "nbdev_export()" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [] 235 | } 236 | ], 237 | "metadata": { 238 | "kernelspec": { 239 | "display_name": "Python 3 (ipykernel)", 240 | "language": "python", 241 | "name": "python3" 242 | } 243 | }, 244 | "nbformat": 4, 245 | "nbformat_minor": 4 246 | } 247 | -------------------------------------------------------------------------------- /nbs/api/04_export.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#|hide\n", 10 | "#|default_exp export" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "# export\n", 18 | "> Exporting a notebook to a library\n", 19 | "- order: 4" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "#|export\n", 29 | "from nbdev.config import *\n", 30 | "from nbdev.maker import *\n", 31 | "from nbdev.imports import *\n", 32 | "from nbdev.process import *\n", 33 | "\n", 34 | "from fastcore.script import *\n", 35 | "from fastcore.basics import *\n", 36 | "from fastcore.imports import *\n", 37 | "\n", 38 | "from collections import defaultdict" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "#|hide\n", 48 | "from fastcore.test import *\n", 49 | "from pdb import set_trace\n", 50 | "from importlib import reload\n", 51 | "from fastcore import shutil\n", 52 | "from execnb.nbio import read_nb" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "#|export\n", 62 | "class ExportModuleProc:\n", 63 | " \"A processor which exports code to a module\"\n", 64 | " def begin(self): self.modules,self.in_all = defaultdict(L),defaultdict(L)\n", 65 | " def _default_exp_(self, cell, exp_to): self.default_exp = exp_to\n", 66 | " def _exporti_(self, cell, exp_to=None): self.modules[ifnone(exp_to, '#')].append(cell)\n", 67 | " def _export_(self, cell, exp_to=None):\n", 68 | " self._exporti_(cell, exp_to)\n", 69 | " self.in_all[ifnone(exp_to, '#')].append(cell)\n", 70 | " _exports_=_export_" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "Specify `dest` where the module(s) will be exported to, and optionally a class to use to create the module (`ModuleMaker`, by default).\n", 78 | "\n", 79 | "Exported cells are stored in a `dict` called `modules`, where the keys are the modules exported to. Those without an explicit module are stored in the `'#'` key, which will be exported to `default_exp`." 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "everything_fn = '../../tests/01_everything.ipynb'\n", 89 | "\n", 90 | "exp = ExportModuleProc()\n", 91 | "proc = NBProcessor(everything_fn, exp)\n", 92 | "proc.process()\n", 93 | "test_eq(exp.default_exp, 'everything')\n", 94 | "assert 'print_function' in exp.modules['#'][0].source\n", 95 | "assert 'h_n' in exp.in_all['some.thing'][0].source" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": null, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "#|export\n", 105 | "def black_format(cell, # Cell to format\n", 106 | " force=False): # Turn black formatting on regardless of settings.ini\n", 107 | " \"Processor to format code with `black`\"\n", 108 | " try: cfg = get_config()\n", 109 | " except FileNotFoundError: return\n", 110 | " if (not cfg.black_formatting and not force) or cell.cell_type != 'code': return\n", 111 | " try: import black\n", 112 | " except: raise ImportError(\"You must install black: `pip install black` if you wish to use black formatting with nbdev\")\n", 113 | " else:\n", 114 | " _format_str = partial(black.format_str, mode = black.Mode())\n", 115 | " try: cell.source = _format_str(cell.source).strip()\n", 116 | " except: pass" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "_cell = read_nb('../../tests/black.ipynb')['cells'][0]\n", 126 | "black_format(_cell, force=True)\n", 127 | "test_eq(_cell.source, 'j = [1, 2, 3]')" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "#|export\n", 137 | "def nb_export(nbname, lib_path=None, procs=black_format, debug=False, mod_maker=ModuleMaker, name=None):\n", 138 | " \"Create module(s) from notebook\"\n", 139 | " if lib_path is None: lib_path = get_config().lib_path\n", 140 | " exp = ExportModuleProc()\n", 141 | " nb = NBProcessor(nbname, [exp]+L(procs), debug=debug)\n", 142 | " nb.process()\n", 143 | " for mod,cells in exp.modules.items():\n", 144 | " all_cells = exp.in_all[mod]\n", 145 | " nm = ifnone(name, getattr(exp, 'default_exp', None) if mod=='#' else mod)\n", 146 | " if not nm:\n", 147 | " warn(f\"Notebook '{nbname}' uses `#|export` without `#|default_exp` cell.\\n\"\n", 148 | " \"Note nbdev2 no longer supports nbdev1 syntax. Run `nbdev_migrate` to upgrade.\\n\"\n", 149 | " \"See https://nbdev.fast.ai/getting_started.html for more information.\")\n", 150 | " return\n", 151 | " mm = mod_maker(dest=lib_path, name=nm, nb_path=nbname, is_new=bool(name) or mod=='#')\n", 152 | " mm.make(cells, all_cells, lib_path=lib_path)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "Let's check we can import a test file:" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "#|eval: false\n", 169 | "shutil.rmtree('tmp', ignore_errors=True)\n", 170 | "nb_export('../../tests/00_some.thing.ipynb', 'tmp')\n", 171 | "\n", 172 | "g = exec_new('import tmp.some.thing')\n", 173 | "test_eq(g['tmp'].some.thing.__all__, ['a'])\n", 174 | "test_eq(g['tmp'].some.thing.a, 1)" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "We'll also check that our 'everything' file exports correctly:" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "#|eval: false\n", 191 | "nb_export(everything_fn, 'tmp')\n", 192 | "\n", 193 | "g = exec_new('import tmp.everything; from tmp.everything import *')\n", 194 | "_alls = L(\"a b d e m n o p q\".split())\n", 195 | "for s in _alls.map(\"{}_y\"): assert s in g, s\n", 196 | "for s in \"c_y_nall _f_y_nall g_n h_n i_n j_n k_n l_n\".split(): assert s not in g, s\n", 197 | "for s in _alls.map(\"{}_y\") + [\"c_y_nall\", \"_f_y_nall\"]: assert hasattr(g['tmp'].everything,s), s" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "That notebook should also export one extra function to `tmp.some.thing`:" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "#|eval: false\n", 214 | "del(sys.modules['tmp.some.thing']) # remove from module cache\n", 215 | "g = exec_new('import tmp.some.thing')\n", 216 | "test_eq(g['tmp'].some.thing.__all__, ['a','h_n'])\n", 217 | "test_eq(g['tmp'].some.thing.h_n(), None)" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "## Export -" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "#|eval: false\n", 234 | "Path('../nbdev/export.py').unlink(missing_ok=True)\n", 235 | "nb_export('04a_export.ipynb')\n", 236 | "\n", 237 | "g = exec_new('import nbdev.export')\n", 238 | "assert hasattr(g['nbdev'].export, 'nb_export')" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [] 247 | } 248 | ], 249 | "metadata": { 250 | "kernelspec": { 251 | "display_name": "Python 3 (ipykernel)", 252 | "language": "python", 253 | "name": "python3" 254 | } 255 | }, 256 | "nbformat": 4, 257 | "nbformat_minor": 4 258 | } 259 | -------------------------------------------------------------------------------- /nbs/api/09_frontmatter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "f14f1df0-7110-452f-beff-430a8f0aec73", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#|default_exp frontmatter" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "f6b8b4a6", 16 | "metadata": {}, 17 | "source": [ 18 | "# frontmatter\n", 19 | "\n", 20 | "> A YAML and formatted-markdown frontmatter processor\n", 21 | "- order: 9" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "2398f5ef-06d3-4890-8a54-7cf4f81f3894", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "#|export\n", 32 | "from nbdev.imports import *\n", 33 | "from nbdev.process import *\n", 34 | "from nbdev.doclinks import _nbpath2html\n", 35 | "\n", 36 | "from execnb.nbio import *\n", 37 | "from fastcore.imports import *\n", 38 | "import yaml" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "ce00cb74", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "#|hide\n", 49 | "from fastcore.test import *" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "id": "9d36f27f", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "#|hide\n", 60 | "_test_file = '../../tests/docs_test.ipynb'" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "6d13ecdb", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "#|export\n", 71 | "_RE_FM_BASE=r'''^---\\s*\n", 72 | "(.*?\\S+.*?)\n", 73 | "---\\s*'''\n", 74 | "\n", 75 | "_re_fm_nb = re.compile(_RE_FM_BASE+'$', flags=re.DOTALL)\n", 76 | "_re_fm_md = re.compile(_RE_FM_BASE, flags=re.DOTALL)\n", 77 | "\n", 78 | "def _fm2dict(s:str, nb=True):\n", 79 | " \"Load YAML frontmatter into a `dict`\"\n", 80 | " re_fm = _re_fm_nb if nb else _re_fm_md\n", 81 | " match = re_fm.search(s.strip())\n", 82 | " return yaml.safe_load(match.group(1)) if match else {}\n", 83 | "\n", 84 | "def _md2dict(s:str):\n", 85 | " \"Convert H1 formatted markdown cell to frontmatter dict\"\n", 86 | " if '#' not in s: return {}\n", 87 | " m = re.search(r'^#\\s+(\\S.*?)\\s*$', s, flags=re.MULTILINE)\n", 88 | " if not m: return {}\n", 89 | " res = {'title': m.group(1)}\n", 90 | " m = re.search(r'^>\\s+(\\S.*?)\\s*$', s, flags=re.MULTILINE)\n", 91 | " if m: res['description'] = m.group(1)\n", 92 | " r = re.findall(r'^-\\s+(\\S.*:.*\\S)\\s*$', s, flags=re.MULTILINE)\n", 93 | " if r:\n", 94 | " try: res.update(yaml.safe_load('\\n'.join(r)))\n", 95 | " except Exception as e: warn(f'Failed to create YAML dict for:\\n{r}\\n\\n{e}\\n')\n", 96 | " return res" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "1b5d9d32", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "#|export\n", 107 | "def _dict2fm(d): return f'---\\n{yaml.dump(d)}\\n---\\n\\n'\n", 108 | "def _insertfm(nb, fm): nb.cells.insert(0, mk_cell(_dict2fm(fm), 'raw'))\n", 109 | "\n", 110 | "class FrontmatterProc(Processor):\n", 111 | " \"A YAML and formatted-markdown frontmatter processor\"\n", 112 | " def begin(self): self.fm = getattr(self.nb, 'frontmatter_', {})\n", 113 | "\n", 114 | " def _update(self, f, cell):\n", 115 | " s = cell.get('source')\n", 116 | " if not s: return\n", 117 | " d = f(s)\n", 118 | " if not d: return\n", 119 | " self.fm.update(d)\n", 120 | " cell.source = None\n", 121 | "\n", 122 | " def cell(self, cell):\n", 123 | " if cell.cell_type=='raw': self._update(_fm2dict, cell)\n", 124 | " elif cell.cell_type=='markdown' and 'title' not in self.fm: self._update(_md2dict, cell)\n", 125 | "\n", 126 | " def end(self):\n", 127 | " self.nb.frontmatter_ = self.fm\n", 128 | " if not self.fm: return\n", 129 | " self.fm.update({'output-file': _nbpath2html(Path(self.nb.path_)).name})\n", 130 | " _insertfm(self.nb, self.fm)" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "id": "d1a5c7c1", 136 | "metadata": {}, 137 | "source": [ 138 | "YAML frontmatter can be added to notebooks in one of two ways:\n", 139 | "\n", 140 | "1. By adding a raw notebook cell with `---` as the first and last lines, and YAML between them, or\n", 141 | "2. A specially formatted markdown cell. The first line should be start with a single `#` (creating an H1 heading), and becomes the title. Then, optionally, a line beginning with `>` (creating a quote block), which becomes the description. Finally, zero or more lines beginning with `- ` (creating a list), each of which contains YAML. (If you already have \"title\" defined in frontmatter in a raw cell, then markdown cells will be ignored.)\n", 142 | "\n", 143 | "For instance, our test notebook contains the following markdown cell:\n", 144 | "\n", 145 | "```\n", 146 | "# a title\n", 147 | "> A description\n", 148 | "- key1: value1\n", 149 | "- key2: value2\n", 150 | "- categories: [c1, c2]\n", 151 | "```\n", 152 | "\n", 153 | "It also contains the following raw cell:\n", 154 | "\n", 155 | "```\n", 156 | "---\n", 157 | "execute:\n", 158 | " echo: false\n", 159 | "---\n", 160 | "```\n", 161 | "\n", 162 | "When we process with `FrontmatterProc`, these will both be removed, and a single raw cell will be added to the top, containing the combined YAML frontmatter:" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "3c0031d2", 169 | "metadata": {}, 170 | "outputs": [ 171 | { 172 | "name": "stdout", 173 | "output_type": "stream", 174 | "text": [ 175 | "---\n", 176 | "categories:\n", 177 | "- c1\n", 178 | "- c2\n", 179 | "description: A description\n", 180 | "execute:\n", 181 | " echo: false\n", 182 | "key1: value1\n", 183 | "key2: value2\n", 184 | "output-file: docs_test.html\n", 185 | "title: a title\n", 186 | "\n", 187 | "---\n", 188 | "\n", 189 | "\n" 190 | ] 191 | } 192 | ], 193 | "source": [ 194 | "nbp = NBProcessor(_test_file, procs=FrontmatterProc)\n", 195 | "nbp.process()\n", 196 | "print(nbp.nb.cells[0].source)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "id": "d71f4a28", 202 | "metadata": {}, 203 | "source": [ 204 | "In addition, a `frontmatter_` attr will be added to the notebook, containing this information as a `dict`:" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "id": "1f592909", 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "data": { 215 | "text/plain": [ 216 | "{'execute': {'echo': False},\n", 217 | " 'title': 'a title',\n", 218 | " 'description': 'A description',\n", 219 | " 'key1': 'value1',\n", 220 | " 'key2': 'value2',\n", 221 | " 'categories': ['c1', 'c2'],\n", 222 | " 'output-file': 'docs_test.html'}" 223 | ] 224 | }, 225 | "execution_count": null, 226 | "metadata": {}, 227 | "output_type": "execute_result" 228 | } 229 | ], 230 | "source": [ 231 | "d = nbp.nb.frontmatter_\n", 232 | "d" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "id": "5e5becda", 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "#|hide\n", 243 | "test_eq(d['description'], 'A description')\n", 244 | "test_eq(d['categories'], ['c1','c2'])\n", 245 | "test_eq(d['output-file'], 'docs_test.html')" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "id": "4af909f4", 251 | "metadata": {}, 252 | "source": [ 253 | "## Export -" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "id": "079a05ac", 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "#|hide\n", 264 | "import nbdev; nbdev.nbdev_export()" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "id": "d28956a7", 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [] 274 | } 275 | ], 276 | "metadata": { 277 | "kernelspec": { 278 | "display_name": "Python 3 (ipykernel)", 279 | "language": "python", 280 | "name": "python3" 281 | } 282 | }, 283 | "nbformat": 4, 284 | "nbformat_minor": 5 285 | } 286 | --------------------------------------------------------------------------------