├── docs ├── _static │ ├── demo.png │ └── plotly_logo.png ├── binder_cfg │ └── requirements.txt ├── tutorials │ ├── README.md │ └── plot_parse.py ├── gallery_conf.py ├── long_description.md ├── changelog.md └── index.md ├── src └── mkdocs_gallery │ ├── static │ ├── no_image.png │ ├── broken_example.png │ ├── sg_gallery-binder.css │ ├── sg_gallery-dataframe.css │ ├── binder_badge_logo.svg │ ├── sg_gallery-rendered-html.css │ └── sg_gallery.css │ ├── errors.py │ ├── mkdocs_compatibility.py │ ├── __init__.py │ ├── downloads.py │ ├── sorting.py │ ├── py_source_parser.py │ ├── binder.py │ ├── notebook.py │ ├── utils.py │ └── backreferences.py ├── noxfile-requirements.txt ├── examples ├── local_module.py ├── no_output │ ├── README.md │ ├── plot_syntaxerror.py │ ├── plot_strings.py │ ├── just_code.py │ └── plot_raise.py ├── README.md ├── plot_10_mayavi.py ├── plot_08_animations.py ├── plot_07_sys_argv.py ├── plot_04b_provide_thumbnail.py ├── plot_02_seaborn.py ├── plot_05_unicode_everywhere.py ├── plot_01_exp.py ├── plot_04_choose_thumbnail.py ├── plot_11_pyvista.py ├── plot_06_function_identifier.py ├── plot_12_async.py ├── plot_09_plotly.py ├── plot_00_sin.py └── plot_03_capture_repr.py ├── tests ├── __init__.py ├── test_packaging.py ├── test_utils.py ├── test_config.py ├── reference_parse.txt └── test_gen_single.py ├── ci_tools ├── flake8-requirements.txt ├── check_python_version.py ├── github_release.py └── nox_utils.py ├── .github ├── workflows │ ├── updater.yml │ └── base.yml └── pull_request_template.md ├── pyproject.toml ├── .zenodo.json ├── setup.py ├── LICENSE ├── .gitignore ├── mkdocs.yml ├── setup.cfg └── README.md /docs/_static/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarie/mkdocs-gallery/HEAD/docs/_static/demo.png -------------------------------------------------------------------------------- /docs/_static/plotly_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarie/mkdocs-gallery/HEAD/docs/_static/plotly_logo.png -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarie/mkdocs-gallery/HEAD/src/mkdocs_gallery/static/no_image.png -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/broken_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarie/mkdocs-gallery/HEAD/src/mkdocs_gallery/static/broken_example.png -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/sg_gallery-binder.css: -------------------------------------------------------------------------------- 1 | /* CSS for binder integration */ 2 | 3 | div.binder-badge { 4 | margin: 1em auto; 5 | vertical-align: middle; 6 | } 7 | -------------------------------------------------------------------------------- /noxfile-requirements.txt: -------------------------------------------------------------------------------- 1 | virtualenv 2 | nox 3 | toml 4 | setuptools<72 # later versions do not read 'tests_require' from setup.cfg anymore 5 | setuptools_scm # used in 'release' 6 | keyring # used in 'release' 7 | -------------------------------------------------------------------------------- /examples/local_module.py: -------------------------------------------------------------------------------- 1 | """ 2 | Local module 3 | ============ 4 | 5 | This example demonstrates how local modules can be imported. 6 | This module is imported in the example 7 | [Plotting the exponential function](./plot_1_exp.md). 8 | """ 9 | 10 | N = 100 11 | -------------------------------------------------------------------------------- /examples/no_output/README.md: -------------------------------------------------------------------------------- 1 | 4 | ## No image output examples 5 | 6 | This section gathers examples which don't produce any figures. Some examples 7 | only output to standard output, others demonstrate how Mkdocs-Gallery handles 8 | examples with errors. 9 | -------------------------------------------------------------------------------- /docs/binder_cfg/requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements for binder to run the example notebooks included in mkdocs-gallery's web page. 2 | # (These aren't required to use mkdocs-gallery, just to run the examples included) 3 | numpy 4 | matplotlib 5 | pillow 6 | seaborn 7 | joblib 8 | mkdocs-gallery 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | -------------------------------------------------------------------------------- /docs/tutorials/README.md: -------------------------------------------------------------------------------- 1 | Notebook style example 2 | ====================== 3 | 4 | You can have multiple galleries, each one for different uses. For example, 5 | one gallery of examples and a different gallery for tutorials. 6 | 7 | This gallery demonstrates the ability of Mkdocs-Gallery to transform a file 8 | with a Jupyter notebook style structure (i.e., with alternating text and code). 9 | -------------------------------------------------------------------------------- /examples/no_output/plot_syntaxerror.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Example with SyntaxError 4 | ======================== 5 | 6 | Sphinx-Gallery uses Python's AST parser, thus you need to have written 7 | valid python code for Sphinx-Gallery to parse it. If your script has a 8 | SyntaxError you'll be presented the traceback and the original code. 9 | """ 10 | # Code source: Óscar Nájera 11 | # License: BSD 3 clause 12 | 13 | Invalid Python code 14 | -------------------------------------------------------------------------------- /examples/no_output/plot_strings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Constrained Text output frame 4 | ============================= 5 | 6 | This example captures the standard output and includes it in the 7 | example. If output is too long it becomes automatically 8 | framed into a text area. 9 | """ 10 | 11 | # Code source: Óscar Nájera 12 | # License: BSD 3 clause 13 | 14 | print('This is a long test Output\n' * 50) 15 | 16 | #################################### 17 | # One line out 18 | 19 | print('one line out') 20 | -------------------------------------------------------------------------------- /ci_tools/flake8-requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools_scm>=3,<4 2 | flake8>=3.6,<4 3 | flake8-html>=0.4,<1 4 | flake8-bandit>=2.1.1,<3 5 | bandit<1.7.3 # temporary until this is fixed https://github.com/tylerwince/flake8-bandit/issues/21 6 | flake8-bugbear>=20.1.0,<21.0.0 7 | flake8-docstrings>=1.5,<2 8 | flake8-print>=3.1.1,<4 9 | flake8-tidy-imports>=4.2.1,<5 10 | flake8-copyright==0.2.2 # Internal forked repo to fix an issue, keep specific version 11 | pydocstyle>=5.1.1,<6 12 | pycodestyle>=2.6.0,<3 13 | mccabe>=0.6.1,<1 14 | naming>=0.5.1,<1 15 | pyflakes>=2.2,<3 16 | genbadge[flake8] 17 | jinja2>=3.0.0,<3.1.0 18 | -------------------------------------------------------------------------------- /examples/no_output/just_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A short Python script 4 | ===================== 5 | 6 | This demonstrates an example `.py` file that is not executed when gallery is 7 | generated (see 8 | [Parsing and executing examples via matching patterns](https://sphinx-gallery.github.io/stable/configuration.html#build-pattern)) 9 | but nevertheless gets included as an example. Note that no output is captured as this file is not executed. 10 | """ 11 | 12 | # Code source: Óscar Nájera 13 | # License: BSD 3 clause 14 | from __future__ import print_function 15 | print([i for i in range(10)]) 16 | -------------------------------------------------------------------------------- /tests/test_packaging.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pkg_resources 4 | 5 | from mkdocs_gallery import glr_path_static 6 | from mkdocs_gallery.scrapers import _find_image_ext 7 | 8 | 9 | def test_packaged_static(): 10 | """Test that the static resources can be found in the package.""" 11 | binder_badge = pkg_resources.resource_string("mkdocs_gallery", "static/binder_badge_logo.svg").decode("utf-8") 12 | assert binder_badge.startswith(" 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | """ 7 | Common errors 8 | """ 9 | 10 | from mkdocs.exceptions import PluginError 11 | 12 | 13 | class MkdocsGalleryError(PluginError): 14 | """The base class of all errors in this plugin. 15 | 16 | See https://www.mkdocs.org/dev-guide/plugins/#handling-errors. 17 | """ 18 | 19 | 20 | class ExtensionError(MkdocsGalleryError): 21 | pass 22 | 23 | 24 | class ConfigError(MkdocsGalleryError): 25 | pass 26 | -------------------------------------------------------------------------------- /.github/workflows/updater.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Version Updater 2 | 3 | # Controls when the action will run. 4 | on: 5 | workflow_dispatch: 6 | schedule: 7 | # Automatically run on every first day of the month 8 | - cron: '0 0 1 * *' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4.1.1 16 | with: 17 | # [Required] Access token with `workflow` scope. 18 | token: ${{ secrets.WORKFLOW_SECRET }} 19 | 20 | - name: Run GitHub Actions Version Updater 21 | uses: saadmk11/github-actions-version-updater@v0.8.1 22 | with: 23 | # [Required] Access token with `workflow` scope. 24 | token: ${{ secrets.WORKFLOW_SECRET }} 25 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom page title, see mkdocs-material reference 3 | --- 4 | 5 | # Gallery of Examples 6 | 7 | This page consists of the 'General example' gallery and a sub-gallery, 8 | 'No image output examples'. This sub-gallery is generated from a 9 | sub-directory within the general examples directory. The file structure of 10 | this gallery looks like this: 11 | 12 | ``` 13 | examples/ # base 'Gallery of Examples' directory 14 | ├── README.txt 15 | ├── <.py files> 16 | └── no_output/ # generates the 'No image output examples' sub-gallery 17 | ├── README.txt 18 | └── <.py files> 19 | ``` 20 | 21 | ## General examples 22 | 23 | This gallery consists of introductory examples and examples demonstrating 24 | specific features of Mkdocs-Gallery. 25 | -------------------------------------------------------------------------------- /examples/plot_10_mayavi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example with the mayavi graphing library 3 | ======================================== 4 | 5 | Mkdocs-Gallery supports examples made with the 6 | [mayavi library](http://docs.enthought.com/mayavi/mayavi/). 7 | 8 | This mayavi demo is from the 9 | [mayavi documentation](https://docs.enthought.com/mayavi/mayavi/mlab.html#a-demo). 10 | """ 11 | 12 | # Create the data. 13 | from numpy import pi, sin, cos, mgrid 14 | dphi, dtheta = pi/250.0, pi/250.0 15 | [phi,theta] = mgrid[0:pi+dphi*1.5:dphi,0:2*pi+dtheta*1.5:dtheta] 16 | m0 = 4; m1 = 3; m2 = 2; m3 = 3; m4 = 6; m5 = 2; m6 = 6; m7 = 4; 17 | r = sin(m0*phi)**m1 + cos(m2*phi)**m3 + sin(m4*theta)**m5 + cos(m6*theta)**m7 18 | x = r*sin(phi)*cos(theta) 19 | y = r*cos(phi) 20 | z = r*sin(phi)*sin(theta) 21 | 22 | # View it. 23 | from mayavi import mlab 24 | s = mlab.mesh(x, y, z) 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=39.2", 4 | "setuptools_scm", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | # pip: no ! does not work in old python 2.7 and not recommended here 9 | # https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html#basic-use 10 | 11 | # [tool.conda] 12 | # Declare that the following packages should be installed with conda instead of pip 13 | # Note: this includes packages declared everywhere, here and in setup.cfg 14 | # conda_packages = [ 15 | # "setuptools", 16 | # "wheel", 17 | # "pip", 18 | # "numpy", 19 | # "pandas", 20 | # "scikit-learn", 21 | # "matplotlib", 22 | # "statsmodels", 23 | # "plotly", 24 | # ] 25 | # pytest: not with conda ! does not work in old python 2.7 and 3.5 26 | 27 | [tool.black] 28 | line-length = 120 29 | -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "mkdocs-gallery: a mkdocs plugin to generate example galleries from python scripts", 3 | "description": "

`mkdocs-gallery` brings the great features from `sphinx-gallery` to `mkdocs`, leveraging `mkdocs-material` and `pymdown-extensions`.

", 4 | "language": "eng", 5 | "license": { 6 | "id": "bsd-license" 7 | }, 8 | "keywords": [ 9 | "python", 10 | "gallery", 11 | "web", 12 | "page", 13 | "generator", 14 | "figure", 15 | "jupyter", 16 | "notebook", 17 | "binder", 18 | "example", 19 | "code", 20 | "latex", 21 | "mkdocs", 22 | "sphinx" 23 | ], 24 | "creators": [ 25 | { 26 | "orcid": "0000-0002-5929-1047", 27 | "affiliation": "Schneider Electric", 28 | "name": "Sylvain Mari\u00e9" 29 | }, 30 | { 31 | "name": "Various github contributors" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /examples/plot_08_animations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Matplotlib animation support 3 | ============================ 4 | 5 | Show a Matplotlib animation, which should end up nicely embedded below. 6 | 7 | In order to enable support for animations `'matplotlib_animations'` 8 | must be set to `True` in the sphinx gallery 9 | [configuration](https://sphinx-gallery.github.io/stable/configuration.html#image-scrapers). 10 | """ 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import matplotlib.animation as animation 15 | 16 | # Adapted from 17 | # https://matplotlib.org/gallery/animation/basic_example.html 18 | 19 | 20 | def _update_line(num): 21 | line.set_data(data[..., :num]) 22 | return line, 23 | 24 | 25 | fig, ax = plt.subplots() 26 | data = np.random.RandomState(0).rand(2, 25) 27 | line, = ax.plot([], [], 'r-') 28 | ax.set(xlim=(0, 1), ylim=(0, 1)) 29 | ani = animation.FuncAnimation(fig, _update_line, 25, interval=100, blit=True) 30 | -------------------------------------------------------------------------------- /examples/plot_07_sys_argv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Using `sys.argv` in examples 3 | ============================== 4 | 5 | This example demonstrates the use of `sys.argv` in example `.py` files. 6 | 7 | By default, all example `.py` files will be run by Mkdocs-Gallery **without** any 8 | arguments. Notice below that `sys.argv` is a list consisting of only the 9 | file name. Further, any arguments added will take on the default value. 10 | 11 | This behavior can be changed by using the `reset_argv` option in the sphinx configuration, 12 | see [Passing command line arguments to example scripts](https://sphinx-gallery.github.io/stable/configuration.html#reset-argv). 13 | 14 | """ 15 | 16 | import argparse 17 | import sys 18 | 19 | parser = argparse.ArgumentParser(description='Toy parser') 20 | parser.add_argument('--option', default='default', 21 | help='a dummy optional argument') 22 | print('sys.argv:', sys.argv) 23 | print('parsed args:', parser.parse_args()) 24 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/sg_gallery-dataframe.css: -------------------------------------------------------------------------------- 1 | /* Pandas dataframe css */ 2 | /* Taken from: https://github.com/spatialaudio/nbsphinx/blob/fb3ba670fc1ba5f54d4c487573dbc1b4ecf7e9ff/src/nbsphinx.py#L587-L619 */ 3 | 4 | table.dataframe { 5 | border: none !important; 6 | border-collapse: collapse; 7 | border-spacing: 0; 8 | border-color: transparent; 9 | color: black; 10 | font-size: 12px; 11 | table-layout: fixed; 12 | } 13 | table.dataframe thead { 14 | border-bottom: 1px solid black; 15 | vertical-align: bottom; 16 | } 17 | table.dataframe tr, 18 | table.dataframe th, 19 | table.dataframe td { 20 | text-align: right; 21 | vertical-align: middle; 22 | padding: 0.5em 0.5em; 23 | line-height: normal; 24 | white-space: normal; 25 | max-width: none; 26 | border: none; 27 | } 28 | table.dataframe th { 29 | font-weight: bold; 30 | } 31 | table.dataframe tbody tr:nth-child(odd) { 32 | background: #f5f5f5; 33 | } 34 | table.dataframe tbody tr:hover { 35 | background: rgba(66, 165, 245, 0.2); 36 | } 37 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/mkdocs_compatibility.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | """ 7 | Backwards-compatility shims for mkdocs. Only logger is here for now. 8 | """ 9 | 10 | import logging 11 | 12 | from mkdocs.utils import warning_filter 13 | 14 | 15 | def red(msg): 16 | # TODO investigate how we can do this in mkdocs console 17 | return msg 18 | 19 | 20 | def getLogger(name="mkdocs-gallery"): 21 | """From https://github.com/fralau/mkdocs-mermaid2-plugin/pull/19/.""" 22 | log = logging.getLogger("mkdocs.plugins." + name) 23 | log.addFilter(warning_filter) 24 | 25 | # todo what about colors ? currently we remove the argument in each call 26 | 27 | # the verbose method does not exist 28 | log.verbose = log.debug 29 | 30 | return log 31 | 32 | 33 | # status_iterator = sphinx.util.status_iterator 34 | -------------------------------------------------------------------------------- /examples/plot_04b_provide_thumbnail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Providing a figure for the thumbnail image 4 | ========================================== 5 | 6 | This example demonstrates how to provide a figure that is displayed as the 7 | thumbnail. This is done by specifying the keyword-value pair 8 | `mkdocs_gallery_thumbnail_path = 'fig path'` as a comment somewhere below the 9 | docstring in the example file. In this example, we specify that we wish the 10 | figure `demo.png` in the folder `_static` to be used for the thumbnail. 11 | """ 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | # mkdocs_gallery_thumbnail_path = '_static/demo.png' 15 | 16 | # %% 17 | 18 | x = np.linspace(0, 4*np.pi, 301) 19 | y1 = np.sin(x) 20 | y2 = np.cos(x) 21 | 22 | # %% 23 | # Plot 1 24 | # ------ 25 | 26 | plt.figure() 27 | plt.plot(x, y1, label='sin') 28 | plt.plot(x, y2, label='cos') 29 | plt.legend() 30 | plt.show() 31 | 32 | # %% 33 | # Plot 2 34 | # ------ 35 | 36 | plt.figure() 37 | plt.plot(x, y1, label='sin') 38 | plt.plot(x, y2, label='cos') 39 | plt.legend() 40 | plt.xscale('log') 41 | plt.yscale('log') 42 | plt.show() 43 | 44 | -------------------------------------------------------------------------------- /examples/plot_02_seaborn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Seaborn example 4 | =============== 5 | 6 | This example demonstrates a Seaborn plot. Figures produced Matplotlib **and** 7 | by any package that is based on Matplotlib (e.g., Seaborn), will be 8 | captured by default. 9 | See [Image scrapers](https://sphinx-gallery.github.io/stable/configuration.html#image-scrapers) for details. 10 | """ 11 | # Author: Michael Waskom & Lucy Liu 12 | # License: BSD 3 clause 13 | 14 | from __future__ import division, absolute_import, print_function 15 | 16 | 17 | import numpy as np 18 | import matplotlib.pyplot as plt 19 | import seaborn as sns 20 | 21 | # Enforce the use of default set style 22 | 23 | # Create a noisy periodic dataset 24 | y_array = np.array([]) 25 | x_array = np.array([]) 26 | rs = np.random.RandomState(8) 27 | for _ in range(15): 28 | x = np.linspace(0, 30 / 2, 30) 29 | y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, .3, 30) 30 | y_array = np.append(y_array, y) 31 | x_array = np.append(x_array, x) 32 | 33 | # Plot the average over replicates with confidence interval 34 | sns.lineplot(y=y_array, x=x_array) 35 | # to avoid text output 36 | plt.show() 37 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/__init__.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | import os 7 | 8 | try: 9 | # -- Distribution mode -- 10 | # import from _version.py generated by setuptools_scm during release 11 | from ._version import version as __version__ 12 | except ImportError: 13 | # -- Source mode -- 14 | # use setuptools_scm to get the current version from src using git 15 | from os import path as _path 16 | 17 | from setuptools_scm import get_version as _gv 18 | 19 | __version__ = _gv(_path.join(_path.dirname(__file__), _path.pardir)) 20 | 21 | 22 | base_path = os.path.dirname(os.path.abspath(__file__)) 23 | 24 | 25 | def glr_path_static(): 26 | """Returns path to packaged static files""" 27 | return os.path.join(base_path, "static") 28 | 29 | 30 | __all__ = [ 31 | "__version__", 32 | # submodules 33 | "plugin", 34 | # symbols 35 | "glr_path_static", 36 | ] 37 | -------------------------------------------------------------------------------- /examples/plot_05_unicode_everywhere.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Using Unicode everywhere 🤗 4 | =========================== 5 | 6 | This example demonstrates how to include non-ASCII characters, mostly emoji 🎉 7 | to stress test the build and test environments that parse the example files. 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | # 🎉 👍 12 | # Code source: Óscar Nájera 13 | # License: BSD 3 clause 14 | 15 | import numpy as np 16 | import matplotlib.pyplot as plt 17 | 18 | plt.rcParams['font.size'] = 20 19 | plt.rcParams["font.monospace"] = ["DejaVu Sans Mono"] 20 | plt.rcParams["font.family"] = "monospace" 21 | 22 | plt.figure() 23 | x = np.random.randn(100) * 2 + 1 24 | y = np.random.randn(100) * 6 + 3 25 | s = np.random.rand(*x.shape) * 800 + 500 26 | plt.scatter(x, y, s, marker=r'$\oint$') 27 | x = np.random.randn(60) * 7 - 4 28 | y = np.random.randn(60) * 3 - 2 29 | s = s[:x.size] 30 | plt.scatter(x, y, s, alpha=0.5, c='g', marker=r'$\clubsuit$') 31 | plt.xlabel('⇒') 32 | plt.ylabel('⇒') 33 | plt.title('♲' * 10) 34 | print('Std out capture 😎') 35 | # To avoid matplotlib text output 36 | plt.show() 37 | 38 | # %% 39 | # Debug fonts 40 | print(plt.rcParams) 41 | -------------------------------------------------------------------------------- /examples/no_output/plot_raise.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Example that fails to execute 4 | ============================= 5 | 6 | This example demonstrates a code block that raises an error and how any code 7 | blocks that follow are not executed. 8 | 9 | When scripts fail, their gallery thumbnail is replaced with the broken 10 | image stamp. This allows easy identification in the gallery display. 11 | 12 | You will also get the python traceback of the failed code block. 13 | """ 14 | 15 | # Code source: Óscar Nájera 16 | # License: BSD 3 clause 17 | # sphinx_gallery_line_numbers = True 18 | 19 | import numpy as np 20 | import matplotlib.pyplot as plt 21 | 22 | plt.pcolormesh(np.random.randn(100, 100)) 23 | 24 | # %% 25 | # This next block will raise a NameError 26 | 27 | iae 28 | 29 | # %% 30 | # Sphinx gallery will stop executing the remaining code blocks after 31 | # the exception has occurred in the example script. Nevertheless the 32 | # html will still render all the example annotated text and 33 | # code blocks, but no output will be shown. 34 | 35 | # %% 36 | # Here is another error raising block but will not be executed 37 | 38 | plt.plot('Strings are not a valid argument for the plot function') 39 | -------------------------------------------------------------------------------- /examples/plot_01_exp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Plotting the exponential function 4 | ================================= 5 | 6 | This example demonstrates how to import a local module and how images are 7 | stacked when two plots are created in one code block. The variable ``N`` from 8 | the example 'Local module' (file ``local_module.py``) is imported in the code 9 | below. Further, note that when there is only one code block in an example, the 10 | output appears before the code block. 11 | """ 12 | 13 | # Code source: Óscar Nájera 14 | # License: BSD 3 clause 15 | 16 | import numpy as np 17 | import matplotlib.pyplot as plt 18 | 19 | # You can use modules local to the example being run, here we import 20 | # N from local_module 21 | from local_module import N # = 100 22 | 23 | 24 | def main(): 25 | x = np.linspace(-1, 2, N) 26 | y = np.exp(x) 27 | 28 | plt.figure() 29 | plt.plot(x, y) 30 | plt.xlabel('$x$') 31 | plt.ylabel('$\exp(x)$') 32 | plt.title('Exponential function') 33 | 34 | plt.figure() 35 | plt.plot(x, -np.exp(-x)) 36 | plt.xlabel('$x$') 37 | plt.ylabel('$-\exp(-x)$') 38 | plt.title('Negative exponential\nfunction') 39 | # To avoid matplotlib text output 40 | plt.show() 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This Pull request fixes #xx, fixes #yy, fixes #zz 2 | 3 | - [ ] I completed the issue numbers (#xx) in the sentence above. The word "fixes" should remain in front of each issue 4 | - [ ] My PR is tagged as draft when I'm still working on it, and I remove the draft flag when it is ready for review. 5 | - [ ] I added one or several `changelog.md` entries in the appropriate "in progress" section (not the last release one) 6 | 7 | ### b - My PR adds some features: 8 | 9 | *(Delete this section if not relevant)* 10 | 11 | - [ ] Each issue is well-described, well-labeled and explains/agrees on the expected solution (high-level). 12 | - [ ] I added documentation gallery examples showing the new features 13 | - [ ] I created tests for each new feature. In particular I have for each feature: 14 | - [ ] nominal tests (several simple ones or a parametrized one, or both) 15 | - [ ] edge tests (valid but very particular cases, for example empty data, daylight savings change day, sparse matrix, etc.) 16 | - [ ] error tests (in which case use `with pytest.raises(MyErrorType, match="...")`) 17 | 18 | ### b - My PR fixes some issues: 19 | 20 | *(Delete this section if not relevant)* 21 | 22 | - [ ] Each issue is well-described, well-labeled, and contains reproducible code examples. 23 | -------------------------------------------------------------------------------- /examples/plot_04_choose_thumbnail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Choosing the thumbnail figure 4 | ============================= 5 | 6 | This example demonstrates how to choose the figure that is displayed as the 7 | thumbnail, if the example generates more than one figure. This is done by 8 | specifying the keyword-value pair 9 | `mkdocs_gallery_thumbnail_number = ` as a 10 | comment somewhere below the docstring in the example file. In this example, we 11 | specify that we wish for the second figure to be the thumbnail. 12 | """ 13 | 14 | # Code source: Óscar Nájera 15 | # License: BSD 3 clause 16 | 17 | import numpy as np 18 | import matplotlib.pyplot as plt 19 | 20 | 21 | def main(): 22 | x = np.linspace(-1, 2, 100) 23 | y = np.exp(x) 24 | 25 | plt.figure() 26 | plt.plot(x, y) 27 | plt.xlabel('$x$') 28 | plt.ylabel('$\exp(x)$') 29 | 30 | # The next line sets the thumbnail for the second figure in the gallery 31 | # (plot with negative exponential in orange) 32 | # mkdocs_gallery_thumbnail_number = 2 33 | plt.figure() 34 | plt.plot(x, -np.exp(-x), color='orange', linewidth=4) 35 | plt.xlabel('$x$') 36 | plt.ylabel('$-\exp(-x)$') 37 | # To avoid matplotlib text output 38 | plt.show() 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /ci_tools/check_python_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if __name__ == "__main__": 4 | # Execute only if run as a script. 5 | # Check the arguments 6 | nbargs = len(sys.argv[1:]) 7 | if nbargs != 1: 8 | raise ValueError("a mandatory argument is required: ") 9 | 10 | expected_version_str = sys.argv[1] 11 | try: 12 | expected_version = tuple(int(i) for i in expected_version_str.split(".")) 13 | except Exception as e: 14 | raise ValueError("Error while parsing expected version %r: %r" % (expected_version, e)) 15 | 16 | if len(expected_version) < 1: 17 | raise ValueError("At least a major is expected") 18 | 19 | if sys.version_info[0] != expected_version[0]: 20 | raise AssertionError("Major version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) 21 | 22 | if len(expected_version) >= 2 and sys.version_info[1] != expected_version[1]: 23 | raise AssertionError("Minor version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) 24 | 25 | if len(expected_version) >= 3 and sys.version_info[2] != expected_version[2]: 26 | raise AssertionError("Patch version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) 27 | 28 | print("SUCCESS - Actual python version %r matches expected one %r" % (sys.version, expected_version_str)) 29 | -------------------------------------------------------------------------------- /examples/plot_11_pyvista.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example with the pyvista 3d plotting library 3 | ============================================ 4 | 5 | Mkdocs-Gallery supports examples made with the 6 | [pyvista library](https://docs.pyvista.org/version/stable/). 7 | 8 | In order to use pyvista, the [`conf_script` of the project](../../index.md#b-advanced) should include the 9 | following lines to adequatly configure pyvista: 10 | 11 | ```python 12 | import pyvista 13 | 14 | pyvista.BUILDING_GALLERY = True 15 | pyvista.OFF_SCREEN = True 16 | 17 | conf = { 18 | ..., 19 | "image_scrapers": ("pyvista", ...), 20 | } 21 | ``` 22 | """ 23 | import pyvista as pv 24 | 25 | # %% 26 | # You can display an animation as a gif 27 | 28 | sphere = pv.Sphere() 29 | pl = pv.Plotter() 30 | pl.enable_hidden_line_removal() 31 | pl.add_mesh(sphere, show_edges=True, color="tan") 32 | # for this example 33 | pl.open_gif("animation.gif", fps=10) 34 | # alternatively, to disable movie generation: 35 | # pl.show(auto_close=False, interactive=False) 36 | delta_x = 0.05 37 | center = sphere.center 38 | for angle in range(0, 360, 10): 39 | 40 | rot = sphere.rotate_x(angle, point=(0, 0, 0), inplace=False) 41 | 42 | pl.clear_actors() 43 | pl.add_mesh(rot, show_edges=True, color="tan") 44 | pl.write_frame() 45 | 46 | 47 | pl.show() 48 | 49 | # %% 50 | # or simply show a static plot 51 | 52 | sphere = pv.Sphere() 53 | pl = pv.Plotter() 54 | pl.add_mesh(sphere, show_edges=True, color="tan") 55 | pl.show() 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand this project's build structure 3 | 4 | - This project uses setuptools, so it is declared as the build system in the pyproject.toml file 5 | - We use as much as possible `setup.cfg` to store the information so that it can be read by other tools such as `tox` 6 | and `nox`. So `setup.py` contains **almost nothing** (see below) 7 | This philosophy was found after trying all other possible combinations in other projects :) 8 | A reference project that was inspiring to make this move : https://github.com/Kinto/kinto/blob/master/setup.cfg 9 | 10 | See also: 11 | https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files 12 | https://packaging.python.org/en/latest/distributing.html 13 | https://github.com/pypa/sampleproject 14 | """ 15 | from setuptools import setup 16 | 17 | 18 | # (1) check required versions (from https://medium.com/@daveshawley/safely-using-setup-cfg-for-metadata-1babbe54c108) 19 | import pkg_resources 20 | 21 | pkg_resources.require("setuptools>=39.2") 22 | pkg_resources.require("setuptools_scm") 23 | 24 | 25 | # (2) Generate download url using git version 26 | from setuptools_scm import get_version # noqa: E402 27 | 28 | URL = "https://github.com/smarie/mkdocs-gallery" 29 | DOWNLOAD_URL = URL + "/tarball/" + get_version() 30 | 31 | 32 | # (3) Call setup() with as little args as possible 33 | setup( 34 | download_url=DOWNLOAD_URL, 35 | use_scm_version={ 36 | "write_to": "src/mkdocs_gallery/_version.py" 37 | }, # we can't put `use_scm_version` in setup.cfg yet unfortunately 38 | ) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Sylvain Marié 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /examples/plot_06_function_identifier.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Identifying function names in a script 4 | ====================================== 5 | 6 | !!! warning "Work in progress" 7 | This feature has not been fully ported to mkdocs, see [#10](https://github.com/smarie/mkdocs-gallery/issues/10) 8 | 9 | This demonstrates how Mkdocs-Gallery identifies function names to figure out 10 | which functions are called in the script and to which module do they belong. 11 | """ 12 | 13 | # Code source: Óscar Nájera 14 | # License: BSD 3 clause 15 | 16 | import os # noqa, analysis:ignore 17 | import matplotlib.pyplot as plt 18 | from mkdocs_gallery.backreferences import identify_names 19 | from mkdocs_gallery.py_source_parser import split_code_and_text_blocks 20 | 21 | filename = os.__file__.replace('.pyc', '.py') 22 | _, script_blocks = split_code_and_text_blocks(filename) 23 | names = identify_names(script_blocks) 24 | figheight = len(names) + .5 25 | 26 | fontsize = 12.5 27 | 28 | # %% 29 | # Mkdocs-Gallery examines both the executed code itself, as well as the 30 | # documentation blocks (such as this one, or the top-level one), 31 | # to find backreferences. This means that by writing `numpy.sin` 32 | # and `numpy.exp` here, a backreference will be created even though 33 | # they are not explicitly used in the code. This is useful in particular when 34 | # functions return classes -- if you add them to the documented blocks of 35 | # examples that use them, they will be shown in the backreferences. 36 | # 37 | # Also note that global variables of the script have intersphinx references 38 | # added to them automatically (e.g., `fig` and `fig.text` below). 39 | 40 | fig = plt.figure(figsize=(7.5, 8)) 41 | 42 | for i, (name, obj) in enumerate(names.items()): 43 | fig.text(0.55, (float(len(names)) - 0.5 - i) / figheight, 44 | name, 45 | ha="right", 46 | size=fontsize, 47 | transform=fig.transFigure, 48 | bbox=dict(boxstyle='square', fc="w", ec="k")) 49 | fig.text(0.6, (float(len(names)) - 0.5 - i) / figheight, 50 | obj[0]["module"], 51 | ha="left", 52 | size=fontsize, 53 | transform=fig.transFigure, 54 | bbox=dict(boxstyle='larrow,pad=0.1', fc="w", ec="k")) 55 | 56 | plt.draw() 57 | -------------------------------------------------------------------------------- /docs/long_description.md: -------------------------------------------------------------------------------- 1 | # mkdocs-gallery 2 | 3 | *[Sphinx-Gallery](https://sphinx-gallery.github.io/) features for [mkdocs](https://www.mkdocs.org/) (no [Sphinx](sphinx-doc.org/) dependency !).* 4 | 5 | [![Python versions](https://img.shields.io/pypi/pyversions/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Build Status](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/mkdocs-gallery/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/mkdocs-gallery/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/mkdocs-gallery/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/mkdocs-gallery) [![Flake8 Status](https://smarie.github.io/mkdocs-gallery/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/flake8/index.html) 6 | 7 | [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/mkdocs-gallery/) [![PyPI](https://img.shields.io/pypi/v/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Downloads](https://pepy.tech/badge/mkdocs-gallery)](https://pepy.tech/project/mkdocs-gallery) [![Downloads per week](https://pepy.tech/badge/mkdocs-gallery/week)](https://pepy.tech/project/mkdocs-gallery) [![GitHub stars](https://img.shields.io/github/stars/smarie/mkdocs-gallery.svg)](https://github.com/smarie/mkdocs-gallery/stargazers) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5786851.svg)](https://doi.org/10.5281/zenodo.5786851) 8 | 9 | Do you love [Sphinx-Gallery](https://sphinx-gallery.github.io/) but prefer [mkdocs](https://www.mkdocs.org/) over [Sphinx](sphinx-doc.org/) for your documentation ? `mkdocs-gallery` was written for you ;) 10 | 11 | It relies on [mkdocs-material](https://squidfunk.github.io/mkdocs-material) to get the most of mkdocs, so that your galleries look nice! 12 | 13 | The documentation for users is available here: [https://smarie.github.io/mkdocs-gallery/](https://smarie.github.io/mkdocs-gallery/) 14 | 15 | A readme for developers is available here: [https://github.com/smarie/mkdocs-gallery](https://github.com/smarie/mkdocs-gallery) 16 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | import os 4 | import pytest 5 | from mkdocs_gallery.utils import matches_filepath_pattern, is_relative_to 6 | 7 | 8 | class TestFilepathPatternMatch: 9 | FILEPATH = Path("/directory/filename.ext") 10 | 11 | @pytest.mark.parametrize("pattern", [ 12 | r"filename", 13 | r"filename\.ext", 14 | r"\.ext", 15 | re.escape(os.sep) + r"filename", 16 | r"directory", 17 | re.escape(os.sep) + r"directory", 18 | ]) 19 | def test_ok(self, pattern): 20 | """Test that the pattern matches the filename""" 21 | 22 | assert matches_filepath_pattern(TestFilepathPatternMatch.FILEPATH, pattern) 23 | 24 | @pytest.mark.parametrize("pattern", [ 25 | r"wrong_filename", 26 | r"wrong_filename\.ext", 27 | r"\.wrong_ext", 28 | re.escape(os.sep) + r"wrong_filename", 29 | r"wrong_directory", 30 | re.escape(os.sep) + r"wrong_directory", 31 | ]) 32 | def test_fails(self, pattern): 33 | """Test that the pattern does not match the filename""" 34 | 35 | assert not matches_filepath_pattern(TestFilepathPatternMatch.FILEPATH, pattern) 36 | 37 | def test_not_path_raises(self): 38 | """Test that the function raises an exception when filepath is not a Path object""" 39 | 40 | filepath = str(TestFilepathPatternMatch.FILEPATH) 41 | pattern = r"filename" 42 | 43 | with pytest.raises(AssertionError): 44 | matches_filepath_pattern(filepath, pattern) 45 | 46 | 47 | class TestRelativePaths: 48 | 49 | @pytest.mark.parametrize( 50 | "path1, path2, expected", [ 51 | ("parent", "parent/sub", True), 52 | ("notparent", "parent/sub", False), 53 | ]) 54 | def test_behavior(self, path1, path2, expected): 55 | """Test that the function behaves as expected""" 56 | 57 | assert is_relative_to(Path(path1), Path(path2)) == expected 58 | 59 | @pytest.mark.parametrize( 60 | "path1, path2", [ 61 | ("parent", "parent/sub"), 62 | (Path("parent"), "parent/sub"), 63 | ("parent", Path("parent/sub")), 64 | ]) 65 | def test_not_paths_raises(self, path1, path2): 66 | """Test that the function raises an exception when both arguments are not Path objects""" 67 | 68 | with pytest.raises(TypeError): 69 | is_relative_to(path1, path2) 70 | -------------------------------------------------------------------------------- /examples/plot_12_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Support for asynchronous code 3 | 4 | [PEP 429](https://peps.python.org/pep-0492), which was first implemented in 5 | [Python 3.5](https://docs.python.org/3/whatsnew/3.5.html#whatsnew-pep-492), added initial syntax for asynchronous 6 | programming in Python: `async` and `await`. 7 | 8 | While this was a major improvement in particular for UX development, one major 9 | downside is that it "poisons" the caller's code base. If you want to `await` a coroutine, you have to be inside a `async def` 10 | context. Doing so turns the function into a coroutine function and thus forces the caller to also `await` its results. 11 | Rinse and repeat until you reach the beginning of the stack. 12 | 13 | Since version `0.10.0`, `mkdocs-gallery` is now able to automatically detect code blocks using async programming, and to handle them nicely so that you don't have to wrap them. This feature is enabled by default and does not require any configuration option. Generated notebooks remain consistent with [`jupyter` notebooks](https://jupyter.org/), or rather the [`IPython` kernel](https://ipython.org/) running 14 | the code inside of them, that is equipped with 15 | [background handling to allow top-level asynchronous code](https://ipython.readthedocs.io/en/stable/interactive/autoawait.html). 16 | """ 17 | 18 | import asyncio 19 | import time 20 | 21 | 22 | async def afn(): 23 | start = time.time() 24 | await asyncio.sleep(0.3) 25 | stop = time.time() 26 | return stop - start 27 | 28 | 29 | f"I waited for {await afn():.1f} seconds!" 30 | 31 | 32 | # %% 33 | # Without any handling, the snippet above would trigger a `SyntaxError`, since we are using `await` outside of an 34 | # asynchronous context. With the background handling, it works just fine. 35 | # 36 | # Apart from `await` that we used above, all other asynchronous syntax is supported as well. 37 | # 38 | # ## Asynchronous Generators 39 | 40 | 41 | async def agen(): 42 | for chunk in "I'm an async iterator!".split(): 43 | yield chunk 44 | 45 | 46 | async for chunk in agen(): 47 | print(chunk, end=" ") 48 | 49 | 50 | # %% 51 | # ## Asynchronous Comprehensions 52 | 53 | " ".join([chunk async for chunk in agen()]) 54 | 55 | # %% 56 | # ## Asynchronous Context Managers 57 | 58 | import contextlib 59 | 60 | 61 | @contextlib.asynccontextmanager 62 | async def acm(): 63 | print("Entering asynchronous context manager!") 64 | yield 65 | print("Exiting asynchronous context manager!") 66 | 67 | 68 | async with acm(): 69 | print("Inside the context!") 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | docs/reports/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # PyCharm project folder 119 | .idea/ 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # mkdocs-gallery specific 'generated' folder 136 | docs/generated/ 137 | 138 | # Version file generated by setuptools_scm 139 | src/*/_version.py 140 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: mkdocs-gallery 2 | # site_description: 'A short description of my project' 3 | repo_url: https://github.com/smarie/mkdocs-gallery 4 | #docs_dir: . 5 | #site_dir: ../site 6 | # default branch is main instead of master now on github 7 | edit_uri : ./edit/main/docs 8 | 9 | theme: 10 | name: material # readthedocs mkdocs material 11 | palette: 12 | primary: light blue 13 | 14 | nav: 15 | - Home: index.md 16 | - Gallery of examples: generated/gallery # The first gallery of examples 17 | - generated/tutorials # The second gallery (from tutorials) 18 | - Changelog: changelog.md 19 | 20 | plugins: 21 | - search 22 | - gallery: 23 | conf_script: docs/gallery_conf.py # Base conf. Possibly modified by items below 24 | examples_dirs: 25 | - examples # path to your example scripts, relative to mkdocs.yml 26 | - docs/tutorials 27 | gallery_dirs: 28 | - docs/generated/gallery # where to save gallery generated output. Note that you may or may not include them in 29 | - docs/generated/tutorials 30 | 31 | backreferences_dir: docs/generated/backreferences # where to generate the back references summary 32 | doc_module: ['mkdocs_gallery', 'numpy'] 33 | # reference_url: {sphinx_gallery: None}, 34 | image_scrapers: 35 | - matplotlib 36 | - mayavi 37 | - pyvista 38 | compress_images: ['images', 'thumbnails'] 39 | # specify the order of examples to be according to filename 40 | within_subsection_order: FileNameSortKey 41 | expected_failing_examples: 42 | - examples/no_output/plot_raise.py 43 | - examples/no_output/plot_syntaxerror.py 44 | 45 | # min_reported_time: min_reported_time, in conf file 46 | binder: 47 | org: smarie 48 | repo: mkdocs-gallery 49 | branch: gh-pages # Use a branch that hosts your docs. 50 | # binderhub_url: https://mybinder.org 51 | dependencies: docs/binder_cfg/requirements.txt # binder configuration files 52 | # see https://mybinder.readthedocs.io/en/latest/using/config_files.html#config-files 53 | # these will be copied to the .binder/ directory of the site. 54 | 55 | notebooks_dir: notebooks # Notebooks for Binder will be copied to this dir (relative to built doc root). 56 | use_jupyter_lab: True # Binder links will start Jupyter Lab instead of the Jupyter Notebook interface. 57 | show_memory: True 58 | # junit: os.path.join('sphinx-gallery', 'junit-results.xml'), 59 | # # capture raw HTML or, if not present, __repr__ of last expression in each code block 60 | # capture_repr: ['_repr_html_', '__repr__'] 61 | matplotlib_animations: True 62 | image_srcset: ['2x'] 63 | 64 | 65 | markdown_extensions: 66 | - pymdownx.arithmatex: 67 | generic: true 68 | 69 | - toc: 70 | permalink: true 71 | 72 | extra_javascript: 73 | - javascripts/mathjax.js 74 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 75 | -------------------------------------------------------------------------------- /examples/plot_09_plotly.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example with the plotly graphing library 3 | ======================================== 4 | 5 | Mkdocs-Gallery supports examples made with the 6 | [plotly library](https://plotly.com/python/). Mkdocs-Gallery is able to 7 | capture the `_repr_html_` of plotly figure objects (see 8 | [Controlling what output is captured](https://sphinx-gallery.github.io/stable/configuration.html#capture-repr)). 9 | To display the figure, the last line in your code block should therefore be the plotly figure object. 10 | 11 | In order to use plotly, the [`conf_script` of the project](../../index.md#b-advanced) should include the 12 | following lines to select the appropriate plotly renderer: 13 | 14 | ```python 15 | import plotly.io as pio 16 | pio.renderers.default = 'sphinx_gallery' 17 | ``` 18 | 19 | **Optional**: the `sphinx_gallery` renderer of plotly will not generate png 20 | thumbnails. For png thumbnails, you can use instead the `sphinx_gallery_png` 21 | renderer, and add `plotly.io._sg_scraper.plotly_sg_scraper` to the list of 22 | [Image scrapers](https://sphinx-gallery.github.io/stable/configuration.html#image-scrapers). 23 | The scraper requires you to 24 | [install the orca package](https://plotly.com/python/static-image-export/). 25 | 26 | This tutorial gives a few examples of plotly figures, starting with its 27 | high-level API [plotly express](https://plotly.com/python/plotly-express/). 28 | """ 29 | import plotly.express as px 30 | import numpy as np 31 | 32 | df = px.data.tips() 33 | fig = px.bar(df, x='sex', y='total_bill', facet_col='day', color='smoker', barmode='group', 34 | template='presentation+plotly' 35 | ) 36 | fig.update_layout(height=400) 37 | fig 38 | 39 | #%% 40 | # In addition to the classical scatter or bar charts, plotly provides a large 41 | # variety of traces, such as the sunburst hierarchical trace of the following 42 | # example. plotly is an interactive library: click on one of the continents 43 | # for a more detailed view of the drill-down. 44 | 45 | df = px.data.gapminder().query("year == 2007") 46 | fig = px.sunburst(df, path=['continent', 'country'], values='pop', 47 | color='lifeExp', hover_data=['iso_alpha'], 48 | color_continuous_scale='RdBu', 49 | color_continuous_midpoint=np.average(df['lifeExp'], weights=df['pop'])) 50 | fig.update_layout(title_text='Life expectancy of countries and continents') 51 | fig 52 | 53 | 54 | #%% 55 | # While plotly express is often the high-level entry point of the plotly 56 | # library, complex figures mixing different types of traces can be made 57 | # with the low-level `graph_objects` imperative API. 58 | 59 | from plotly.subplots import make_subplots 60 | import plotly.graph_objects as go 61 | fig = make_subplots(rows=1, cols=2, specs=[[{}, {'type':'domain'}]]) 62 | fig.add_trace(go.Bar(x=[2018, 2019, 2020], y=[3, 2, 5], showlegend=False), 1, 1) 63 | fig.add_trace(go.Pie(labels=['A', 'B', 'C'], values=[1, 3, 6]), 1, 2) 64 | fig.update_layout(height=400, template='presentation', yaxis_title_text='revenue') 65 | fig 66 | 67 | # mkdocs_gallery_thumbnail_path = '_static/plotly_logo.png' 68 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/downloads.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | """ 7 | Utilities for downloadable items 8 | """ 9 | 10 | from __future__ import absolute_import, division, print_function 11 | 12 | from pathlib import Path 13 | from typing import List 14 | from zipfile import ZipFile 15 | 16 | from .gen_data_model import Gallery 17 | from .utils import _new_file, _replace_by_new_if_needed 18 | 19 | 20 | def python_zip(file_list: List[Path], gallery: Gallery, extension=".py"): 21 | """Stores all files in file_list with modified extension `extension` into an zip file 22 | 23 | Parameters 24 | ---------- 25 | file_list : List[Path] 26 | Holds all the files to be included in zip file. Note that if extension 27 | is set to 28 | 29 | gallery : Gallery 30 | gallery for which to create the zip file 31 | 32 | extension : str 33 | The replacement extension for files in file_list. '.py' or '.ipynb'. 34 | In order to deal with downloads of python sources and jupyter notebooks, 35 | since we know that there is one notebook for each python file. 36 | 37 | Returns 38 | ------- 39 | zipfile : Path 40 | zip file, written as `_{python,jupyter}.zip` depending on the extension 41 | """ 42 | zipfile = gallery.zipfile_python if extension == ".py" else gallery.zipfile_jupyter 43 | 44 | # Create the new zip 45 | zipfile_new = _new_file(zipfile) 46 | with ZipFile(str(zipfile_new), mode="w") as zipf: 47 | for file in file_list: 48 | file_src = file.with_suffix(extension) 49 | zipf.write(file_src, file_src.relative_to(gallery.generated_dir)) 50 | 51 | # Replace the old one if needed 52 | _replace_by_new_if_needed(zipfile_new) 53 | 54 | return zipfile 55 | 56 | 57 | def generate_zipfiles(gallery: Gallery): 58 | """ 59 | Collects all Python source files and Jupyter notebooks in 60 | gallery_dir and makes zipfiles of them 61 | 62 | Parameters 63 | ---------- 64 | gallery : Gallery 65 | path of the gallery to collect downloadable sources 66 | 67 | Return 68 | ------ 69 | download_md: str 70 | Markdown to include download buttons to the generated files 71 | """ 72 | # Collect the files to include in the zip 73 | listdir = gallery.list_downloadable_sources(recurse=True) 74 | 75 | # Create the two zip files 76 | python_zip(listdir, gallery, extension=".py") 77 | python_zip(listdir, gallery, extension=".ipynb") 78 | 79 | icon = ":fontawesome-solid-download:" 80 | dw_md = f""" 81 | 82 | 83 | [{icon} Download all examples in Python source code: {gallery.zipfile_python.name}](./{gallery.zipfile_python_rel_index_md}){{ .md-button .center}} 84 | 85 | [{icon} Download all examples in Jupyter notebooks: {gallery.zipfile_jupyter.name}](./{gallery.zipfile_jupyter_rel_index_md}){{ .md-button .center}} 86 | """ # noqa 87 | return dw_md 88 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/binder_badge_logo.svg: -------------------------------------------------------------------------------- 1 | launchlaunchbinderbinder -------------------------------------------------------------------------------- /docs/tutorials/plot_parse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Alternating text and code 4 | ========================= 5 | 6 | Mkdocs-Gallery is capable of transforming Python files into MD files 7 | with a notebook structure. For this to be used you need to respect some syntax 8 | rules. This example demonstrates how to alternate text and code blocks and some 9 | edge cases. It was designed to be compared with the 10 | [source Python script](./plot_parse.py). 11 | """ 12 | 13 | # %% 14 | # This is the first text block and directly follows the header docstring above. 15 | 16 | import numpy as np 17 | 18 | # %% 19 | 20 | # You can separate code blocks using either a single line of ``#``'s 21 | # (>=20 columns), ``#%%``, or ``# %%``. For consistency, it is recommend that 22 | # you use only one of the above three 'block splitter' options in your project. 23 | A = 1 24 | 25 | import matplotlib.pyplot as plt 26 | 27 | # %% 28 | # Block splitters allow you alternate between code and text blocks **and** 29 | # separate sequential blocks of code (above) and text (below). 30 | 31 | ############################################################################## 32 | # A line of ``#``'s also works for separating blocks. The above line of ``#``'s 33 | # separates the text block above from this text block. Notice however, that 34 | # separated text blocks only shows as a new lines between text, in the rendered 35 | # output. 36 | 37 | def dummy(): 38 | """This should not be part of a 'text' block'""" 39 | 40 | # %% 41 | # This comment inside a code block will remain in the code block 42 | pass 43 | 44 | # this line should not be part of a 'text' block 45 | 46 | # %% 47 | # 48 | # #################################################################### 49 | # 50 | # The above syntax makes a line cut in Sphinx. Note the space between the first 51 | # ``#`` and the line of ``#``'s. 52 | 53 | # %% 54 | # !!! warning 55 | # The next kind of comments are not supported (notice the line of ``#``'s 56 | # and the ``# %%`` start at the margin instead of being indented like 57 | # above) and become too hard to escape so just don't use code like this: 58 | # 59 | # def dummy2(): 60 | # """Function docstring""" 61 | # #################################### 62 | # # This comment 63 | # # %% 64 | # # and this comment inside python indentation 65 | # # breaks the block structure and is not 66 | # # supported 67 | # dummy2 68 | # 69 | 70 | """Free strings are not supported. They will be rendered as a code block""" 71 | 72 | # %% 73 | # New lines can be included in your text block and the parser 74 | # is capable of retaining this important whitespace to work with Sphinx. 75 | # Everything after a block splitter and starting with ``#`` then one space, 76 | # is interpreted by Sphinx-Gallery to be a MD text block. Keep your text 77 | # block together using ``#`` and a space at the beginning of each line. 78 | # 79 | # ## MD header within a text block 80 | # 81 | # Note that Markdown supports 82 | # [an alternate syntax for headings](https://www.markdownguide.org/basic-syntax/), 83 | # that is far easier to read in the context of gallery examples: 84 | # 85 | # MD header within a text block, alternate syntax 86 | # ----------------------------------------------- 87 | # 88 | 89 | print('one') 90 | 91 | # %% 92 | # 93 | 94 | # another way to separate code blocks shown above 95 | B = 1 96 | 97 | # %% 98 | # Last text block. 99 | # 100 | # That's all folks ! 101 | # 102 | # ```python title="plot_parse.py" 103 | # --8<-- "docs/tutorials/plot_parse.py" 104 | # ``` 105 | # 106 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pathlib import Path 3 | 4 | from mkdocs.config import load_config 5 | from mkdocs_gallery.plugin import GalleryPlugin 6 | from mkdocs.utils import yaml_load 7 | 8 | 9 | @pytest.fixture 10 | def tmp_root_dir(tmpdir): 11 | """A temporary directory that gets set as the current working dir during the test using it""" 12 | with tmpdir.as_cwd() as _old_cwd: 13 | yield Path(str(tmpdir)) 14 | 15 | 16 | @pytest.fixture 17 | def basic_mkdocs_config(tmp_root_dir): 18 | """A basic mkdocs config""" 19 | 20 | docs_dir = tmp_root_dir / "docs" 21 | examples_dir = docs_dir / "examples" 22 | examples_dir.mkdir(parents=True) 23 | tmp_cfg_file = tmp_root_dir / "mkdocs.yml" 24 | 25 | # contents of the config file 26 | tmp_cfg_file.write_text(""" 27 | site_name: basic_conf 28 | """) 29 | 30 | yield load_config(str(tmp_cfg_file)) 31 | 32 | 33 | def test_minimal_conf(basic_mkdocs_config): 34 | """Test that minimal config can be loaded without problem""" 35 | 36 | # Create a mini config 37 | mini_config = yaml_load(""" 38 | examples_dirs: docs/examples # path to your example scripts 39 | gallery_dirs: docs/generated/gallery # where to save generated gallery 40 | """) 41 | 42 | # Load it (this triggers validation and default values according to the plugin config schema) 43 | plugin = GalleryPlugin() 44 | errors, warnings = plugin.load_config(mini_config) 45 | 46 | assert len(errors) == 0 47 | assert len(warnings) == 0 48 | 49 | # Now mimic the on_config event 50 | result = plugin.on_config(basic_mkdocs_config) 51 | 52 | # See also https://github.com/mkdocs/mkdocs/blob/master/mkdocs/tests/plugin_tests.py 53 | # And https://github.com/mkdocs/mkdocs/blob/master/mkdocs/tests/search_tests.py 54 | 55 | assert isinstance(plugin.config, dict) 56 | assert len(plugin.config) > 0 57 | 58 | 59 | REPO_ROOT_DIR = Path(__file__).parent.parent 60 | 61 | 62 | def test_full_conf(basic_mkdocs_config, monkeypatch): 63 | """Test that full config can be loaded without problem""" 64 | 65 | monkeypatch.chdir(REPO_ROOT_DIR) 66 | 67 | # Create a mini config 68 | full_config = yaml_load(""" 69 | conf_script: docs/gallery_conf.py 70 | examples_dirs: 71 | - examples 72 | - docs/tutorials 73 | # TODO mayavi_examples 74 | gallery_dirs: 75 | - docs/generated/gallery 76 | - docs/generated/tutorials 77 | # TODO tutorials and mayavi_examples 78 | 79 | backreferences_dir: docs/generated/backreferences 80 | doc_module: ['mkdocs_gallery', 'numpy'] 81 | # reference_url: {sphinx_gallery: None} 82 | image_scrapers: matplotlib 83 | compress_images: ['images', 'thumbnails'] 84 | within_subsection_order: FileNameSortKey 85 | expected_failing_examples: 86 | - examples/no_output/plot_raise.py 87 | - examples/no_output/plot_syntaxerror.py 88 | 89 | # min_reported_time: min_reported_time, in conf file 90 | 91 | binder: 92 | org: smarie 93 | repo: mkdocs-gallery 94 | branch: gh-pages 95 | binderhub_url: https://mybinder.org 96 | dependencies: docs/binder_cfg/requirements.txt 97 | notebooks_dir: notebooks 98 | use_jupyter_lab: True 99 | show_memory: True 100 | # junit: foo/junit-results.xml 101 | capture_repr: ['_repr_html_', '__repr__'] 102 | matplotlib_animations: True 103 | image_srcset: ['2x'] 104 | """) 105 | 106 | # Load it (this triggers validation and default values according to the plugin config schema) 107 | plugin = GalleryPlugin() 108 | errors, warnings = plugin.load_config(full_config) 109 | 110 | assert len(errors) == 0 111 | assert len(warnings) == 0 112 | 113 | # Now mimic the on_config event 114 | result = plugin.on_config(basic_mkdocs_config) 115 | 116 | # See also https://github.com/mkdocs/mkdocs/blob/master/mkdocs/tests/plugin_tests.py 117 | # And https://github.com/mkdocs/mkdocs/blob/master/mkdocs/tests/search_tests.py 118 | 119 | assert isinstance(plugin.config, dict) 120 | assert len(plugin.config) > 0 121 | -------------------------------------------------------------------------------- /tests/reference_parse.txt: -------------------------------------------------------------------------------- 1 | [('text', 2 | '\n' 3 | 'Alternating text and code\n' 4 | '=========================\n' 5 | '\n' 6 | 'Mkdocs-Gallery is capable of transforming Python files into MD files\n' 7 | 'with a notebook structure. For this to be used you need to respect some ' 8 | 'syntax\n' 9 | 'rules. This example demonstrates how to alternate text and code blocks and ' 10 | 'some\n' 11 | 'edge cases. It was designed to be compared with the\n' 12 | '[source Python script](./plot_parse.py).', 13 | 1), 14 | ('text', 15 | 'This is the first text block and directly follows the header docstring ' 16 | 'above.\n', 17 | 14), 18 | ('code', '\nimport numpy as np\n\n', 15), 19 | ('code', 20 | '\n' 21 | "# You can separate code blocks using either a single line of ``#``'s\n" 22 | '# (>=20 columns), ``#%%``, or ``# %%``. For consistency, it is recommend ' 23 | 'that\n' 24 | "# you use only one of the above three 'block splitter' options in your " 25 | 'project.\n' 26 | 'A = 1\n' 27 | '\n' 28 | 'import matplotlib.pyplot as plt\n' 29 | '\n', 30 | 19), 31 | ('text', 32 | 'Block splitters allow you alternate between code and text blocks **and**\n' 33 | 'separate sequential blocks of code (above) and text (below).\n', 34 | 28), 35 | ('text', 36 | "A line of ``#``'s also works for separating blocks. The above line of " 37 | "``#``'s\n" 38 | 'separates the text block above from this text block. Notice however, that\n' 39 | 'separated text blocks only shows as a new lines between text, in the ' 40 | 'rendered\n' 41 | 'output.\n', 42 | 32), 43 | ('code', 44 | '\n' 45 | 'def dummy():\n' 46 | ' """This should not be part of a \'text\' block\'"""\n' 47 | '\n' 48 | ' # %%\n' 49 | ' # This comment inside a code block will remain in the code block\n' 50 | ' pass\n' 51 | '\n' 52 | "# this line should not be part of a 'text' block\n" 53 | '\n', 54 | 36), 55 | ('text', 56 | '####################################################################\n' 57 | '\n' 58 | 'The above syntax makes a line cut in Sphinx. Note the space between the ' 59 | 'first\n' 60 | "``#`` and the line of ``#``'s.\n", 61 | 47), 62 | ('text', 63 | '!!! warning\n' 64 | ' The next kind of comments are not supported (notice the line of ' 65 | "``#``'s\n" 66 | ' and the ``# %%`` start at the margin instead of being indented like\n' 67 | " above) and become too hard to escape so just don't use code like " 68 | 'this:\n' 69 | '\n' 70 | ' def dummy2():\n' 71 | ' """Function docstring"""\n' 72 | ' ####################################\n' 73 | ' # This comment\n' 74 | ' # %%\n' 75 | ' # and this comment inside python indentation\n' 76 | ' # breaks the block structure and is not\n' 77 | ' # supported\n' 78 | ' dummy2\n' 79 | '\n', 80 | 54), 81 | ('code', 82 | '\n' 83 | '"""Free strings are not supported. They will be rendered as a code ' 84 | 'block"""\n' 85 | '\n', 86 | 69), 87 | ('text', 88 | 'New lines can be included in your text block and the parser\n' 89 | 'is capable of retaining this important whitespace to work with Sphinx.\n' 90 | 'Everything after a block splitter and starting with ``#`` then one space,\n' 91 | 'is interpreted by Sphinx-Gallery to be a MD text block. Keep your text\n' 92 | 'block together using ``#`` and a space at the beginning of each line.\n' 93 | '\n' 94 | '## MD header within a text block\n' 95 | '\n' 96 | 'Note that Markdown supports\n' 97 | '[an alternate syntax for headings](https://www.markdownguide.org/basic-syntax/),\n' 98 | 'that is far easier to read in the context of gallery examples:\n' 99 | '\n' 100 | 'MD header within a text block, alternate syntax\n' 101 | '-----------------------------------------------\n' 102 | '\n', 103 | 73), 104 | ('code', "\nprint('one')\n\n", 88), 105 | ('code', 106 | '\n# another way to separate code blocks shown above\nB = 1\n\n', 107 | 93), 108 | ('text', 109 | 'Last text block.\n' 110 | '\n' 111 | "That's all folks !\n" 112 | '\n' 113 | '```python title="plot_parse.py"\n' 114 | '--8<-- "docs/tutorials/plot_parse.py"\n' 115 | '```\n' 116 | '\n', 117 | 98)] -------------------------------------------------------------------------------- /examples/plot_00_sin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Introductory example - Plotting sin 4 | =================================== 5 | 6 | This is a general example demonstrating a Matplotlib plot output, embedded 7 | Markdown, the use of math notation and cross-linking to other examples. It would be 8 | useful to compare the [:fontawesome-solid-download: source Python file](./plot_0_sin.py) 9 | with the output below. 10 | 11 | Source files for gallery examples should start with a triple-quoted header 12 | docstring. Anything before the docstring is ignored by Mkdocs-Gallery and will 13 | not appear in the rendered output, nor will it be executed. This docstring 14 | requires a Markdown header, which is used as the title of the example and 15 | to correctly build cross-referencing links. 16 | 17 | Code and embedded Markdown text blocks follow the docstring. The first block 18 | immediately after the docstring is deemed a code block, by default, unless you 19 | specify it to be a text block using a line of ``#``'s or ``#%%`` (see below). 20 | All code blocks get executed by Mkdocs-Gallery and any output, including plots 21 | will be captured. Typically, code and text blocks are interspersed to provide 22 | narrative explanations of what the code is doing or interpretations of code 23 | output. 24 | 25 | Mathematical expressions can be included as LaTeX, and will be rendered with 26 | MathJax. See 27 | [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/mathjax) 28 | for configuration of your `mkdocs.yml` as well as for syntax details. For example, 29 | we are about to plot the following function: 30 | 31 | $$ 32 | x \\rightarrow \\sin(x) 33 | $$ 34 | 35 | Here the function $\sin$ is evaluated at each point the variable $x$ is defined. 36 | When including LaTeX in a Python string, ensure that you escape the backslashes 37 | or use a raw docstring. You do not need to do this in 38 | text blocks (see below). 39 | """ 40 | 41 | import numpy as np 42 | import matplotlib.pyplot as plt 43 | 44 | x = np.linspace(0, 2 * np.pi, 100) 45 | y = np.sin(x) 46 | 47 | plt.plot(x, y) 48 | plt.xlabel(r'$x$') 49 | plt.ylabel(r'$\sin(x)$') 50 | # To avoid matplotlib text output 51 | plt.show() 52 | 53 | #%% 54 | # To include embedded Markdown, use a line of >= 20 ``#``'s or ``#%%`` between 55 | # your Markdown and your code (see [syntax](../../index.md#3-add-gallery-examples)). This separates your example 56 | # into distinct text and code blocks. You can continue writing code below the 57 | # embedded Markdown text block: 58 | 59 | print('This example shows a sin plot!') 60 | 61 | #%% 62 | # LaTeX syntax in the text blocks does not require backslashes to be escaped: 63 | # 64 | # $$ 65 | # \sin 66 | # $$ 67 | # 68 | # Cross referencing 69 | # ----------------- 70 | # 71 | # You can refer to an example from any part of the documentation, 72 | # including from other examples. However as opposed to what happens in Sphinx, 73 | # there is no possibility to create unique identifiers in MkDocs. 74 | # 75 | # So you should use relative paths. First, let's note that the markdown 76 | # for the current file is located at `docs/generated/gallery/plot_1_sin.md`. 77 | # This is because the configuration for this gallery in `mkdocs.yml` states 78 | # that the `examples/` gallery should be generated in the `generated/gallery` 79 | # folder (see [Configuration](../../index.md#a-basics)). 80 | # 81 | # Below, the example we want to cross-reference is the 'SyntaxError' example, 82 | # located in the `no_output` subgallery of the `examples` gallery. 83 | # The associated generated file is 84 | # `docs/generated/gallery/no_output/plot_syntaxerror.md`. 85 | # 86 | # ``` 87 | # docs/ 88 | # └── generated/ 89 | # └── gallery/ 90 | # ├── no_output/ 91 | # │ ├── plot_syntaxerror.md # example to reference 92 | # │ └── ... 93 | # ├── plot_1_sin.md # current example 94 | # └── ... 95 | # ``` 96 | # 97 | # We can therefore cross-link to the example using 98 | # `[SyntaxError](./no_output/plot_syntaxerror.md)`: 99 | # [SyntaxError](./no_output/plot_syntaxerror.md). 100 | # 101 | # Of course as for normal documents, we can leverage plugins 102 | # (e.g. mkdocs-material) and extensions. So here we use 103 | # [admonitions](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types): 104 | # to create a nice "see also" note: 105 | # 106 | # !!! info "See also" 107 | # See [SyntaxError](./no_output/plot_syntaxerror.md) for an example with an error. 108 | # 109 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/sorting.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE 2 | # + All contributors to 3 | # 4 | # Original idea and code: sphinx-gallery, 5 | # License: 3-clause BSD, 6 | """ 7 | Sorters for mkdocs-gallery (sub)sections 8 | ======================================== 9 | 10 | Sorting key functions for gallery subsection folders and section files. 11 | """ 12 | 13 | from __future__ import absolute_import, division, print_function 14 | 15 | import os 16 | import types 17 | from enum import Enum 18 | from pathlib import Path 19 | from typing import Iterable, Type 20 | 21 | from .errors import ConfigError 22 | from .gen_single import extract_intro_and_title 23 | from .py_source_parser import split_code_and_text_blocks 24 | 25 | 26 | class _SortKey(object): 27 | """Base class for section order key classes.""" 28 | 29 | def __repr__(self): 30 | return "<%s>" % (self.__class__.__name__,) 31 | 32 | 33 | class ExplicitOrder(_SortKey): 34 | """Sorting key for all gallery subsections. 35 | 36 | This requires all folders to be listed otherwise an exception is raised. 37 | 38 | Parameters 39 | ---------- 40 | ordered_list : list, tuple, or :term:`python:generator` 41 | Hold the paths of each galleries' subsections. 42 | 43 | Raises 44 | ------ 45 | ValueError 46 | Wrong input type or Subgallery path missing. 47 | """ 48 | 49 | def __init__(self, ordered_list: Iterable[str]): 50 | if not isinstance(ordered_list, (list, tuple, types.GeneratorType)): 51 | raise ConfigError( 52 | "ExplicitOrder sorting key takes a list, " 53 | "tuple or Generator, which hold" 54 | "the paths of each gallery subfolder" 55 | ) 56 | 57 | self.ordered_list = list(os.path.normpath(path) for path in ordered_list) 58 | 59 | def __call__(self, item: Path): 60 | if item.name in self.ordered_list: 61 | return self.ordered_list.index(item.name) 62 | else: 63 | raise ConfigError( 64 | "If you use an explicit folder ordering, you " 65 | "must specify all folders. Explicit order not " 66 | "found for {}".format(item.name) 67 | ) 68 | 69 | def __repr__(self): 70 | return "<%s : %s>" % (self.__class__.__name__, self.ordered_list) 71 | 72 | 73 | class NumberOfCodeLinesSortKey(_SortKey): 74 | """Sort examples by the number of code lines.""" 75 | 76 | def __call__(self, file: Path): 77 | file_conf, script_blocks = split_code_and_text_blocks(file) 78 | amount_of_code = sum([len(bcontent) for blabel, bcontent, lineno in script_blocks if blabel == "code"]) 79 | return amount_of_code 80 | 81 | 82 | class FileSizeSortKey(_SortKey): 83 | """Sort examples by file size.""" 84 | 85 | def __call__(self, file: Path): 86 | # src_file = os.path.normpath(str(file)) 87 | # return int(os.stat(src_file).st_size) 88 | return file.stat().st_size 89 | 90 | 91 | class FileNameSortKey(_SortKey): 92 | """Sort examples by file name.""" 93 | 94 | def __call__(self, file: Path): 95 | return file.name 96 | 97 | 98 | class ExampleTitleSortKey(_SortKey): 99 | """Sort examples by example title.""" 100 | 101 | def __call__(self, file: Path): 102 | _, script_blocks = split_code_and_text_blocks(file) 103 | _, title = extract_intro_and_title(file, script_blocks[0][1]) 104 | return title 105 | 106 | 107 | class SortingMethod(Enum): 108 | """ 109 | All known sorting methods. 110 | """ 111 | 112 | ExplicitOrder = ExplicitOrder 113 | NumberOfCodeLinesSortKey = NumberOfCodeLinesSortKey 114 | FileSizeSortKey = FileSizeSortKey 115 | FileNameSortKey = FileNameSortKey 116 | ExampleTitleSortKey = ExampleTitleSortKey 117 | 118 | def __call__(self, *args, **kwargs): 119 | """When enum member is called, return the class""" 120 | return self.value(*args, **kwargs) 121 | 122 | @classmethod 123 | def all_names(cls): 124 | return [s.name for s in cls] 125 | 126 | @classmethod 127 | def from_str(cls, name) -> "SortingMethod": 128 | try: 129 | return cls[name] 130 | except KeyError: 131 | raise ValueError(f"Unknown sorting method {name!r}. Available methods: {cls.all_names()}") 132 | 133 | 134 | def str_to_sorting_method(name: str) -> Type: 135 | """Return the sorting method class associated with the fiven name.""" 136 | return SortingMethod.from_str(name).value 137 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/sg_gallery-rendered-html.css: -------------------------------------------------------------------------------- 1 | /* Adapted from notebook/static/style/style.min.css */ 2 | 3 | .rendered_html { 4 | color: #000; 5 | /* any extras will just be numbers: */ 6 | } 7 | .rendered_html em { 8 | font-style: italic; 9 | } 10 | .rendered_html strong { 11 | font-weight: bold; 12 | } 13 | .rendered_html u { 14 | text-decoration: underline; 15 | } 16 | .rendered_html :link { 17 | text-decoration: underline; 18 | } 19 | .rendered_html :visited { 20 | text-decoration: underline; 21 | } 22 | .rendered_html h1 { 23 | font-size: 185.7%; 24 | margin: 1.08em 0 0 0; 25 | font-weight: bold; 26 | line-height: 1.0; 27 | } 28 | .rendered_html h2 { 29 | font-size: 157.1%; 30 | margin: 1.27em 0 0 0; 31 | font-weight: bold; 32 | line-height: 1.0; 33 | } 34 | .rendered_html h3 { 35 | font-size: 128.6%; 36 | margin: 1.55em 0 0 0; 37 | font-weight: bold; 38 | line-height: 1.0; 39 | } 40 | .rendered_html h4 { 41 | font-size: 100%; 42 | margin: 2em 0 0 0; 43 | font-weight: bold; 44 | line-height: 1.0; 45 | } 46 | .rendered_html h5 { 47 | font-size: 100%; 48 | margin: 2em 0 0 0; 49 | font-weight: bold; 50 | line-height: 1.0; 51 | font-style: italic; 52 | } 53 | .rendered_html h6 { 54 | font-size: 100%; 55 | margin: 2em 0 0 0; 56 | font-weight: bold; 57 | line-height: 1.0; 58 | font-style: italic; 59 | } 60 | .rendered_html h1:first-child { 61 | margin-top: 0.538em; 62 | } 63 | .rendered_html h2:first-child { 64 | margin-top: 0.636em; 65 | } 66 | .rendered_html h3:first-child { 67 | margin-top: 0.777em; 68 | } 69 | .rendered_html h4:first-child { 70 | margin-top: 1em; 71 | } 72 | .rendered_html h5:first-child { 73 | margin-top: 1em; 74 | } 75 | .rendered_html h6:first-child { 76 | margin-top: 1em; 77 | } 78 | .rendered_html ul:not(.list-inline), 79 | .rendered_html ol:not(.list-inline) { 80 | padding-left: 2em; 81 | } 82 | .rendered_html ul { 83 | list-style: disc; 84 | } 85 | .rendered_html ul ul { 86 | list-style: square; 87 | margin-top: 0; 88 | } 89 | .rendered_html ul ul ul { 90 | list-style: circle; 91 | } 92 | .rendered_html ol { 93 | list-style: decimal; 94 | } 95 | .rendered_html ol ol { 96 | list-style: upper-alpha; 97 | margin-top: 0; 98 | } 99 | .rendered_html ol ol ol { 100 | list-style: lower-alpha; 101 | } 102 | .rendered_html ol ol ol ol { 103 | list-style: lower-roman; 104 | } 105 | .rendered_html ol ol ol ol ol { 106 | list-style: decimal; 107 | } 108 | .rendered_html * + ul { 109 | margin-top: 1em; 110 | } 111 | .rendered_html * + ol { 112 | margin-top: 1em; 113 | } 114 | .rendered_html hr { 115 | color: black; 116 | background-color: black; 117 | } 118 | .rendered_html pre { 119 | margin: 1em 2em; 120 | padding: 0px; 121 | background-color: #fff; 122 | } 123 | .rendered_html code { 124 | background-color: #eff0f1; 125 | } 126 | .rendered_html p code { 127 | padding: 1px 5px; 128 | } 129 | .rendered_html pre code { 130 | background-color: #fff; 131 | } 132 | .rendered_html pre, 133 | .rendered_html code { 134 | border: 0; 135 | color: #000; 136 | font-size: 100%; 137 | } 138 | .rendered_html blockquote { 139 | margin: 1em 2em; 140 | } 141 | .rendered_html table { 142 | margin-left: auto; 143 | margin-right: auto; 144 | border: none; 145 | border-collapse: collapse; 146 | border-spacing: 0; 147 | color: black; 148 | font-size: 12px; 149 | table-layout: fixed; 150 | } 151 | .rendered_html thead { 152 | border-bottom: 1px solid black; 153 | vertical-align: bottom; 154 | } 155 | .rendered_html tr, 156 | .rendered_html th, 157 | .rendered_html td { 158 | text-align: right; 159 | vertical-align: middle; 160 | padding: 0.5em 0.5em; 161 | line-height: normal; 162 | white-space: normal; 163 | max-width: none; 164 | border: none; 165 | } 166 | .rendered_html th { 167 | font-weight: bold; 168 | } 169 | .rendered_html tbody tr:nth-child(odd) { 170 | background: #f5f5f5; 171 | } 172 | .rendered_html tbody tr:hover { 173 | background: rgba(66, 165, 245, 0.2); 174 | } 175 | .rendered_html * + table { 176 | margin-top: 1em; 177 | } 178 | .rendered_html p { 179 | text-align: left; 180 | } 181 | .rendered_html * + p { 182 | margin-top: 1em; 183 | } 184 | .rendered_html img { 185 | display: block; 186 | margin-left: auto; 187 | margin-right: auto; 188 | } 189 | .rendered_html * + img { 190 | margin-top: 1em; 191 | } 192 | .rendered_html img, 193 | .rendered_html svg { 194 | max-width: 100%; 195 | height: auto; 196 | } 197 | .rendered_html img.unconfined, 198 | .rendered_html svg.unconfined { 199 | max-width: none; 200 | } 201 | .rendered_html .alert { 202 | margin-bottom: initial; 203 | } 204 | .rendered_html * + .alert { 205 | margin-top: 1em; 206 | } 207 | [dir="rtl"] .rendered_html p { 208 | text-align: right; 209 | } 210 | -------------------------------------------------------------------------------- /examples/plot_03_capture_repr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Capturing output representations 4 | ================================ 5 | 6 | This example demonstrates how the `capture_repr` configuration option 7 | ([Controlling what output is captured](https://sphinx-gallery.github.io/stable/configuration.html#capture-repr)) 8 | works. The default `capture_repr` setting is 9 | `('_repr_html_', '__repr__')` and was used to build this 10 | Mkdocs-Gallery documentation. The output that is captured with this setting 11 | is demonstrated in this example. Differences in outputs that would be captured 12 | with other `capture_repr` settings are also explained. 13 | """ 14 | #%% 15 | # Nothing is captured for the code block below because no data is directed to 16 | # standard output and the last statement is an assignment, not an expression. 17 | 18 | # example 1 19 | a = 2 20 | b = 10 21 | 22 | #%% 23 | # If you did wish to capture the value of `b`, you would need to use: 24 | 25 | # example 2 26 | a = 2 27 | b = 10 28 | b # this is an expression 29 | 30 | #%% 31 | # Mkdocs-Gallery first attempts to capture the `_repr_html_` of `b` as this 32 | # is the first 'representation' method in the `capture_repr` tuple. As this 33 | # method does not exist for `b`, Mkdocs-Gallery moves on and tries to capture 34 | # the `__repr__` method, which is second in the tuple. This does exist for 35 | # `b` so it is captured and the output is seen above. 36 | # 37 | # A pandas dataframe is used in the code block below to provide an example of 38 | # an expression with a `_repr_html_` method. 39 | 40 | # example 3 41 | import pandas as pd 42 | 43 | df = pd.DataFrame(data = {'col1': [1, 2], 'col2': [3, 4]}) 44 | df 45 | 46 | #%% 47 | # The pandas dataframe `df` has both a `__repr__` and `_repr_html_` 48 | # method. As `_repr_html_` appears first in the `capture_repr` tuple, the 49 | # `_repr_html_` is captured in preference to `__repr__`. 50 | # 51 | # Statsmodels tables should also be styled appropriately: 52 | 53 | # example 4 54 | import numpy as np 55 | import statsmodels.iolib.table 56 | statsmodels.iolib.table.SimpleTable(np.zeros((3, 3))) 57 | 58 | #%% 59 | # For the example below, there is data directed to standard output and the last 60 | # statement is an expression. 61 | 62 | # example 5 63 | print('Hello world') 64 | a + b 65 | 66 | #%% 67 | # `print()` outputs to standard output, which is always captured. The 68 | # string `'Hello world'` is thus captured. A 'representation' of the last 69 | # expression is also captured. Again, since this expression `a + b` does not 70 | # have a `_repr_html_` method, the `__repr__` method is captured. 71 | # 72 | # 73 | # Matplotlib output 74 | # ----------------- 75 | # 76 | # Matplotlib function calls generally return a Matplotlib object as well as 77 | # outputting the figure. For code blocks where the last statement is a 78 | # Matplotlib expression, a 'representation' of the object will be captured, as 79 | # well as the plot. This is because Matplotlib objects have a `__repr__` 80 | # method and our `capture_repr` tuple contains `__repr__`. Note that 81 | # Matplotlib objects also have a `__str__` method. 82 | # 83 | # In the example below, `matplotlib.pyplot.plot()` returns a list of 84 | # `Line2D` objects representing the plotted data and the `__repr__` of the 85 | # list is captured as well as the figure: 86 | 87 | import matplotlib.pyplot as plt 88 | 89 | plt.plot([1,2,3]) 90 | 91 | #%% 92 | # To avoid capturing the text representation, you can assign the last Matplotlib 93 | # expression to a temporary variable: 94 | 95 | _ = plt.plot([1,2,3]) 96 | 97 | #%% 98 | # Alternatively, you can add `plt.show()`, which does not return anything, 99 | # to the end of the code block: 100 | 101 | plt.plot([1,2,3]) 102 | plt.show() 103 | 104 | #%% 105 | # The `capture_repr` configuration 106 | # -------------------------------- 107 | # 108 | # The `capture_repr` configuration is `('_repr_html_', '__repr__')` by 109 | # default. This directs Mkdocs-Gallery to capture 'representations' of the last 110 | # statement of a code block, if it is an expression. Mkdocs-Gallery does 111 | # this according to the order 'representations' appear in the tuple. 112 | # 113 | # With the default `capture_repr` setting, `_repr_html_` is attempted to be 114 | # captured first. If this method does not exist, the `__repr__` method would be 115 | # captured. If the `__repr__` also does not exist (unlikely for non-user 116 | # defined objects), nothing would be captured. For example, if the the 117 | # configuration was set to `'capture_repr': ('_repr_html_')` nothing would be 118 | # captured for example 2 as `b` does not have a `_repr_html_`. 119 | # You can change the 'representations' in the `capture_repr` tuple to finely 120 | # tune what is captured in your example `.py` files. 121 | # 122 | # To only capture data directed to standard output you can set `capture_repr` 123 | # to be an empty tuple: `capture_repr: ()`. With this setting, only data 124 | # directed to standard output is captured. For the examples above, output would 125 | # only be captured for example 4. Although the last statement is an expression 126 | # for examples 2, 3 and 4 no 'representation' of the last expression would be 127 | # output. You would need to add `print()` to the last expression to capture 128 | # a 'representation' of it. 129 | # 130 | # The empty tuple setting imitates the behaviour of Sphinx-Gallery prior to 131 | # v0.5.0, when this configuration was introduced. 132 | -------------------------------------------------------------------------------- /tests/test_gen_single.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import asyncio 3 | import codeop 4 | import sys 5 | from textwrap import dedent 6 | 7 | import pytest 8 | 9 | from mkdocs_gallery.gen_single import _needs_async_handling, _parse_code 10 | from mkdocs_gallery.utils import run_async 11 | 12 | SRC_FILE = __file__ 13 | COMPILER = codeop.Compile() 14 | COMPILER_FLAGS = COMPILER.flags 15 | 16 | 17 | needs_ast_unparse = pytest.mark.skipif( 18 | sys.version_info < (3, 9), reason="ast.unparse is only available for Python >= 3.9" 19 | ) 20 | 21 | 22 | def make_globals(): 23 | return {"__run_async__": run_async} 24 | 25 | 26 | def test_non_async_syntax_error(): 27 | with pytest.raises(SyntaxError, match="unexpected indent"): 28 | _parse_code("foo = None\n bar = None", src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) 29 | 30 | 31 | @needs_ast_unparse 32 | def test_no_async_roundtrip(): 33 | code = "None" 34 | assert not _needs_async_handling(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) 35 | 36 | code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)) 37 | 38 | assert code_unparsed == code 39 | 40 | 41 | @needs_ast_unparse 42 | @pytest.mark.parametrize( 43 | "code", 44 | [ 45 | pytest.param( 46 | dedent( 47 | """ 48 | async def afn(): 49 | return True 50 | 51 | assert await afn() 52 | """ 53 | ), 54 | id="await", 55 | ), 56 | pytest.param( 57 | dedent( 58 | """ 59 | async def agen(): 60 | yield True 61 | 62 | async for item in agen(): 63 | assert item 64 | """ 65 | ), 66 | id="async_for", 67 | ), 68 | pytest.param( 69 | dedent( 70 | """ 71 | async def agen(): 72 | yield True 73 | 74 | assert [item async for item in agen()] == [True] 75 | """ 76 | ), 77 | id="async_comprehension", 78 | ), 79 | pytest.param( 80 | dedent( 81 | """ 82 | import contextlib 83 | 84 | @contextlib.asynccontextmanager 85 | async def acm(): 86 | yield True 87 | 88 | async with acm() as ctx: 89 | assert ctx 90 | """ 91 | ), 92 | id="async_context_manager", 93 | ), 94 | ], 95 | ) 96 | def test_async_handling(code): 97 | assert _needs_async_handling(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) 98 | 99 | # Since AST objects are quite involved to compare, we unparse again and check that nothing has changed. Note that 100 | # since we are dealing with AST and not CST here, all whitespace is eliminated in the process and this needs to be 101 | # reflected in the input as well. 102 | code_stripped = "\n".join(line for line in code.splitlines() if line) 103 | code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)) 104 | assert code_unparsed != code_stripped 105 | 106 | assert not _needs_async_handling(code_unparsed, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) 107 | 108 | exec(COMPILER(code_unparsed, SRC_FILE, "exec"), make_globals()) 109 | 110 | 111 | @needs_ast_unparse 112 | def test_async_handling_locals(): 113 | sentinel = "sentinel" 114 | code = dedent( 115 | """ 116 | async def afn(): 117 | return True 118 | 119 | sentinel = {sentinel} 120 | 121 | assert await afn() 122 | """.format( 123 | sentinel=repr(sentinel) 124 | ) 125 | ) 126 | code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)) 127 | 128 | globals_ = make_globals() 129 | exec(COMPILER(code_unparsed, SRC_FILE, "exec"), globals_) 130 | 131 | assert "sentinel" in globals_ and globals_["sentinel"] == sentinel 132 | 133 | 134 | @needs_ast_unparse 135 | def test_async_handling_last_expression(): 136 | code = dedent( 137 | """ 138 | async def afn(): 139 | return True 140 | 141 | result = await afn() 142 | assert result 143 | result 144 | """ 145 | ) 146 | 147 | code_unparsed_ast = _parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) 148 | code_unparsed = ast.unparse(code_unparsed_ast) 149 | 150 | last = code_unparsed_ast.body[-1] 151 | assert isinstance(last, ast.Expr) 152 | 153 | globals_ = make_globals() 154 | exec(COMPILER(code_unparsed, SRC_FILE, "exec"), globals_) 155 | assert eval(ast.unparse(last.value), globals_) 156 | 157 | 158 | @needs_ast_unparse 159 | def test_get_event_loop_after_async_handling(): 160 | # Non-regression test for https://github.com/smarie/mkdocs-gallery/issues/93 161 | code = dedent( 162 | """ 163 | async def afn(): 164 | return True 165 | 166 | assert await afn() 167 | """ 168 | ) 169 | 170 | code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)) 171 | exec(COMPILER(code_unparsed, SRC_FILE, "exec"), make_globals()) 172 | 173 | asyncio.events.get_event_loop() 174 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/static/sg_gallery.css: -------------------------------------------------------------------------------- 1 | /* 2 | mkdocs-gallery has compatible CSS to fix default sphinx themes 3 | Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, 4 | scrolls, agogo, traditional, nature, haiku, pyramid 5 | Tested for Read the Docs theme 0.1.7 */ 6 | .mkd-glr-thumbcontainer { 7 | background: #fff; 8 | border: solid #fff 1px; 9 | -moz-border-radius: 5px; 10 | -webkit-border-radius: 5px; 11 | border-radius: 5px; 12 | box-shadow: none; 13 | float: left; 14 | margin: 5px; 15 | min-height: 230px; 16 | padding-top: 5px; 17 | position: relative; 18 | } 19 | .mkd-glr-thumbcontainer:hover { 20 | border: solid #b4ddfc 1px; 21 | box-shadow: 0 0 15px rgba(142, 176, 202, 0.5); 22 | } 23 | .mkd-glr-thumbcontainer a.internal { 24 | bottom: 0; 25 | display: block; 26 | left: 0; 27 | padding: 150px 10px 0; 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | } 32 | /* Next one is to avoid Sphinx traditional theme to cover all the 33 | thumbnail with its default link Background color */ 34 | .mkd-glr-thumbcontainer a.internal:hover { 35 | background-color: transparent; 36 | } 37 | 38 | .mkd-glr-thumbcontainer p { 39 | margin: 0 0 .1em 0; 40 | } 41 | .mkd-glr-thumbcontainer .figure { 42 | margin: 10px; 43 | width: 160px; 44 | } 45 | .mkd-glr-thumbcontainer img { 46 | display: inline; 47 | max-height: 112px; 48 | max-width: 160px; 49 | } 50 | .mkd-glr-thumbcontainer[tooltip]:hover:after { 51 | background: rgba(0, 0, 0, 0.8); 52 | -webkit-border-radius: 5px; 53 | -moz-border-radius: 5px; 54 | border-radius: 5px; 55 | color: #fff; 56 | content: attr(tooltip); 57 | left: 95%; 58 | padding: 5px 15px; 59 | position: absolute; 60 | z-index: 98; 61 | width: 220px; 62 | bottom: 52%; 63 | } 64 | .mkd-glr-thumbcontainer[tooltip]:hover:before { 65 | border: solid; 66 | border-color: #333 transparent; 67 | border-width: 18px 0 0 20px; 68 | bottom: 58%; 69 | content: ''; 70 | left: 85%; 71 | position: absolute; 72 | z-index: 99; 73 | } 74 | 75 | /* The wrapper for output sections. Not sure all these are needed, except the color... now that we use code blocks */ 76 | .mkd-glr-script-out { 77 | color: #888; 78 | margin: 0; 79 | } 80 | p.mkd-glr-script-out { 81 | padding-top: 0.7em; 82 | } 83 | .mkd-glr-script-out .highlight { 84 | background-color: transparent; 85 | margin-left: 2.5em; 86 | margin-top: -2.1em; 87 | } 88 | .mkd-glr-script-out .highlight pre { 89 | background-color: #fafae2; 90 | border: 0; 91 | max-height: 30em; 92 | overflow: auto; 93 | padding-left: 1ex; 94 | margin: 0px; 95 | word-break: break-word; 96 | } 97 | .mkd-glr-script-out + p { 98 | margin-top: 1.8em; 99 | } 100 | blockquote.mkd-glr-script-out { 101 | margin-left: 0pt; 102 | } 103 | .mkd-glr-script-out.highlight-pytb .highlight pre { 104 | color: #000; 105 | background-color: #ffe4e4; 106 | border: 1px solid #f66; 107 | margin-top: 10px; 108 | padding: 7px; 109 | } 110 | 111 | /* Traceback code blocks in case of exception */ 112 | .mkd-glr-script-err-disp code { 113 | background-color: #ffe4e4; 114 | } 115 | 116 | /* Script output in case no exception */ 117 | .mkd-glr-script-out-disp code { 118 | max-height: 30em; 119 | background-color: #fafae2; 120 | } 121 | 122 | div.mkd-glr-footer { 123 | text-align: center; 124 | } 125 | 126 | div.mkd-glr-download { 127 | margin: 1em auto; 128 | vertical-align: middle; 129 | } 130 | 131 | div.mkd-glr-download a { 132 | background-color: #ffc; 133 | background-image: linear-gradient(to bottom, #FFC, #d5d57e); 134 | border-radius: 4px; 135 | border: 1px solid #c2c22d; 136 | color: #000; 137 | display: inline-block; 138 | font-weight: bold; 139 | padding: 1ex; 140 | text-align: center; 141 | } 142 | 143 | div.mkd-glr-download code.download { 144 | display: inline-block; 145 | white-space: normal; 146 | word-break: normal; 147 | overflow-wrap: break-word; 148 | /* border and background are given by the enclosing 'a' */ 149 | border: none; 150 | background: none; 151 | } 152 | 153 | div.mkd-glr-download a:hover { 154 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 155 | text-decoration: none; 156 | background-image: none; 157 | background-color: #d5d57e; 158 | } 159 | 160 | .mkd-glr-example-title:target::before { 161 | display: block; 162 | content: ""; 163 | margin-top: -50px; 164 | height: 50px; 165 | visibility: hidden; 166 | } 167 | 168 | ul.mkd-glr-horizontal { 169 | list-style: none; 170 | padding: 0; 171 | } 172 | ul.mkd-glr-horizontal li { 173 | display: inline; 174 | } 175 | ul.sphx-glr-horizontal img { 176 | height: auto !important; 177 | } 178 | 179 | .sphx-glr-single-img { 180 | margin: auto; 181 | display: block; 182 | max-width: 100%; 183 | } 184 | 185 | .sphx-glr-multi-img { 186 | max-width: 42%; 187 | height: auto; 188 | } 189 | 190 | div.mkd-glr-animation { 191 | margin: auto; 192 | display: block; 193 | max-width: 100%; 194 | } 195 | div.mkd-glr-animation .animation{ 196 | display: block; 197 | } 198 | 199 | /* was p.sphx-glr-signature a.reference.external */ 200 | a.mkd-glr-signature { 201 | -moz-border-radius: 5px; 202 | -webkit-border-radius: 5px; 203 | border-radius: 5px; 204 | padding: 3px; 205 | font-size: 75%; 206 | text-align: right; 207 | margin-left: auto; 208 | display: table; 209 | } 210 | 211 | .mkd-glr-clear{ 212 | clear: both; 213 | } 214 | 215 | a.mkd-glr-backref-instance { 216 | text-decoration: none; 217 | } 218 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # See https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files 2 | # And this great example : https://github.com/Kinto/kinto/blob/master/setup.cfg 3 | [metadata] 4 | name = mkdocs-gallery 5 | description = a `mkdocs` plugin to generate example galleries from python scripts, similar to `sphinx-gallery`. 6 | description_file = README.md 7 | license = BSD 3-Clause 8 | long_description = file: docs/long_description.md 9 | long_description_content_type=text/markdown 10 | keywords = gallery web page generator figure jupyter notebook binder example code latex mkdocs sphinx 11 | author = Sylvain MARIE 12 | url = https://github.com/smarie/mkdocs-gallery 13 | # download_url = https://github.com/smarie/mkdocs-gallery/tarball/master >> do it in the setup.py to get the right version 14 | classifiers = 15 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 16 | Development Status :: 5 - Production/Stable 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Environment :: Plugins 20 | Environment :: Web Environment 21 | Topic :: Documentation 22 | Programming Language :: Python 23 | ; Programming Language :: Python :: 2 24 | ; Programming Language :: Python :: 2.7 25 | ; Programming Language :: Python :: 3 26 | ; Programming Language :: Python :: 3.5 27 | ; Programming Language :: Python :: 3.6 28 | Programming Language :: Python :: 3.7 29 | Programming Language :: Python :: 3.8 30 | Programming Language :: Python :: 3.9 31 | Programming Language :: Python :: 3.10 32 | Programming Language :: Python :: 3.11 33 | 34 | [options] 35 | # one day these will be able to come from requirement files, see https://github.com/pypa/setuptools/issues/1951. But will it be better ? 36 | setup_requires = 37 | setuptools_scm 38 | pytest-runner 39 | install_requires = 40 | mkdocs>=1,<2 41 | mkdocs-material 42 | tqdm 43 | packaging 44 | tests_require = 45 | pytest 46 | # for some reason these pytest dependencies were not declared in old versions of pytest 47 | ; six;python_version<'3.6' 48 | ; attr;python_version<'3.6' 49 | ; pluggy;python_version<'3.6' 50 | 51 | # test_suite = tests --> no need apparently 52 | # 53 | zip_safe = False 54 | # explicitly setting zip_safe=False to avoid downloading `ply` see https://github.com/smarie/python-getversion/pull/5 55 | # and makes mypy happy see https://mypy.readthedocs.io/en/latest/installed_packages.html 56 | package_dir= 57 | =src 58 | packages = find: 59 | # see [options.packages.find] below 60 | # IMPORTANT: DO NOT set the `include_package_data` flag !! It triggers inclusion of all git-versioned files 61 | # see https://github.com/pypa/setuptools_scm/issues/190#issuecomment-351181286 62 | # include_package_data = True 63 | [options.packages.find] 64 | where=src 65 | exclude = 66 | contrib 67 | docs 68 | *tests* 69 | 70 | [options.package_data] 71 | * = py.typed, *.pyi, static/* 72 | 73 | 74 | # Optional dependencies that can be installed with e.g. $ pip install -e .[dev,test] 75 | # [options.extras_require] 76 | 77 | # -------------- Packaging ----------- 78 | [options.entry_points] 79 | mkdocs.plugins = 80 | gallery = mkdocs_gallery.plugin:GalleryPlugin 81 | 82 | # [egg_info] >> already covered by setuptools_scm 83 | 84 | [bdist_wheel] 85 | # Code is written to work on both Python 2 and Python 3. 86 | universal=1 87 | 88 | # ------------- Others ------------- 89 | # In order to be able to execute 'python setup.py test' 90 | # from https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner 91 | [aliases] 92 | test = pytest 93 | 94 | # pytest default configuration 95 | [tool:pytest] 96 | testpaths = tests/ 97 | addopts = 98 | --verbose 99 | --doctest-modules 100 | --ignore-glob='**/_*.py' 101 | 102 | # we need the 'always' for python 2 tests to work see https://github.com/pytest-dev/pytest/issues/2917 103 | filterwarnings = 104 | always 105 | ; ignore::UserWarning 106 | 107 | # Coverage config 108 | [coverage:run] 109 | branch = True 110 | omit = *tests* 111 | # this is done in nox.py (github actions) or ci_tools/run_tests.sh (travis) 112 | # source = mkdocs-gallery 113 | # command_line = -m pytest --junitxml="reports/pytest_reports/pytest.xml" --html="reports/pytest_reports/pytest.html" -v mkdocs-gallery/tests/ 114 | 115 | [coverage:report] 116 | fail_under = 15 117 | # TODO raise the bar again :) 118 | show_missing = True 119 | exclude_lines = 120 | # this line for all the python 2 not covered lines 121 | except ImportError: 122 | # we have to repeat this when exclude_lines is set 123 | pragma: no cover 124 | 125 | # Done in nox.py 126 | # [coverage:html] 127 | # directory = site/reports/coverage_reports 128 | # [coverage:xml] 129 | # output = site/reports/coverage_reports/coverage.xml 130 | 131 | [flake8] 132 | max-line-length = 120 133 | extend-ignore = D, E203 # D: Docstring errors, E203: see https://github.com/PyCQA/pycodestyle/issues/373 134 | copyright-check = True 135 | copyright-regexp = ^\#\s+Authors:\s+Sylvain MARIE \n\#\s+\+\sAll contributors to \n\#\n\#\s+Original idea and code: sphinx\-gallery, \n\#\s+License: 3\-clause BSD, 136 | exclude = 137 | .git 138 | .github 139 | .nox 140 | .pytest_cache 141 | ci_tools 142 | docs 143 | tests 144 | noxfile.py 145 | setup.py 146 | */_version.py 147 | 148 | [isort] 149 | # Isort config preconized not to interfere with `black` 150 | multi_line_output = 3 151 | include_trailing_comma = True 152 | force_grid_wrap = 0 153 | use_parentheses = True 154 | ensure_newline_before_comments = True 155 | line_length = 120 156 | -------------------------------------------------------------------------------- /ci_tools/github_release.py: -------------------------------------------------------------------------------- 1 | # a clone of the ruby example https://gist.github.com/valeriomazzeo/5491aee76f758f7352e2e6611ce87ec1 2 | import os 3 | from os import path 4 | 5 | import re 6 | 7 | import click 8 | from click import Path 9 | from github import Github, UnknownObjectException 10 | # from valid8 import validate not compliant with python 2.7 11 | 12 | 13 | @click.command() 14 | @click.option('-u', '--user', help='GitHub username') 15 | @click.option('-p', '--pwd', help='GitHub password') 16 | @click.option('-s', '--secret', help='GitHub access token') 17 | @click.option('-r', '--repo-slug', help='Repo slug. i.e.: apple/swift') 18 | @click.option('-cf', '--changelog-file', help='Changelog file path') 19 | @click.option('-d', '--doc-url', help='Documentation url') 20 | @click.option('-df', '--data-file', help='Data file to upload', type=Path(exists=True, file_okay=True, dir_okay=False, 21 | resolve_path=True)) 22 | @click.argument('tag') 23 | def create_or_update_release(user, pwd, secret, repo_slug, changelog_file, doc_url, data_file, tag): 24 | """ 25 | Creates or updates (TODO) 26 | a github release corresponding to git tag . 27 | """ 28 | # 1- AUTHENTICATION 29 | if user is not None and secret is None: 30 | # using username and password 31 | # validate('user', user, instance_of=str) 32 | assert isinstance(user, str) 33 | # validate('pwd', pwd, instance_of=str) 34 | assert isinstance(pwd, str) 35 | g = Github(user, pwd) 36 | elif user is None and secret is not None: 37 | # or using an access token 38 | # validate('secret', secret, instance_of=str) 39 | assert isinstance(secret, str) 40 | g = Github(secret) 41 | else: 42 | raise ValueError("You should either provide username/password OR an access token") 43 | click.echo("Logged in as {user_name}".format(user_name=g.get_user())) 44 | 45 | # 2- CHANGELOG VALIDATION 46 | regex_pattern = "[\s\S]*[\n][#]+[\s]*(?P[\S ]*%s[\S ]*)[\n]+?(?P<body>[\s\S]*?)[\n]*?(\n#|$)" % re.escape(tag) 47 | changelog_section = re.compile(regex_pattern) 48 | if changelog_file is not None: 49 | # validate('changelog_file', changelog_file, custom=os.path.exists, 50 | # help_msg="changelog file should be a valid file path") 51 | assert os.path.exists(changelog_file), "changelog file should be a valid file path" 52 | with open(changelog_file) as f: 53 | contents = f.read() 54 | 55 | match = changelog_section.match(contents).groupdict() 56 | if match is None or len(match) != 2: 57 | raise ValueError("Unable to find changelog section matching regexp pattern in changelog file.") 58 | else: 59 | title = match['title'] 60 | message = match['body'] 61 | else: 62 | title = tag 63 | message = '' 64 | 65 | # append footer if doc url is provided 66 | message += "\n\nSee [documentation page](%s) for details." % doc_url 67 | 68 | # 3- REPOSITORY EXPLORATION 69 | # validate('repo_slug', repo_slug, instance_of=str, min_len=1, help_msg="repo_slug should be a non-empty string") 70 | assert isinstance(repo_slug, str) and len(repo_slug) > 0, "repo_slug should be a non-empty string" 71 | repo = g.get_repo(repo_slug) 72 | 73 | # -- Is there a tag with that name ? 74 | try: 75 | tag_ref = repo.get_git_ref("tags/" + tag) 76 | except UnknownObjectException: 77 | raise ValueError("No tag with name %s exists in repository %s" % (tag, repo.name)) 78 | 79 | # -- Is there already a release with that tag name ? 80 | click.echo("Checking if release %s already exists in repository %s" % (tag, repo.name)) 81 | try: 82 | release = repo.get_release(tag) 83 | if release is not None: 84 | raise ValueError("Release %s already exists in repository %s. Please set overwrite to True if you wish to " 85 | "update the release (Not yet supported)" % (tag, repo.name)) 86 | except UnknownObjectException: 87 | # Release does not exist: we can safely create it. 88 | click.echo("Creating release %s on repo: %s" % (tag, repo.name)) 89 | click.echo("Release title: '%s'" % title) 90 | click.echo("Release message:\n--\n%s\n--\n" % message) 91 | repo.create_git_release(tag=tag, name=title, 92 | message=message, 93 | draft=False, prerelease=False) 94 | 95 | # add the asset file if needed 96 | if data_file is not None: 97 | release = None 98 | while release is None: 99 | release = repo.get_release(tag) 100 | release.upload_asset(path=data_file, label=path.split(data_file)[1], content_type="application/gzip") 101 | 102 | # --- Memo --- 103 | # release.target_commitish # 'master' 104 | # release.tag_name # '0.5.0' 105 | # release.title # 'First public release' 106 | # release.body # markdown body 107 | # release.draft # False 108 | # release.prerelease # False 109 | # # 110 | # release.author 111 | # release.created_at # datetime.datetime(2018, 11, 9, 17, 49, 56) 112 | # release.published_at # datetime.datetime(2018, 11, 9, 20, 11, 10) 113 | # release.last_modified # None 114 | # # 115 | # release.id # 13928525 116 | # release.etag # 'W/"dfab7a13086d1b44fe290d5d04125124"' 117 | # release.url # 'https://api.github.com/repos/smarie/python-mkdocs-gallery/releases/13928525' 118 | # release.html_url # 'https://github.com/smarie/python-mkdocs-gallery/releases/tag/0.5.0' 119 | # release.tarball_url # 'https://api.github.com/repos/smarie/python-mkdocs-gallery/tarball/0.5.0' 120 | # release.zipball_url # 'https://api.github.com/repos/smarie/python-mkdocs-gallery/zipball/0.5.0' 121 | # release.upload_url # 'https://uploads.github.com/repos/smarie/python-mkdocs-gallery/releases/13928525/assets{?name,label}' 122 | 123 | 124 | if __name__ == '__main__': 125 | create_or_update_release() 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-gallery 2 | 3 | *[Sphinx-Gallery](https://sphinx-gallery.github.io/) features for [mkdocs](https://www.mkdocs.org/) (no [Sphinx](sphinx-doc.org/) dependency !).* 4 | 5 | [![Python versions](https://img.shields.io/pypi/pyversions/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Build Status](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/mkdocs-gallery/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/mkdocs-gallery/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/mkdocs-gallery/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/mkdocs-gallery) [![Flake8 Status](https://smarie.github.io/mkdocs-gallery/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/mkdocs-gallery/reports/flake8/index.html) 6 | 7 | [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/mkdocs-gallery/) [![PyPI](https://img.shields.io/pypi/v/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Downloads](https://pepy.tech/badge/mkdocs-gallery)](https://pepy.tech/project/mkdocs-gallery) [![Downloads per week](https://pepy.tech/badge/mkdocs-gallery/week)](https://pepy.tech/project/mkdocs-gallery) [![GitHub stars](https://img.shields.io/github/stars/smarie/mkdocs-gallery.svg)](https://github.com/smarie/mkdocs-gallery/stargazers) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5786851.svg)](https://doi.org/10.5281/zenodo.5786851) 8 | 9 | Do you love [Sphinx-Gallery](https://sphinx-gallery.github.io/) but prefer [mkdocs](https://www.mkdocs.org/) over [Sphinx](sphinx-doc.org/) for your documentation ? `mkdocs-gallery` was written for you ;) 10 | 11 | It relies on [mkdocs-material](https://squidfunk.github.io/mkdocs-material) to get the most of mkdocs, so that your galleries look nice! 12 | 13 | **This is the readme for developers.** The documentation for users is available here: [https://smarie.github.io/mkdocs-gallery/](https://smarie.github.io/mkdocs-gallery/) 14 | 15 | ## Want to contribute ? 16 | 17 | Contributions are welcome ! Simply fork this project on github, commit your contributions, and create pull requests. 18 | 19 | Here is a non-exhaustive list of interesting open topics: [https://github.com/smarie/mkdocs-gallery/issues](https://github.com/smarie/mkdocs-gallery/issues) 20 | 21 | ## `nox` setup 22 | 23 | This project uses `nox` to define all lifecycle tasks. In order to be able to run those tasks, you should create python 3.7 environment and install the requirements: 24 | 25 | ```bash 26 | >>> conda create -n noxenv python="3.7" 27 | >>> activate noxenv 28 | (noxenv) >>> pip install -r noxfile-requirements.txt 29 | ``` 30 | 31 | You should then be able to list all available tasks using: 32 | 33 | ``` 34 | >>> nox --list 35 | Sessions defined in <path>\noxfile.py: 36 | 37 | * tests-2.7 -> Run the test suite, including test reports generation and coverage reports. 38 | * tests-3.5 -> Run the test suite, including test reports generation and coverage reports. 39 | * tests-3.6 -> Run the test suite, including test reports generation and coverage reports. 40 | * tests-3.8 -> Run the test suite, including test reports generation and coverage reports. 41 | * tests-3.7 -> Run the test suite, including test reports generation and coverage reports. 42 | - docs-3.7 -> Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead. 43 | - publish-3.7 -> Deploy the docs+reports on github pages. Note: this rebuilds the docs 44 | - release-3.7 -> Create a release on github corresponding to the latest tag 45 | ``` 46 | 47 | ## Running the tests and generating the reports 48 | 49 | This project uses `pytest` so running `pytest` at the root folder will execute all tests on current environment. However it is a bit cumbersome to manage all requirements by hand ; it is easier to use `nox` to run `pytest` on all supported python environments with the correct package requirements: 50 | 51 | ```bash 52 | nox 53 | ``` 54 | 55 | Tests and coverage reports are automatically generated under `https://smarie.github.io/mkdocs-gallery/docs/reports` for one of the sessions (`tests-3.7`). 56 | 57 | If you wish to execute tests on a specific environment, use explicit session names, e.g. `nox -s tests-3.6`. 58 | 59 | 60 | ## Editing the documentation 61 | 62 | This project uses `mkdocs` to generate its documentation page. Therefore building a local copy of the doc page may be done using `mkdocs build -f mkdocs.yml`. However once again things are easier with `nox`. You can easily build and serve locally a version of the documentation site using: 63 | 64 | ```bash 65 | >>> nox -s docs 66 | nox > Running session docs-3.7 67 | nox > Creating conda env in .nox\docs-3-7 with python=3.7 68 | nox > [docs] Installing requirements with pip: ['mkdocs-material', 'mkdocs', 'pymdown-extensions', 'pygments'] 69 | nox > python -m pip install mkdocs-material mkdocs pymdown-extensions pygments 70 | nox > mkdocs serve -f https://smarie.github.io/mkdocs-gallery/docs/mkdocs.yml 71 | INFO - Building documentation... 72 | INFO - Cleaning site directory 73 | INFO - The following pages exist in the docs directory, but are not included in the "nav" configuration: 74 | - long_description.md 75 | INFO - Documentation built in 1.07 seconds 76 | INFO - Serving on http://127.0.0.1:8000 77 | INFO - Start watching changes 78 | ... 79 | ``` 80 | 81 | While this is running, you can edit the files under `https://smarie.github.io/mkdocs-gallery/docs/` and browse the automatically refreshed documentation at the local [http://127.0.0.1:8000](http://127.0.0.1:8000) page. 82 | 83 | Once you are done, simply hit `<CTRL+C>` to stop the session. 84 | 85 | Publishing the documentation (including tests and coverage reports) is done automatically by [the continuous integration engine](https://github.com/smarie/mkdocs-gallery/actions), using the `nox -s publish` session, this is not needed for local development. 86 | 87 | ## Packaging 88 | 89 | This project uses `setuptools_scm` to synchronise the version number. Therefore the following command should be used for development snapshots as well as official releases: `python setup.py sdist bdist_wheel`. However this is not generally needed since [the continuous integration engine](https://github.com/smarie/mkdocs-gallery/actions) does it automatically for us on git tags. For reference, this is done in the `nox -s release` session. 90 | 91 | ### Merging pull requests with edits - memo 92 | 93 | As explained in github ('get commandline instructions'): 94 | 95 | ```bash 96 | git checkout -b <git_name>-<feature_branch> main 97 | git pull https://github.com/<git_name>/mkdocs-gallery.git <feature_branch> --no-commit --ff-only 98 | ``` 99 | 100 | if the second step does not work, do a normal auto-merge (do not use **rebase**!): 101 | 102 | ```bash 103 | git pull https://github.com/<git_name>/mkdocs-gallery.git <feature_branch> --no-commit 104 | ``` 105 | 106 | Finally review the changes, possibly perform some modifications, and commit. 107 | -------------------------------------------------------------------------------- /ci_tools/nox_utils.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import logging 3 | from pathlib import Path 4 | import shutil 5 | import os 6 | 7 | from typing import Sequence, Dict, Union 8 | 9 | import nox 10 | 11 | 12 | nox_logger = logging.getLogger("nox") 13 | 14 | 15 | PY27, PY35, PY36, PY37, PY38, PY39, PY310, PY311, PY312, PY313 = ("2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", 16 | "3.11", "3.12", "3.13") 17 | DONT_INSTALL = "dont_install" 18 | 19 | 20 | def install_reqs( 21 | session, 22 | # pre wired phases 23 | setup=False, 24 | install=False, 25 | tests=False, 26 | extras=(), 27 | # custom phase 28 | phase=None, 29 | phase_reqs=None, 30 | versions_dct=None 31 | ): 32 | """ 33 | A high-level helper to install requirements from the various project files 34 | 35 | - pyproject.toml "[build-system] requires" (if setup=True) 36 | - setup.cfg "[options] setup_requires" (if setup=True) 37 | - setup.cfg "[options] install_requires" (if install=True) 38 | - setup.cfg "[options] test_requires" (if tests=True) 39 | - setup.cfg "[options.extras_require] <...>" (if extras=(a tuple of extras)) 40 | 41 | Two additional mechanisms are provided in order to customize how packages are installed. 42 | 43 | Conda packages 44 | -------------- 45 | If the session runs on a conda environment, you can add a [tool.conda] section to your pyproject.toml. This 46 | section should contain a `conda_packages` entry containing the list of package names that should be installed 47 | using conda instead of pip. 48 | 49 | ``` 50 | [tool.conda] 51 | # Declare that the following packages should be installed with conda instead of pip 52 | # Note: this includes packages declared everywhere, here and in setup.cfg 53 | conda_packages = [ 54 | "setuptools", 55 | "wheel", 56 | "pip" 57 | ] 58 | ``` 59 | 60 | Version constraints 61 | ------------------- 62 | In addition to the version constraints in the pyproject.toml and setup.cfg, you can specify additional temporary 63 | constraints with the `versions_dct` argument , for example if you know that this executes on a specific python 64 | version that requires special care. 65 | For this, simply pass a dictionary of {'pkg_name': 'pkg_constraint'} for example {"pip": ">10"}. 66 | 67 | """ 68 | 69 | # Read requirements from pyproject.toml 70 | toml_setup_reqs, toml_use_conda_for = read_pyproject_toml() 71 | if setup: 72 | install_any(session, "pyproject.toml#build-system", toml_setup_reqs, 73 | use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 74 | 75 | # Read test requirements from setup.cfg 76 | setup_cfg = read_setuptools_cfg() 77 | if setup: 78 | install_any(session, "setup.cfg#setup_requires", setup_cfg.setup_requires, 79 | use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 80 | if install: 81 | install_any(session, "setup.cfg#install_requires", setup_cfg.install_requires, 82 | use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 83 | if tests: 84 | install_any(session, "setup.cfg#tests_requires", setup_cfg.tests_requires, 85 | use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 86 | 87 | for extra in extras: 88 | install_any(session, "setup.cfg#extras_require#%s" % extra, setup_cfg.extras_require[extra], 89 | use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 90 | 91 | if phase is not None: 92 | install_any(session, phase, phase_reqs, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) 93 | 94 | 95 | def install_any(session, 96 | phase_name: str, 97 | pkgs: Sequence[str], 98 | use_conda_for: Sequence[str] = (), 99 | versions_dct: Dict[str, str] = None, 100 | ): 101 | """Install the `pkgs` provided with `session.install(*pkgs)`, except for those present in `use_conda_for`""" 102 | 103 | # use the provided versions dictionary to update the versions 104 | if versions_dct is None: 105 | versions_dct = dict() 106 | pkgs = [pkg + versions_dct.get(pkg, "") for pkg in pkgs if versions_dct.get(pkg, "") != DONT_INSTALL] 107 | 108 | nox_logger.debug("\nAbout to install *%s* requirements: %s.\n " 109 | "Conda pkgs are %s" % (phase_name, pkgs, use_conda_for)) 110 | 111 | # install on conda... if the session uses conda backend 112 | if not isinstance(session.virtualenv, nox.virtualenv.CondaEnv): 113 | conda_pkgs = [] 114 | else: 115 | conda_pkgs = [pkg_req for pkg_req in pkgs if any(get_req_pkg_name(pkg_req) == c for c in use_conda_for)] 116 | if len(conda_pkgs) > 0: 117 | nox_logger.info("[%s] Installing requirements with conda: %s" % (phase_name, conda_pkgs)) 118 | session.conda_install(*conda_pkgs) 119 | 120 | pip_pkgs = [pkg_req for pkg_req in pkgs if pkg_req not in conda_pkgs] 121 | # safety: make sure that nothing went modified or forgotten 122 | assert set(conda_pkgs).union(set(pip_pkgs)) == set(pkgs) 123 | if len(pip_pkgs) > 0: 124 | nox_logger.info("[%s] Installing requirements with pip: %s" % (phase_name, pip_pkgs)) 125 | session.install(*pip_pkgs) 126 | 127 | 128 | # ------------- requirements related 129 | 130 | 131 | def read_pyproject_toml() -> Union[list, list]: 132 | """ 133 | Reads the `pyproject.toml` and returns 134 | 135 | - a list of setup requirements from [build-system] requires 136 | - sub-list of these requirements that should be installed with conda, from [tool.my_conda] conda_packages 137 | """ 138 | if os.path.exists("pyproject.toml"): 139 | import toml 140 | nox_logger.debug("\nA `pyproject.toml` file exists. Loading it.") 141 | pyproject = toml.load("pyproject.toml") 142 | requires = pyproject['build-system']['requires'] 143 | try: 144 | conda_pkgs = pyproject['tool']['conda']['conda_packages'] 145 | except KeyError: 146 | conda_pkgs = [] 147 | return requires, conda_pkgs 148 | else: 149 | raise FileNotFoundError("No `pyproject.toml` file exists. No dependency will be installed ...") 150 | 151 | 152 | SetupCfg = namedtuple('SetupCfg', ('setup_requires', 'install_requires', 'tests_requires', 'extras_require')) 153 | 154 | 155 | def read_setuptools_cfg(): 156 | """ 157 | Reads the `setup.cfg` file and extracts the various requirements lists 158 | """ 159 | # see https://stackoverflow.com/a/30679041/7262247 160 | from setuptools import Distribution 161 | dist = Distribution() 162 | dist.parse_config_files() 163 | return SetupCfg(setup_requires=dist.setup_requires, 164 | install_requires=dist.install_requires, 165 | tests_requires=dist.tests_require, 166 | extras_require=dist.extras_require) 167 | 168 | 169 | def get_req_pkg_name(r): 170 | """Return the package name part of a python package requirement. 171 | 172 | For example 173 | "funcsigs;python<'3.5'" will return "funcsigs" 174 | "pytest>=3" will return "pytest" 175 | """ 176 | return r.replace('<', '=').replace('>', '=').replace(';', '=').split("=")[0] 177 | 178 | 179 | # ----------- other goodies 180 | 181 | 182 | def rm_file(folder: Union[str, Path]): 183 | """Since on windows Path.unlink throws permission error sometimes, os.remove is preferred.""" 184 | if isinstance(folder, str): 185 | folder = Path(folder) 186 | 187 | if folder.exists(): 188 | os.remove(str(folder)) 189 | # Folders.site.unlink() --> possible PermissionError 190 | 191 | 192 | def rm_folder(folder: Union[str, Path]): 193 | """Since on windows Path.unlink throws permission error sometimes, shutil is preferred.""" 194 | if isinstance(folder, str): 195 | folder = Path(folder) 196 | 197 | if folder.exists(): 198 | shutil.rmtree(str(folder)) 199 | # Folders.site.unlink() --> possible PermissionError 200 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/py_source_parser.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE <sylvain.marie@se.com> 2 | # + All contributors to <https://github.com/smarie/mkdocs-gallery> 3 | # 4 | # Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 5 | # License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 6 | """ 7 | Parser for python source files 8 | """ 9 | 10 | from __future__ import absolute_import, division, print_function 11 | 12 | import ast 13 | import platform 14 | import re 15 | import tokenize 16 | from packaging.version import parse as parse_version 17 | from io import BytesIO 18 | from pathlib import Path 19 | from textwrap import dedent 20 | from typing import Dict, List, Tuple, Union 21 | 22 | from .errors import ExtensionError 23 | from .mkdocs_compatibility import getLogger 24 | 25 | logger = getLogger("mkdocs-gallery") 26 | 27 | SYNTAX_ERROR_DOCSTRING = """ 28 | SyntaxError 29 | =========== 30 | 31 | Example script with invalid Python syntax 32 | """ 33 | 34 | # The pattern for in-file config comments is designed to not greedily match 35 | # newlines at the start and end, except for one newline at the end. This 36 | # ensures that the matched pattern can be removed from the code without 37 | # changing the block structure; i.e. empty newlines are preserved, e.g. in 38 | # 39 | # a = 1 40 | # 41 | # # mkdocs_gallery_thumbnail_number = 2 42 | # 43 | # b = 2 44 | INFILE_CONFIG_PATTERN = re.compile(r"^[\ \t]*#\s*mkdocs_gallery_([A-Za-z0-9_]+)(\s*=\s*(.+))?[\ \t]*\n?", re.MULTILINE) 45 | 46 | 47 | def parse_source_file(file: Path): 48 | """Parse source file into AST node. 49 | 50 | Parameters 51 | ---------- 52 | file : Path 53 | File path 54 | 55 | Returns 56 | ------- 57 | node : AST node 58 | content : utf-8 encoded string 59 | """ 60 | # with codecs.open(filename, 'r', 'utf-8') as fid: 61 | # content = fid.read() 62 | content = file.read_text(encoding="utf-8") 63 | 64 | # change from Windows format to UNIX for uniformity 65 | content = content.replace("\r\n", "\n") 66 | 67 | try: 68 | node = ast.parse(content) 69 | return node, content 70 | except SyntaxError: 71 | return None, content 72 | 73 | 74 | def _get_docstring_and_rest(file: Path): 75 | """Separate ``filename`` content between docstring and the rest. 76 | 77 | Strongly inspired from ast.get_docstring. 78 | 79 | Parameters 80 | ---------- 81 | file : Path 82 | The source file 83 | 84 | Returns 85 | ------- 86 | docstring : str 87 | docstring of ``filename`` 88 | rest : str 89 | ``filename`` content without the docstring 90 | lineno : int 91 | The line number. 92 | node : ast Node 93 | The node. 94 | """ 95 | node, content = parse_source_file(file) 96 | 97 | if node is None: 98 | return SYNTAX_ERROR_DOCSTRING, content, 1, node 99 | 100 | if not isinstance(node, ast.Module): 101 | raise ExtensionError("This function only supports modules. " "You provided {0}".format(node.__class__.__name__)) 102 | if not (node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str)): 103 | raise ExtensionError( 104 | f'Could not find docstring in file "{file}". ' 105 | "A docstring is required by mkdocs-gallery " 106 | 'unless the file is ignored by "ignore_pattern"' 107 | ) 108 | 109 | if parse_version(platform.python_version()) >= parse_version("3.7"): 110 | docstring = ast.get_docstring(node) 111 | assert docstring is not None # noqa # should be guaranteed above 112 | # This is just for backward compat 113 | if len(node.body[0].value.s) and node.body[0].value.s[0] == "\n": 114 | # just for strict backward compat here 115 | docstring = "\n" + docstring 116 | ts = tokenize.tokenize(BytesIO(content.encode()).readline) 117 | # find the first string according to the tokenizer and get its end row 118 | for tk in ts: 119 | if tk.exact_type == 3: 120 | lineno, _ = tk.end 121 | break 122 | else: 123 | lineno = 0 124 | else: 125 | # TODO this block can be removed when python 3.6 support is dropped 126 | docstring_node = node.body[0] 127 | docstring = docstring_node.value.s 128 | lineno = docstring_node.lineno # The last line of the string. 129 | 130 | # This get the content of the file after the docstring last line 131 | # Note: 'maxsplit' argument is not a keyword argument in python2 132 | rest = "\n".join(content.split("\n")[lineno:]) 133 | lineno += 1 134 | return docstring, rest, lineno, node 135 | 136 | 137 | def extract_file_config(content): 138 | """ 139 | Pull out the file-specific config specified in the docstring. 140 | """ 141 | file_conf = {} 142 | for match in re.finditer(INFILE_CONFIG_PATTERN, content): 143 | name = match.group(1) 144 | value = match.group(3) 145 | if value is None: # a flag rather than a config setting 146 | continue 147 | try: 148 | value = ast.literal_eval(value) 149 | except (SyntaxError, ValueError): 150 | logger.warning("mkdocs-gallery option %s was passed invalid value %s", name, value) 151 | else: 152 | file_conf[name] = value 153 | return file_conf 154 | 155 | 156 | def split_code_and_text_blocks( 157 | source_file: Union[str, Path], return_node=False 158 | ) -> Union[Tuple[Dict, List], Tuple[Dict, List, ast.AST]]: 159 | """Return list with source file separated into code and text blocks. 160 | 161 | Parameters 162 | ---------- 163 | source_file : Union[str, Path] 164 | Path to the source file. 165 | return_node : bool 166 | If True, return the ast node. 167 | 168 | Returns 169 | ------- 170 | file_conf : dict 171 | File-specific settings given in source file comments as: 172 | ``# mkdocs_gallery_<name> = <value>`` 173 | blocks : list 174 | (label, content, line_number) 175 | List where each element is a tuple with the label ('text' or 'code'), 176 | the corresponding content string of block and the leading line number 177 | node : ast Node 178 | The parsed node. 179 | """ 180 | source_file = Path(source_file) 181 | docstring, rest_of_content, lineno, node = _get_docstring_and_rest(source_file) 182 | blocks = [("text", docstring, 1)] 183 | 184 | file_conf = extract_file_config(rest_of_content) 185 | 186 | pattern = re.compile( 187 | r"(?P<header_line>^#{20,}.*|^# ?%%.*)\s(?P<text_content>(?:^#.*\s?)*)", 188 | flags=re.M, 189 | ) 190 | sub_pat = re.compile("^#", flags=re.M) 191 | 192 | pos_so_far = 0 193 | for match in re.finditer(pattern, rest_of_content): 194 | code_block_content = rest_of_content[pos_so_far : match.start()] 195 | if code_block_content.strip(): 196 | blocks.append(("code", code_block_content, lineno)) 197 | lineno += code_block_content.count("\n") 198 | 199 | lineno += 1 # Ignored header line of hashes. 200 | text_content = match.group("text_content") 201 | text_block_content = dedent(re.sub(sub_pat, "", text_content)).lstrip() 202 | if text_block_content.strip(): 203 | blocks.append(("text", text_block_content, lineno)) 204 | lineno += text_content.count("\n") 205 | 206 | pos_so_far = match.end() 207 | 208 | remaining_content = rest_of_content[pos_so_far:] 209 | if remaining_content.strip(): 210 | blocks.append(("code", remaining_content, lineno)) 211 | 212 | out = (file_conf, blocks) 213 | if return_node: 214 | out += (node,) 215 | return out 216 | 217 | 218 | def remove_config_comments(code_block): 219 | """ 220 | Return the content of *code_block* with in-file config comments removed. 221 | 222 | Comment lines of the pattern '# mkdocs_gallery_[option] = [val]' are 223 | removed, but surrounding empty lines are preserved. 224 | 225 | Parameters 226 | ---------- 227 | code_block : str 228 | A code segment. 229 | """ 230 | parsed_code, _ = re.subn(INFILE_CONFIG_PATTERN, "", code_block) 231 | return parsed_code 232 | -------------------------------------------------------------------------------- /.github/workflows/base.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/base.yml 2 | name: Build 3 | on: 4 | # this one is to trigger the workflow manually from the interface 5 | workflow_dispatch: 6 | 7 | push: 8 | tags: 9 | - '*' 10 | branches: 11 | - main 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | defaults: 17 | run: 18 | shell: bash -l {0} 19 | 20 | jobs: 21 | # pre-job to read nox tests matrix - see https://stackoverflow.com/q/66747359/7262247 22 | list_nox_test_sessions: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4.1.1 27 | 28 | - name: Install python 3.9 29 | uses: actions/setup-python@v5.0.0 30 | with: 31 | python-version: 3.9 32 | architecture: x64 33 | 34 | - name: Install noxfile requirements 35 | run: pip install -r noxfile-requirements.txt 36 | 37 | - name: List 'tests' nox sessions and required python versions 38 | id: set-matrix 39 | run: echo "matrix=$(nox --json -l -s tests -v)" >> $GITHUB_OUTPUT 40 | 41 | outputs: 42 | matrix: ${{ steps.set-matrix.outputs.matrix }} # save nox sessions list to outputs 43 | 44 | run_all_tests: 45 | needs: list_nox_test_sessions 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | # see https://github.com/actions/setup-python/issues/544 50 | # os: [ ubuntu-20.04 ] 51 | os: [ ubuntu-latest, windows-latest ] # , macos-latest, windows-latest] 52 | # all nox sessions: manually > dynamically from previous job 53 | # nox_session: ["tests-2.7", "tests-3.7"] 54 | nox_session: ${{ fromJson(needs.list_nox_test_sessions.outputs.matrix) }} 55 | 56 | name: ${{ matrix.os }} ${{ matrix.nox_session.python }} ${{ matrix.nox_session.session }} # ${{ matrix.name_suffix }} 57 | runs-on: ${{ matrix.os }} 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4.1.1 61 | 62 | - name: Install python ${{ matrix.nox_session.python }} for tests 63 | if: ${{ ! contains(fromJson('["3.13"]'), matrix.nox_session.python ) }} 64 | uses: MatteoH2O1999/setup-python@v3.2.1 # actions/setup-python@v5.0.0 65 | id: set-py 66 | with: 67 | python-version: ${{ matrix.nox_session.python }} 68 | architecture: x64 69 | allow-build: info 70 | cache-build: true 71 | 72 | - name: Install python ${{ matrix.nox_session.python }} for tests (3.13) 73 | if: contains(fromJson('["3.13"]'), matrix.nox_session.python ) 74 | uses: actions/setup-python@v5 75 | id: set-py-latest 76 | with: 77 | # Include all versions including pre releases 78 | # See https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#specifying-a-python-version 79 | python-version: ${{ format('~{0}.0-alpha.0', matrix.nox_session.python) }} 80 | architecture: x64 81 | allow-build: info 82 | cache-build: true 83 | 84 | - name: Install python 3.12 for nox 85 | uses: actions/setup-python@v5.0.0 86 | with: 87 | python-version: 3.12 88 | architecture: x64 89 | 90 | - name: pin virtualenv==20.15.1 in old python versions 91 | # pinned to keep compatibility with old versions, see https://github.com/MatteoH2O1999/setup-python/issues/28#issuecomment-1745613621 92 | if: contains(fromJson('["2.7", "3.5", "3.6"]'), matrix.nox_session.python ) 93 | run: sed -i "s/virtualenv/virtualenv==20.15.1/g" noxfile-requirements.txt 94 | 95 | - name: Setup headless display 96 | uses: pyvista/setup-headless-display-action@v2 97 | with: 98 | qt: true 99 | 100 | - name: Install noxfile requirements 101 | run: pip install -r noxfile-requirements.txt 102 | 103 | - name: Run nox session ${{ matrix.nox_session.session }} 104 | run: nox -s "${{ matrix.nox_session.session }}" 105 | 106 | # Share ./docs/reports so that they can be deployed with doc in next job 107 | - name: Share reports with other jobs 108 | if: runner.os == 'Linux' 109 | uses: actions/upload-artifact@master 110 | with: 111 | name: reports_dir 112 | path: ./docs/reports 113 | 114 | # build_doc: useless in our case since own doc is part of the tests session 115 | # runs-on: ubuntu-latest 116 | # if: github.event_name == 'pull_request' 117 | # steps: 118 | # - name: Checkout 119 | # uses: actions/checkout@v4.1.1 120 | # 121 | # - name: Install python 3.9 for nox 122 | # uses: actions/setup-python@v5.0.0 123 | # with: 124 | # python-version: 3.9 125 | # architecture: x64 126 | # 127 | # - name: Install noxfile requirements 128 | # run: pip install -r noxfile-requirements.txt 129 | # 130 | # - name: Build the doc including example gallery 131 | # run: nox -s docs -- build 132 | 133 | publish_release: 134 | needs: run_all_tests 135 | runs-on: ubuntu-latest 136 | if: github.event_name == 'push' 137 | steps: 138 | - name: GitHub context to debug conditional steps 139 | env: 140 | GITHUB_CONTEXT: ${{ toJSON(github) }} 141 | run: echo "$GITHUB_CONTEXT" 142 | 143 | - name: Checkout with no depth 144 | uses: actions/checkout@v4.1.1 145 | with: 146 | fetch-depth: 0 # so that gh-deploy works 147 | 148 | - name: Install python 3.9 for nox 149 | uses: actions/setup-python@v5.0.0 150 | with: 151 | python-version: 3.9 152 | architecture: x64 153 | 154 | - name: Setup headless display 155 | uses: pyvista/setup-headless-display-action@v2 156 | with: 157 | qt: true 158 | 159 | # 1) retrieve the reports generated previously 160 | - name: Retrieve reports 161 | uses: actions/download-artifact@v4.1.1 162 | with: 163 | name: reports_dir 164 | path: ./docs/reports 165 | 166 | # Nox install 167 | - name: Install noxfile requirements 168 | run: pip install -r noxfile-requirements.txt 169 | 170 | # 5) Run the flake8 report and badge 171 | - name: Run flake8 analysis and generate corresponding badge 172 | run: nox -s flake8 173 | 174 | # -------------- only on Ubuntu + MAIN PUSH (no pull request, no tag) ----------- 175 | 176 | # 5) Publish the doc and test reports 177 | - name: \[not on TAG\] Publish documentation, tests and coverage reports 178 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') # startsWith(matrix.os,'ubuntu') 179 | run: nox -s publish 180 | 181 | # 6) Publish coverage report 182 | - name: \[not on TAG\] Create codecov.yaml with correct paths 183 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') 184 | shell: bash 185 | run: | 186 | cat << EOF > codecov.yml 187 | # codecov.yml 188 | fixes: 189 | - "/home/runner/work/smarie/mkdocs-gallery/::" # Correct paths 190 | EOF 191 | - name: \[not on TAG\] Publish coverage report 192 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') 193 | uses: codecov/codecov-action@v4.0.1 194 | with: 195 | files: ./docs/reports/coverage/coverage.xml 196 | 197 | # -------------- only on Ubuntu + TAG PUSH (no pull request) ----------- 198 | 199 | # 7) Create github release and build the wheel 200 | - name: \[TAG only\] Build wheel and create github release 201 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 202 | run: nox -s release -- ${{ secrets.GITHUB_TOKEN }} 203 | 204 | # 8) Publish the wheel on PyPi 205 | - name: \[TAG only\] Deploy on PyPi 206 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 207 | uses: pypa/gh-action-pypi-publish@release/v1 208 | with: 209 | user: __token__ 210 | password: ${{ secrets.PYPI_API_TOKEN }} 211 | 212 | delete-artifacts: 213 | needs: publish_release 214 | runs-on: ubuntu-latest 215 | if: github.event_name == 'push' 216 | steps: 217 | - uses: kolpav/purge-artifacts-action@v1 218 | with: 219 | token: ${{ secrets.GITHUB_TOKEN }} 220 | expire-in: 0 # Setting this to 0 will delete all artifacts 221 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.10.4 - Bugfixes 4 | 5 | - Fixed `DeprecationWarning` with `mkdocs-material` `>=9.4` by using `material.extensions.emoji` instead of 6 | `materialx.emoji.twemoji`. Fixes [#103](https://github.com/smarie/mkdocs-gallery/issues/103). PR 7 | [#104](https://github.com/smarie/mkdocs-gallery/pull/104) by [BalzaniEduardo](https://github.com/BalzaniEdoardo). 8 | 9 | ### 0.10.3 - Bugfixes 10 | 11 | - Don't use `asyncio.run` for async handling. Fixes [#93](https://github.com/smarie/mkdocs-gallery/issues/93). 12 | 13 | ### 0.10.2 - Bugfixes 14 | 15 | - **SECURITY** removed insecure polyfill extra javascript from example. Fixes [#99](https://github.com/smarie/mkdocs-gallery/issues/99). 16 | - Fixed dead link at the bottom of the generated gallery examples. Fixes [#97](https://github.com/smarie/mkdocs-gallery/issues/97). 17 | - Fixed compliance issue with `mkdocs-material`'s [metadata declaration feature](https://squidfunk.github.io/mkdocs-material/reference/#usage). Fixes [#96](https://github.com/smarie/mkdocs-gallery/issues/96). 18 | 19 | ### 0.10.1 - More flexible gallery folders 20 | 21 | - `examples` folder is not required to be in a subfolder of `docs` anymore. Fixes [#54](https://github.com/smarie/mkdocs-gallery/issues/54). PR [#92](https://github.com/smarie/mkdocs-gallery/pull/92) by [Louis-Pujol](https://github.com/Louis-Pujol). 22 | 23 | ### 0.10.0 - Support for asynchronous code 24 | 25 | - Gallery scripts now support top-level asynchronous code. PR [#90](https://github.com/smarie/mkdocs-gallery/pull/90) by [pmeier](https://github.com/pmeier) 26 | 27 | ### 0.9.0 - Pyvista 28 | 29 | - Pyvista can now be used in gallery examples as in `sphinx-gallery`. PR [#91](https://github.com/smarie/mkdocs-gallery/pull/91) by [Louis-Pujol](https://github.com/Louis-Pujol) 30 | 31 | ### 0.8.0 - Mayavi 32 | 33 | - Mayavi can now be used in gallery examples just as in `sphinx-gallery`. PR [#69](https://github.com/smarie/mkdocs-gallery/pull/69) by [GenevieveBuckley](https://github.com/GenevieveBuckley) 34 | - Fixed for `README.md` that contains `html` comments. Fixes [#85](https://github.com/smarie/mkdocs-gallery/issues/85). PR [#86](https://github.com/smarie/mkdocs-gallery/pull/86) by [AntoineD](https://github.com/AntoineD). 35 | 36 | ### 0.7.10 - `sys.path` is not reset between code blocks 37 | 38 | - `sys.path` modifications now persist across blocks of an example. `sys.path` is still reset after each example. PR [#82](https://github.com/smarie/mkdocs-gallery/pull/82) by [Louis-Pujol](https://github.com/Louis-Pujol). 39 | 40 | ### 0.7.9 - `optipng` and better error messages 41 | 42 | - Fixed `AttributeError` when `optipng` is installed and used through the `compress_images` option. PR [#77](https://github.com/smarie/mkdocs-gallery/pull/77) by [Samreay](https://github.com/Samreay) 43 | - Swapped from deprecated `disutils.version` to `packaging.version`. PR [#79](https://github.com/smarie/mkdocs-gallery/pull/79) by [Samreay](https://github.com/Samreay) 44 | - Re-raise errors for better ExtensionError messages, so users have full details about the original problem. PR [#58](https://github.com/smarie/mkdocs-gallery/pull/58) by [GenevieveBuckley](https://github.com/GenevieveBuckley) 45 | 46 | ### 0.7.8 - Bugfixes 47 | 48 | - Fixed `Plugin 'gallery' option 'binder': Sub-option 'org': Required configuration not provided.`. Fixes [#62](https://github.com/smarie/mkdocs-gallery/issues/62) 49 | - Support relative path to `mkdocs.yaml` file using `--config-file` option. Fixes [#63](https://github.com/smarie/mkdocs-gallery/issues/63). PR [#64](https://github.com/smarie/mkdocs-gallery/pull/64) by [fgrbr](https://github.com/fgrbr). 50 | 51 | ### 0.7.7 - Bugfixes and new python versions 52 | 53 | - Official support for python 3.10 and 3.11. PR [#52](https://github.com/smarie/mkdocs-gallery/pull/52) by [GenevieveBuckley](https://github.com/GenevieveBuckley) 54 | - Fixed `AttributeError: MySubConfig has no '_pre_validate'` with `mkdocs` version `1.4` or greater. Fixes [#57](https://github.com/smarie/mkdocs-gallery/issues/57) 55 | 56 | ### 0.7.6 - Bugfixes 57 | 58 | - Fixed incorrect img `srcset` paths leading to figures not being displayed in gallery examples. Fixes [#47](https://github.com/smarie/mkdocs-gallery/issues/47) 59 | - Fixed `TypeError: startswith first arg must be str or a tuple of str, not WindowsPath` when running with `mkdocs serve`. Fixes [#45](https://github.com/smarie/mkdocs-gallery/issues/45). PR [#46](https://github.com/smarie/mkdocs-gallery/pull/46) by [mchaaler](https://github.com/mchaaler). 60 | 61 | ### 0.7.5 - Bugfixes 62 | 63 | - Examples expected to fail are now correctly skipped in case of identical md5 hash, too. Fixes [#34](https://github.com/smarie/mkdocs-gallery/issues/34). PR [#39](https://github.com/smarie/mkdocs-gallery/pull/39) by [mchaaler](https://github.com/mchaaler). 64 | 65 | ### 0.7.4 - Bugfixes 66 | 67 | - Python scripts are now correctly skipped in case of identical md5 hash. Fixes [#29](https://github.com/smarie/mkdocs-gallery/issues/29). PR [#27](https://github.com/smarie/mkdocs-gallery/pull/27) by [mchaaler](https://github.com/mchaaler). 68 | - Fixed error when `edit_url` is set to empty to [disable the "edit page" feature](https://www.mkdocs.org/user-guide/configuration/#edit_uri). Fixes [#32](https://github.com/smarie/mkdocs-gallery/issues/32). PR [#27](https://github.com/smarie/mkdocs-gallery/pull/27) by [mchaaler](https://github.com/mchaaler). 69 | 70 | This release was yanked because of [#34](https://github.com/smarie/mkdocs-gallery/issues/34). 71 | 72 | ### 0.7.3 - Bugfix 73 | 74 | - `matplotlib` was still not optional by default because of the associated `image_scraper` that was called even when not used. It is now truly optional. Fixed [#24](https://github.com/smarie/mkdocs-gallery/issues/24) 75 | 76 | ### 0.7.2 - Misc. bug fixes and improvements 77 | 78 | - Fixed `KeyError` issue when using a minimalistic configuration. Fixed [#22](https://github.com/smarie/mkdocs-gallery/issues/22). 79 | - `matplotlib` is now optional. Fixed [#24](https://github.com/smarie/mkdocs-gallery/issues/24) 80 | 81 | ### 0.7.1 - Packaging is now correct 82 | 83 | - Fixed packaging issue: static resources were not included in wheel. Adopted `src/` layout. Fixed [#19](https://github.com/smarie/mkdocs-gallery/issues/19). 84 | 85 | ### 0.7.0 - Code output max height + updated one example 86 | 87 | - Code output now have a correct height limit. Fixed [#7](https://github.com/smarie/mkdocs-gallery/issues/7) 88 | - Fixed the "Notebook Style Example" tutorial. Fixed [#17](https://github.com/smarie/mkdocs-gallery/issues/17) 89 | 90 | ### 0.6.0 - All examples + Edit page link + Binder badges ! 91 | 92 | - Completed the gallery of examples. Fixed [#1](https://github.com/smarie/mkdocs-gallery/issues/1) 93 | - Fixed HTML repr (example 2, typically used to display pandas tables). Fixed [#11](https://github.com/smarie/mkdocs-gallery/issues/11) 94 | - Binder badges now work correctly. Fixes [#5](https://github.com/smarie/mkdocs-gallery/issues/5) 95 | - "Edit page" links (pencil icon at the top) now work as expected: they take the user to the source python file used to generate the page. It also works for gallery readme pages ! Fixes [#8](https://github.com/smarie/mkdocs-gallery/issues/8) 96 | - backreferences files are now written but it is still not clear how they should be used in a mkdocs context, see [#10](https://github.com/smarie/mkdocs-gallery/issues/10) 97 | - Fixed most flake8 issues and warnings. 98 | 99 | ### 0.5.0 - First public working release 100 | 101 | Initial release with: 102 | 103 | - Basic features: 104 | - pages generation with markdown-based gallery examples. Currently only the `material` theme renders correctly 105 | - download buttons (both on each example and on the summaries) and page header link to the downloads section 106 | - subgalleries 107 | - gallery synthesis with proper icons and subgalleries 108 | - auto inclusion in the ToC (nav) with support for the [section index pages feature](https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#section-index-pages) 109 | - working `mkdocs serve`: correctly ignoring generated files to avoid infinite build loop 110 | - working `mkdocs.yml` configuration for most options 111 | - New option `conf_script` to configure via a script as in Sphinx-gallery. 112 | 113 | - All gallery examples from Sphinx-Gallery successfully translated, in particular: 114 | - LaTeX support works 115 | 116 | - Refactoring: 117 | - Using pathlib all over the place 118 | - Using f-string whenever possible 119 | - Object-oriented approach for configuration and dir/file names used in the generated files. 120 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/binder.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE <sylvain.marie@se.com> 2 | # + All contributors to <https://github.com/smarie/mkdocs-gallery> 3 | # 4 | # Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 5 | # License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 6 | """ 7 | Binder utility functions 8 | """ 9 | import os 10 | import shutil 11 | from pathlib import Path 12 | from typing import Dict 13 | from urllib.parse import quote 14 | 15 | from tqdm import tqdm 16 | 17 | from . import glr_path_static, mkdocs_compatibility 18 | from .errors import ConfigError 19 | from .gen_data_model import GalleryScript 20 | 21 | logger = mkdocs_compatibility.getLogger("mkdocs-gallery") 22 | 23 | 24 | def gen_binder_url(script: GalleryScript, binder_conf): 25 | """Generate a Binder URL according to the configuration in conf.py. 26 | 27 | Parameters 28 | ---------- 29 | script: GalleryScript 30 | The script for which a Binder badge will be generated. 31 | binder_conf: dict or None 32 | The Binder configuration dictionary. See `gen_binder_md` for details. 33 | 34 | Returns 35 | ------- 36 | binder_url : str 37 | A URL that can be used to direct the user to the live Binder environment. 38 | """ 39 | # Build the URL 40 | fpath_prefix = binder_conf.get("filepath_prefix") 41 | link_base = binder_conf.get("notebooks_dir") 42 | 43 | # We want to keep the relative path to sub-folders 44 | path_link = os.path.join(link_base, script.ipynb_file_rel_site_root.as_posix()) 45 | 46 | # In case our website is hosted in a sub-folder 47 | if fpath_prefix is not None: 48 | path_link = "/".join([fpath_prefix.strip("/"), path_link]) 49 | 50 | # Make sure we have the right slashes (in case we're on Windows) 51 | path_link = path_link.replace(os.path.sep, "/") 52 | 53 | # Create the URL 54 | # See https://mybinder.org/ to check that it is still the right one 55 | # Note: the branch will typically be gh-pages 56 | binder_url = "/".join( 57 | [ 58 | binder_conf["binderhub_url"], 59 | "v2", 60 | "gh", 61 | binder_conf["org"], 62 | binder_conf["repo"], 63 | quote(binder_conf["branch"]), 64 | ] 65 | ) 66 | 67 | if binder_conf.get("use_jupyter_lab", False) is True: 68 | binder_url += "?urlpath=lab/tree/{}".format(quote(path_link)) 69 | else: 70 | binder_url += "?filepath={}".format(quote(path_link)) 71 | return binder_url 72 | 73 | 74 | def gen_binder_md(script: GalleryScript, binder_conf: Dict): 75 | """Generate the MD + link for the Binder badge. 76 | 77 | Parameters 78 | ---------- 79 | script: GalleryScript 80 | The script for which a Binder badge will be generated. 81 | 82 | binder_conf: dict or None 83 | If a dictionary it must have the following keys: 84 | 85 | 'binderhub_url' 86 | The URL of the BinderHub instance that's running a Binder service. 87 | 'org' 88 | The GitHub organization to which the documentation will be pushed. 89 | 'repo' 90 | The GitHub repository to which the documentation will be pushed. 91 | 'branch' 92 | The Git branch on which the documentation exists (e.g., gh-pages). 93 | 'dependencies' 94 | A list of paths to dependency files that match the Binderspec. 95 | 96 | Returns 97 | ------- 98 | md : str 99 | The Markdown for the Binder badge that links to this file. 100 | """ 101 | binder_url = gen_binder_url(script, binder_conf) 102 | 103 | # TODO revisit this comment for mkdocs 104 | # In theory we should be able to use glr_path_static for this, but Sphinx only allows paths to be relative to the 105 | # build root. On Linux, absolute paths can be used and they work, but this does not seem to be 106 | # documented behavior: https://github.com/sphinx-doc/sphinx/issues/7772 107 | # And in any case, it does not work on Windows, so here we copy the SVG to `images` for each gallery and link to it 108 | # there. This will make a few copies, and there will be an extra in `_static` at the end of the build, but it at 109 | # least works... 110 | physical_path = script.gallery.images_dir / "binder_badge_logo.svg" 111 | if not physical_path.exists(): 112 | # Make sure parent dirs exists (this should not be necessary actually) 113 | physical_path.parent.mkdir(parents=True, exist_ok=True) 114 | shutil.copyfile(os.path.join(glr_path_static(), "binder_badge_logo.svg"), str(physical_path)) 115 | else: 116 | assert physical_path.is_file() # noqa 117 | 118 | # Create the markdown image with a link 119 | return f"[![Launch binder](./images/binder_badge_logo.svg)]({binder_url}){{ .center}}" 120 | 121 | 122 | def copy_binder_files(gallery_conf, mkdocs_conf): 123 | """Copy all Binder requirements and notebooks files.""" 124 | # if exception is not None: 125 | # return 126 | # 127 | # if app.builder.name not in ['html', 'readthedocs']: 128 | # return 129 | 130 | # gallery_conf = app.config.sphinx_gallery_conf 131 | binder_conf = gallery_conf["binder"] 132 | 133 | if not len(binder_conf) > 0: 134 | return 135 | 136 | logger.info("copying binder requirements...") # , color='white') 137 | _copy_binder_reqs(binder_conf, mkdocs_conf) 138 | _copy_binder_notebooks(gallery_conf, mkdocs_conf) 139 | 140 | 141 | def _copy_binder_reqs(binder_conf, mkdocs_conf): 142 | """Copy Binder requirements files to a ".binder" folder in the docs. 143 | 144 | See https://mybinder.readthedocs.io/en/latest/using/config_files.html#config-files 145 | """ 146 | path_reqs = binder_conf.get("dependencies") 147 | 148 | # Check that they exist (redundant since the check is already done by mkdocs.) 149 | for path in path_reqs: 150 | if not os.path.exists(path): 151 | raise ConfigError(f"Couldn't find the Binder requirements file: {path}, did you specify it correctly?") 152 | 153 | # Destination folder: a ".binder" folder 154 | binder_folder = os.path.join(mkdocs_conf["site_dir"], ".binder") 155 | if not os.path.isdir(binder_folder): 156 | os.makedirs(binder_folder) 157 | 158 | # Copy over the requirement files to the output directory 159 | for path in path_reqs: 160 | shutil.copy(path, binder_folder) 161 | 162 | 163 | def _remove_ipynb_files(path, contents): 164 | """Given a list of files in `contents`, remove all files named `ipynb` or 165 | directories named `images` and return the result. 166 | 167 | Used with the `shutil` "ignore" keyword to filter out non-ipynb files.""" 168 | contents_return = [] 169 | for entry in contents: 170 | if entry.endswith(".ipynb"): 171 | # Don't include ipynb files 172 | pass 173 | elif (entry != "images") and os.path.isdir(os.path.join(path, entry)): 174 | # Don't include folders not called "images" 175 | pass 176 | else: 177 | # Keep everything else 178 | contents_return.append(entry) 179 | return contents_return 180 | 181 | 182 | def _copy_binder_notebooks(gallery_conf, mkdocs_conf): 183 | """Copy Jupyter notebooks to the binder notebooks directory. 184 | 185 | Copy each output gallery directory structure but only including the 186 | Jupyter notebook files.""" 187 | 188 | gallery_dirs = gallery_conf.get("gallery_dirs") 189 | binder_conf = gallery_conf.get("binder") 190 | notebooks_dir = os.path.join(mkdocs_conf["site_dir"], binder_conf.get("notebooks_dir")) 191 | shutil.rmtree(notebooks_dir, ignore_errors=True) 192 | os.makedirs(notebooks_dir) 193 | 194 | if not isinstance(gallery_dirs, (list, tuple)): 195 | gallery_dirs = [gallery_dirs] 196 | 197 | for gallery_dir in tqdm(gallery_dirs, desc=f"copying binder notebooks... "): 198 | gallery_dir_rel_docs_dir = Path(gallery_dir).relative_to(mkdocs_conf["docs_dir"]) 199 | shutil.copytree( 200 | gallery_dir, 201 | os.path.join(notebooks_dir, gallery_dir_rel_docs_dir), 202 | ignore=_remove_ipynb_files, 203 | ) 204 | 205 | 206 | def check_binder_conf(binder_conf): 207 | """Check to make sure that the Binder configuration is correct.""" 208 | 209 | # Grab the configuration and return None if it's not configured 210 | binder_conf = {} if binder_conf is None else binder_conf 211 | if not isinstance(binder_conf, dict): 212 | raise ConfigError("`binder_conf` must be a dictionary or None.") 213 | if len(binder_conf) == 0: 214 | return binder_conf 215 | 216 | # Ensure all fields are populated 217 | req_values = ["binderhub_url", "org", "repo", "branch", "dependencies"] 218 | optional_values = ["filepath_prefix", "notebooks_dir", "use_jupyter_lab"] 219 | missing_values = [] 220 | for val in req_values: 221 | if binder_conf.get(val) is None: 222 | missing_values.append(val) 223 | 224 | if len(missing_values) > 0: 225 | raise ConfigError(f"binder_conf is missing values for: {missing_values}") 226 | 227 | for key in binder_conf.keys(): 228 | if key not in (req_values + optional_values): 229 | raise ConfigError(f"Unknown Binder config key: {key}") 230 | 231 | # Ensure we have http in the URL 232 | if not any(binder_conf["binderhub_url"].startswith(ii) for ii in ["http://", "https://"]): 233 | raise ConfigError(f"did not supply a valid url, gave binderhub_url: {binder_conf['binderhub_url']}") 234 | 235 | # Ensure we have at least one dependency file 236 | # Need at least one of these three files 237 | required_reqs_files = ["requirements.txt", "environment.yml", "Dockerfile"] 238 | 239 | path_reqs = binder_conf["dependencies"] 240 | if isinstance(path_reqs, str): 241 | path_reqs = [path_reqs] 242 | binder_conf["dependencies"] = path_reqs 243 | elif not isinstance(path_reqs, (list, tuple)): 244 | raise ConfigError(f"`dependencies` value should be a list of strings. Got type {type(path_reqs)}.") 245 | 246 | binder_conf["notebooks_dir"] = binder_conf.get("notebooks_dir", "notebooks") 247 | 248 | path_reqs_filenames = [os.path.basename(ii) for ii in path_reqs] 249 | if not any(ii in path_reqs_filenames for ii in required_reqs_files): 250 | raise ConfigError( 251 | 'Did not find one of `requirements.txt` or `environment.yml` in the "dependencies" section' 252 | " of the binder configuration for mkdocs-gallery. A path to at least one of these files must" 253 | " exist in your Binder dependencies." 254 | ) 255 | 256 | return binder_conf 257 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/notebook.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE <sylvain.marie@se.com> 2 | # + All contributors to <https://github.com/smarie/mkdocs-gallery> 3 | # 4 | # Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 5 | # License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 6 | """ 7 | Parser for Jupyter notebooks 8 | """ 9 | 10 | from __future__ import absolute_import, division, print_function 11 | 12 | import argparse 13 | import base64 14 | import copy 15 | import json 16 | import mimetypes 17 | import os 18 | import re 19 | import sys 20 | from functools import partial 21 | from pathlib import Path 22 | from typing import Dict, List 23 | 24 | from . import mkdocs_compatibility 25 | from .errors import ExtensionError 26 | from .gen_data_model import GalleryScript 27 | from .py_source_parser import split_code_and_text_blocks 28 | 29 | logger = mkdocs_compatibility.getLogger("mkdocs-gallery") 30 | 31 | 32 | def jupyter_notebook_skeleton(): 33 | """Returns a dictionary with the elements of a Jupyter notebook""" 34 | py_version = sys.version_info 35 | notebook_skeleton = { 36 | "cells": [], 37 | "metadata": { 38 | "kernelspec": { 39 | "display_name": "Python " + str(py_version[0]), 40 | "language": "python", 41 | "name": "python" + str(py_version[0]), 42 | }, 43 | "language_info": { 44 | "codemirror_mode": {"name": "ipython", "version": py_version[0]}, 45 | "file_extension": ".py", 46 | "mimetype": "text/x-python", 47 | "name": "python", 48 | "nbconvert_exporter": "python", 49 | "pygments_lexer": "ipython" + str(py_version[0]), 50 | "version": "{0}.{1}.{2}".format(*sys.version_info[:3]), 51 | }, 52 | }, 53 | "nbformat": 4, 54 | "nbformat_minor": 0, 55 | } 56 | return notebook_skeleton 57 | 58 | 59 | def directive_fun(match, directive): 60 | """Helper to fill in directives""" 61 | directive_to_alert = dict(note="info", warning="danger") 62 | return '<div class="alert alert-{0}"><h4>{1}</h4><p>{2}</p></div>'.format( 63 | directive_to_alert[directive], directive.capitalize(), match.group(1).strip() 64 | ) 65 | 66 | 67 | def rst2md(text, gallery_conf, target_dir, heading_levels): 68 | """Converts the RST text from the examples docstrings and comments 69 | into markdown text for the Jupyter notebooks 70 | 71 | Parameters 72 | ---------- 73 | text: str 74 | RST input to be converted to MD 75 | gallery_conf : dict 76 | The mkdocs-gallery configuration dictionary. 77 | target_dir : str 78 | Path that notebook is intended for. Used where relative paths 79 | may be required. 80 | heading_levels: dict 81 | Mapping of heading style ``(over_char, under_char)`` to heading level. 82 | Note that ``over_char`` is `None` when only underline is present. 83 | """ 84 | 85 | # Characters recommended for use with headings 86 | # https://docutils.readthedocs.io/en/sphinx-docs/user/rst/quickstart.html#sections 87 | adornment_characters = "=`:.'\"~^_*+#<>-" 88 | headings = re.compile( 89 | # Start of string or blank line 90 | r"(?P<pre>\A|^[ \t]*\n)" 91 | # Optional over characters, allowing leading space on heading text 92 | r"(?:(?P<over>[{0}])(?P=over)*\n[ \t]*)?" 93 | # The heading itself, with at least one non-white space character 94 | r"(?P<heading>\S[^\n]*)\n" 95 | # Under character, setting to same character if over present. 96 | r"(?P<under>(?(over)(?P=over)|[{0}]))(?P=under)*$" r"".format(adornment_characters), 97 | flags=re.M, 98 | ) 99 | 100 | text = re.sub( 101 | headings, 102 | lambda match: "{1}{0} {2}".format( 103 | "#" * heading_levels[match.group("over", "under")], *match.group("pre", "heading") 104 | ), 105 | text, 106 | ) 107 | 108 | math_eq = re.compile(r"^\.\. math::((?:.+)?(?:\n+^ .+)*)", flags=re.M) 109 | text = re.sub( 110 | math_eq, 111 | lambda match: r"\begin{{align}}{0}\end{{align}}".format(match.group(1).strip()), 112 | text, 113 | ) 114 | inline_math = re.compile(r":math:`(.+?)`", re.DOTALL) 115 | text = re.sub(inline_math, r"$\1$", text) 116 | 117 | directives = ("warning", "note") 118 | for directive in directives: 119 | directive_re = re.compile(r"^\.\. %s::((?:.+)?(?:\n+^ .+)*)" % directive, flags=re.M) 120 | text = re.sub(directive_re, partial(directive_fun, directive=directive), text) 121 | 122 | links = re.compile(r"^ *\.\. _.*:.*$\n", flags=re.M) 123 | text = re.sub(links, "", text) 124 | 125 | refs = re.compile(r":ref:`") 126 | text = re.sub(refs, "`", text) 127 | 128 | contents = re.compile(r"^\s*\.\. contents::.*$(\n +:\S+: *$)*\n", flags=re.M) 129 | text = re.sub(contents, "", text) 130 | 131 | images = re.compile(r"^\.\. image::(.*$)((?:\n +:\S+:.*$)*)\n", flags=re.M) 132 | image_opts = re.compile(r"\n +:(\S+): +(.*)$", flags=re.M) 133 | text = re.sub( 134 | images, 135 | lambda match: '<img src="{}"{}>\n'.format( 136 | generate_image_src(match.group(1).strip(), gallery_conf, target_dir), 137 | re.sub(image_opts, r' \1="\2"', match.group(2) or ""), 138 | ), 139 | text, 140 | ) 141 | 142 | return text 143 | 144 | 145 | def generate_image_src(image_path, gallery_conf, target_dir): 146 | if re.match(r"https?://", image_path): 147 | return image_path 148 | 149 | if not gallery_conf["notebook_images"]: 150 | return "file://" + image_path.lstrip("/") 151 | 152 | # If absolute path from source directory given 153 | if image_path.startswith("/"): 154 | # Path should now be relative to source dir, not target dir 155 | target_dir = mkdocs_conf["docs_dir"] 156 | image_path = image_path.lstrip("/") 157 | full_path = os.path.join(target_dir, image_path.replace("/", os.sep)) 158 | 159 | if isinstance(gallery_conf["notebook_images"], str): 160 | # Use as prefix e.g. URL 161 | prefix = gallery_conf["notebook_images"] 162 | rel_path = os.path.relpath(full_path, mkdocs_conf["docs_dir"]) 163 | return prefix + rel_path.replace(os.sep, "/") 164 | else: 165 | # True, but not string. Embed as data URI. 166 | try: 167 | with open(full_path, "rb") as image_file: 168 | data = base64.b64encode(image_file.read()) 169 | except OSError: 170 | raise ExtensionError("Unable to open {} to generate notebook data URI" "".format(full_path)) 171 | mime_type = mimetypes.guess_type(full_path) 172 | return "data:{};base64,{}".format(mime_type[0], data.decode("ascii")) 173 | 174 | 175 | def jupyter_notebook(script: GalleryScript, script_blocks: List): 176 | """Generate a Jupyter notebook file cell-by-cell 177 | 178 | Parameters 179 | ---------- 180 | script : GalleryScript 181 | Script 182 | 183 | script_blocks : list 184 | Script execution cells. 185 | """ 186 | # Grab the possibly custom first and last cells 187 | first_cell = script.gallery_conf["first_notebook_cell"] 188 | last_cell = script.gallery_conf["last_notebook_cell"] 189 | 190 | # Initialize with a notebook skeleton 191 | work_notebook = jupyter_notebook_skeleton() 192 | 193 | # Custom first cell 194 | if first_cell is not None: 195 | add_code_cell(work_notebook, first_cell) 196 | 197 | # Fill the notebook per se 198 | fill_notebook(work_notebook, script_blocks) 199 | 200 | # Custom last cell 201 | if last_cell is not None: 202 | add_code_cell(work_notebook, last_cell) 203 | 204 | return work_notebook 205 | 206 | 207 | def add_code_cell(work_notebook, code): 208 | """Add a code cell to the notebook 209 | 210 | Parameters 211 | ---------- 212 | code : str 213 | Cell content 214 | """ 215 | 216 | code_cell = { 217 | "cell_type": "code", 218 | "execution_count": None, 219 | "metadata": {"collapsed": False}, 220 | "outputs": [], 221 | "source": [code.strip()], 222 | } 223 | work_notebook["cells"].append(code_cell) 224 | 225 | 226 | def add_markdown_cell(work_notebook, markdown): 227 | """Add a markdown cell to the notebook 228 | 229 | Parameters 230 | ---------- 231 | markdown : str 232 | Markdown cell content. 233 | """ 234 | markdown_cell = {"cell_type": "markdown", "metadata": {}, "source": [markdown]} 235 | work_notebook["cells"].append(markdown_cell) 236 | 237 | 238 | def fill_notebook(work_notebook, script_blocks): 239 | """Writes the Jupyter notebook cells 240 | 241 | If available, uses pypandoc to convert rst to markdown >> not anymore. 242 | 243 | Parameters 244 | ---------- 245 | script_blocks : list 246 | Each list element should be a tuple of (label, content, lineno). 247 | """ 248 | # heading_level_counter = count(start=1) 249 | # heading_levels = defaultdict(lambda: next(heading_level_counter)) 250 | for blabel, bcontent, _lineno in script_blocks: 251 | if blabel == "code": 252 | add_code_cell(work_notebook, bcontent) 253 | else: 254 | # if gallery_conf["pypandoc"] is False: 255 | # markdown = rst2md( 256 | # bcontent + '\n', gallery_conf, target_dir, heading_levels) 257 | # else: 258 | # import pypandoc 259 | # # pandoc automatically addds \n to the end 260 | # markdown = pypandoc.convert_text( 261 | # bcontent, to='md', format='rst', **gallery_conf["pypandoc"] 262 | # ) 263 | markdown = bcontent + "\n" 264 | add_markdown_cell(work_notebook, markdown) 265 | 266 | 267 | def save_notebook(work_notebook: Dict, write_file: Path): 268 | """Saves the Jupyter work_notebook to write_file""" 269 | with open(str(write_file), "w") as out_nb: 270 | json.dump(work_notebook, out_nb, indent=2) 271 | 272 | 273 | ############################################################################### 274 | # Notebook shell utility 275 | 276 | 277 | def python_to_jupyter_cli(args=None, namespace=None): 278 | """Exposes the jupyter notebook renderer to the command line 279 | 280 | Takes the same arguments as ArgumentParser.parse_args 281 | """ 282 | from . import gen_gallery # To avoid circular import 283 | 284 | parser = argparse.ArgumentParser(description="mkdocs-gallery Notebook converter") 285 | parser.add_argument( 286 | "python_src_file", 287 | nargs="+", 288 | help="Input Python file script to convert. " "Supports multiple files and shell wildcards" " (e.g. *.py)", 289 | ) 290 | args = parser.parse_args(args, namespace) 291 | 292 | for src_file in args.python_src_file: 293 | file_conf, blocks = split_code_and_text_blocks(src_file) 294 | print("Converting {0}".format(src_file)) # noqa # this is a cli 295 | gallery_conf = copy.deepcopy(gen_gallery.DEFAULT_GALLERY_CONF) 296 | target_dir = os.path.dirname(src_file) 297 | example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) 298 | save_notebook(example_nb, get_ipynb_for_py_script(src_file)) 299 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # mkdocs-gallery 2 | 3 | *[Sphinx-Gallery](https://sphinx-gallery.github.io/) features for [mkdocs](https://www.mkdocs.org/) (no [Sphinx](sphinx-doc.org/) dependency !).* 4 | 5 | [![Python versions](https://img.shields.io/pypi/pyversions/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Build Status](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/mkdocs-gallery/actions/workflows/base.yml) [![Tests Status](./reports/junit/junit-badge.svg?dummy=8484744)](./reports/junit/report.html) [![Coverage Status](./reports/coverage/coverage-badge.svg?dummy=8484744)](./reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/mkdocs-gallery/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/mkdocs-gallery) [![Flake8 Status](./reports/flake8/flake8-badge.svg?dummy=8484744)](./reports/flake8/index.html) 6 | 7 | [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/mkdocs-gallery/) [![PyPI](https://img.shields.io/pypi/v/mkdocs-gallery.svg)](https://pypi.python.org/pypi/mkdocs-gallery/) [![Downloads](https://pepy.tech/badge/mkdocs-gallery)](https://pepy.tech/project/mkdocs-gallery) [![Downloads per week](https://pepy.tech/badge/mkdocs-gallery/week)](https://pepy.tech/project/mkdocs-gallery) [![GitHub stars](https://img.shields.io/github/stars/smarie/mkdocs-gallery.svg)](https://github.com/smarie/mkdocs-gallery/stargazers) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5786851.svg)](https://doi.org/10.5281/zenodo.5786851) 8 | 9 | Do you love [Sphinx-Gallery](https://sphinx-gallery.github.io/) but prefer [mkdocs](https://www.mkdocs.org/) over [Sphinx](sphinx-doc.org/) for your documentation ? `mkdocs-gallery` was written for you ;) 10 | 11 | It relies on [mkdocs-material](https://squidfunk.github.io/mkdocs-material) to get the most of mkdocs, so that your galleries look nice ! 12 | 13 | 14 | ## Installing 15 | 16 | ```bash 17 | > pip install mkdocs-gallery 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### 1. Create a source gallery folder 23 | 24 | First, create a folder that will contain your gallery examples, for example `docs/examples/`. It will be referenced by the `examples_dirs` [configuration option](#2-configure-mkdocs). 25 | 26 | Then in this folder, you may add a readme. This readme should be written in markdown, be named `README` or `readme` or `Readme`, and have a `.md` or `.txt` extension. 27 | 28 | Note: the folder can be located inside the usual mkdocs source folder: 29 | 30 | ``` 31 | docs/ # base mkdocs source directory 32 | └── examples/ # base 'Gallery of Examples' directory 33 | └── README.md 34 | ``` 35 | 36 | or not 37 | 38 | ``` 39 | examples/ # base 'Gallery of Examples' directory 40 | └── README.md 41 | docs/ # base mkdocs source directory 42 | ``` 43 | 44 | ### 2. Configure mkdocs 45 | 46 | #### a. Basics 47 | 48 | Simply add the following configuration to you `mkdocs.yml`: 49 | 50 | ```yaml 51 | theme: material # This theme is mandatory for now, see below 52 | 53 | plugins: 54 | - gallery: 55 | examples_dirs: docs/examples # path to your example scripts 56 | gallery_dirs: docs/generated/gallery # where to save generated gallery 57 | # ... (other options) 58 | 59 | - search # make sure the search plugin is still enabled 60 | ``` 61 | 62 | Most [sphinx-gallery configuration options](https://sphinx-gallery.github.io/stable/configuration.html) are supported and can be configured in here after `examples_dirs` and `gallery_dirs`. All paths should be relative to the `mkdocs.yml` file (which is supposed to be located at project root). 63 | 64 | For some general rules: 65 | 66 | 1. The default matching filename pattern is `plot_`, so to have your files run, ensure the filenames are prefixed with `plot_`. 67 | 2. `__init__.py` files are ignored. You can change what's ignored by setting the `ignore_pattern` as per the [sphinx-gallery configuration options](https://sphinx-gallery.github.io/stable/configuration.html) 68 | 69 | You can look at the configuration used to generate this site as an example: [mkdocs.yml](https://github.com/smarie/mkdocs-gallery/blob/main/mkdocs.yml). 70 | 71 | !!! caution 72 | `mkdocs-gallery` currently requires that you use the `material` theme from `mkdocs-material` to render properly. You may wish to try other themes to see what is missing to support them: actually, only a few things concerning buttons and icons do not seem to currently work properly. 73 | 74 | !!! note 75 | The `search` plugin is not related with mkdocs-gallery. It is activated by default in mkdocs but if you edit the `plugins` configuration you have to add it explicitly again. 76 | 77 | Once you've done this, the corresponding gallery will be created the next time you call `mkdocs build` or `mkdocs serve`. However the gallery will not yet appear in the table of contents (mkdocs `nav`). For this you should add the generated gallery to the nav in `mkdocs.yml`: 78 | 79 | ```yaml 80 | nav: 81 | - Home: index.md 82 | - generated/gallery # This node will automatically be named and have sub-nodes. 83 | ``` 84 | 85 | When the root folder or the root `index.md` of a gallery is added to the nav, it will be automatically populated with sub-nodes for all examples and subgalleries. If you prefer to select examples or subgalleries to include one by one, you may refer to any of them directly in the nav. In that case, no nav automation will be performed - just the usual explicit mkdocs nav. 86 | 87 | You may wish to change the gallery's names for display and still benefit from this automation: 88 | 89 | ```yaml 90 | nav: 91 | - Home: index.md 92 | - My Gallery: generated/gallery # This node will automatically be named and have sub-nodes. 93 | ``` 94 | 95 | See [this site's config](https://github.com/smarie/mkdocs-gallery/blob/main/mkdocs.yml) for an example. See also [mkdocs configuration](https://www.mkdocs.org/user-guide/configuration/) for general information about the `mkdocs.yml` file. 96 | 97 | #### b. Advanced 98 | 99 | You may wish to use the special `conf_script` option to create the base configuration using a python script, like what was done in Sphinx-gallery: 100 | 101 | ```yaml 102 | plugins: 103 | - gallery: 104 | conf_script: docs/gallery_conf.py 105 | # ... other options can still be added here 106 | ``` 107 | 108 | The python script should be executable without error, and at the end of execution should contain a `conf` variable defined at the module level. For example this is a valid script: 109 | 110 | ```python 111 | from mkdocs_gallery.gen_gallery import DefaultResetArgv 112 | 113 | conf = { 114 | 'reset_argv': DefaultResetArgv(), 115 | } 116 | ``` 117 | 118 | You can set options both in the script and in the yaml. In case of duplicates, the yaml options override the script-defined ones. 119 | 120 | ### 3. Add gallery examples 121 | 122 | Gallery examples are structured [the same way as in sphinx-gallery](https://sphinx-gallery.github.io/stable/syntax.html), with two major differences: 123 | 124 | - All comment blocks should be written using **Markdown** instead of rST. 125 | - No sphinx directive is supported: all markdown directives should be supported by `mkdocs`, by one of its activated [plugins](https://www.mkdocs.org/dev-guide/plugins/) or by a base markdown extension (see note below). 126 | - All per-file and per-code block configuration options from sphinx-gallery ([here, bottom](https://sphinx-gallery.github.io/stable/configuration.html?highlight=sphinx_gallery_#list-of-config-options)) are supported, but you have to use the `mkdocs_gallery_[option]` prefix instead of `sphinx_gallery_[options]`. 127 | 128 | ``` 129 | examples/ # base 'Gallery of Examples' directory 130 | ├── README.md 131 | ├── <.py files> 132 | └── subgallery_1/ # generates the 'No image output examples' sub-gallery 133 | ├── README.md 134 | └── <.py files> 135 | ``` 136 | 137 | ### 4. Examples 138 | 139 | The entire original [gallery of examples from sphinx-gallery](https://sphinx-gallery.github.io/stable/auto_examples/index.html) is being ported [here](./generated/gallery/) (work in progress). You may wish to check it out in order to see how each technical aspect translates in the mkdocs world. 140 | 141 | You can look at the configuration used to generate it here: [mkdocs.yml](https://github.com/smarie/mkdocs-gallery/blob/main/mkdocs.yml). 142 | 143 | ### 5. Feature Highlights 144 | 145 | #### a. Mkdocs "serve" mode 146 | 147 | `mkdocs-gallery` supports the mkdocs dev-server `mkdocs serve` so that you can edit your gallery examples with live auto-rebuild (similar to sphinx-autobuild). 148 | 149 | As soon as you modify an example file, it will rebuild the documentation and notify your browser. The examples that did not change will be automatically skipped (based on md5, identical to sphinx-gallery). 150 | 151 | See [mkdocs documentation](https://www.mkdocs.org/getting-started/) for details. 152 | 153 | #### b. Editing Examples 154 | 155 | All mkdocs-gallery generated pages have a working "edit page" pencil icon at the top, including gallery summary (readme) pages. This link will take you directly to the source file for easy pull requests on gallery examples ! 156 | 157 | #### c. Binder 158 | 159 | Binder configuration is slightly easier than the one in sphinx-gallery (as of version 1.0.1), as 2 pieces of config are now optional: 160 | 161 | - `branch` (defaults to `"gh-pages"`) 162 | - `binderhub_url` (defaults to `"https://mybinder.org"`) 163 | 164 | ### 6. Make your examples shine ! 165 | 166 | The following `mkdocs` plugins and extensions are automatically activated - you may therefore use them in your markdown blocks without changing your `mkdocs.yml` configuration: 167 | 168 | - [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material) mkdocs plugin: **make sure you check this one out !** 169 | - [`navigation.indexes`](https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#section-index-pages) in the `material` theme. This is used for the gallery readme pages to be selectible in the nav without creating an extra entry (see left pane). 170 | - [icons + emojis](https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/) :thumbsup: :slight_smile: 171 | - All no-conf features are there too, for example support for $\LaTeX$ using [Mathjax](https://squidfunk.github.io/mkdocs-material/reference/mathjax/), [code blocks](https://squidfunk.github.io/mkdocs-material/reference/code-blocks/), [Admonitions](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) etc. 172 | 173 | - markdown extensions: 174 | - [`attr_list`](https://python-markdown.github.io/extensions/attr_list/) to declare attributes such as css classes on markdown elements. 175 | - [`admonition`](https://python-markdown.github.io/extensions/admonition/) used to add notes. 176 | - [`pymdownx.details`](https://facelessuser.github.io/pymdown-extensions/extensions/details/) to create foldable notes such as this one. 177 | - [`pymdownx.highlight`](https://facelessuser.github.io/pymdown-extensions/extensions/highlight/) 178 | - [`pymdownx.inlinehilite`](https://facelessuser.github.io/pymdown-extensions/extensions/inlinehilite/) 179 | - [`pymdownx.superfences`](https://facelessuser.github.io/pymdown-extensions/extensions/superfences/) 180 | - [`pymdownx.snippets`](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/) (Warning: the base path is 181 | a bit counter-intuitive: it is relative to `cwd`, not to the markdown file ; see the last line of this [tutorial](./generated/tutorials/plot_parse.md)) 182 | - [`pymdownx.emoji`](https://facelessuser.github.io/pymdown-extensions/extensions/emoji/) configured with the catalog from mkdocs-material (see above) 183 | 184 | 185 | ## Citing 186 | 187 | If `mkdocs-gallery` helps you with your research work, don't hesitate to spread the word ! For this simply use this Zenodo link [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5786851.svg)](https://doi.org/10.5281/zenodo.5786851) to get the proper citation entry (at the bottom right of the page, many formats available including BibTeX). 188 | 189 | Note: do not hesitate to cite sphinx-gallery too ! [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3741780.svg)](https://doi.org/10.5281/zenodo.3741780) 190 | 191 | ## See Also 192 | 193 | - [`sphinx-gallery`](https://sphinx-gallery.github.io/) 194 | - [`mkdocs`](https://www.mkdocs.org/) 195 | - [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material) 196 | - [`PyMdown Extensions`](https://facelessuser.github.io/pymdown-extensions/) 197 | 198 | ### Others 199 | 200 | *Do you like this library ? You might also like [smarie's other python libraries](https://github.com/smarie/OVERVIEW#python)* 201 | 202 | ## Want to contribute ? 203 | 204 | Details on the github page: [https://github.com/smarie/mkdocs-gallery](https://github.com/smarie/mkdocs-gallery) 205 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/utils.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE <sylvain.marie@se.com> 2 | # + All contributors to <https://github.com/smarie/mkdocs-gallery> 3 | # 4 | # Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 5 | # License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 6 | """ 7 | Utilities 8 | ========= 9 | 10 | Miscellaneous utilities. 11 | """ 12 | 13 | from __future__ import absolute_import, division, print_function 14 | 15 | import asyncio 16 | import hashlib 17 | import os 18 | import re 19 | import subprocess 20 | from pathlib import Path 21 | from shutil import copyfile, move 22 | from typing import Tuple 23 | 24 | from . import mkdocs_compatibility 25 | from .errors import ExtensionError 26 | 27 | logger = mkdocs_compatibility.getLogger("mkdocs-gallery") 28 | 29 | 30 | def _get_image(): 31 | try: 32 | from PIL import Image 33 | except ImportError as exc: # capture the error for the modern way 34 | try: 35 | import Image 36 | except ImportError: 37 | raise ExtensionError( 38 | "Could not import pillow, which is required " "to rescale images (e.g., for thumbnails): %s" % (exc,) 39 | ) 40 | return Image 41 | 42 | 43 | def rescale_image(in_file: Path, out_file: Path, max_width, max_height): 44 | """Scales an image with the same aspect ratio centered in an 45 | image box with the given max_width and max_height 46 | if in_file == out_file the image can only be scaled down 47 | """ 48 | # local import to avoid testing dependency on PIL: 49 | Image = _get_image() 50 | img = Image.open(in_file) 51 | # XXX someday we should just try img.thumbnail((max_width, max_height)) ... 52 | width_in, height_in = img.size 53 | scale_w = max_width / float(width_in) 54 | scale_h = max_height / float(height_in) 55 | 56 | if height_in * scale_w <= max_height: 57 | scale = scale_w 58 | else: 59 | scale = scale_h 60 | 61 | if scale >= 1.0 and in_file.absolute().as_posix() == out_file.absolute().as_posix(): 62 | # do not proceed: the image can only be scaled down. 63 | return 64 | 65 | width_sc = int(round(scale * width_in)) 66 | height_sc = int(round(scale * height_in)) 67 | 68 | # resize the image using resize; if using .thumbnail and the image is 69 | # already smaller than max_width, max_height, then this won't scale up 70 | # at all (maybe could be an option someday...) 71 | img = img.resize((width_sc, height_sc), Image.BICUBIC) 72 | # img.thumbnail((width_sc, height_sc), Image.BICUBIC) 73 | # width_sc, height_sc = img.size # necessary if using thumbnail 74 | 75 | # insert centered 76 | thumb = Image.new("RGBA", (max_width, max_height), (255, 255, 255, 0)) 77 | pos_insert = ((max_width - width_sc) // 2, (max_height - height_sc) // 2) 78 | thumb.paste(img, pos_insert) 79 | 80 | try: 81 | thumb.save(out_file) 82 | except IOError: 83 | # try again, without the alpha channel (e.g., for JPEG) 84 | thumb.convert("RGB").save(out_file) 85 | 86 | 87 | def optipng(file: Path, args=()): 88 | """Optimize a PNG in place. 89 | 90 | Parameters 91 | ---------- 92 | file : Path 93 | The file. If it ends with '.png', ``optipng -o7 fname`` will 94 | be run. If it fails because the ``optipng`` executable is not found 95 | or optipng fails, the function returns. 96 | args : tuple 97 | Extra command-line arguments, such as ``['-o7']``. 98 | """ 99 | if file.suffix == ".png": 100 | # -o7 because this is what CPython used 101 | # https://github.com/python/cpython/pull/8032 102 | fname = file.as_posix() 103 | try: 104 | subprocess.check_call( 105 | ["optipng"] + list(args) + [fname], 106 | stdout=subprocess.PIPE, 107 | stderr=subprocess.PIPE, 108 | ) 109 | except (subprocess.CalledProcessError, IOError): # FileNotFoundError 110 | pass 111 | else: 112 | raise ValueError(f"File extension is not .png: {file}") 113 | 114 | 115 | def _has_optipng(): 116 | try: 117 | subprocess.check_call(["optipng", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 118 | except IOError: # FileNotFoundError 119 | return False 120 | else: 121 | return True 122 | 123 | 124 | def replace_ext(file: Path, new_ext: str, expected_ext: str = None) -> Path: 125 | """Replace the extension in `file` with `new_ext`, with optional initial `expected_ext` check. 126 | 127 | Parameters 128 | ---------- 129 | file : Path 130 | the file path. 131 | 132 | new_ext : str 133 | The new extension, e.g. '.ipynb' 134 | 135 | expected_ext : str 136 | The expected original extension for checking, if provided. 137 | 138 | Returns 139 | ------- 140 | new_file : Path 141 | The same file with a different ext. 142 | """ 143 | # Optional extension checking 144 | if expected_ext is not None and file.suffix != expected_ext: 145 | raise ValueError(f"Unrecognized file extension, expected {expected_ext}, got {file.suffix}") 146 | 147 | # Replace extension 148 | return file.with_suffix(new_ext) 149 | 150 | 151 | def get_md5sum(src_file: Path, mode="b"): 152 | """Returns md5sum of file 153 | 154 | Parameters 155 | ---------- 156 | src_file : str 157 | Filename to get md5sum for. 158 | mode : 't' or 'b' 159 | File mode to open file with. When in text mode, universal line endings 160 | are used to ensure consitency in hashes between platforms. 161 | """ 162 | errors = "surrogateescape" if mode == "t" else None 163 | with open(str(src_file), "r" + mode, errors=errors) as src_data: 164 | src_content = src_data.read() 165 | if mode == "t": 166 | src_content = src_content.encode(errors=errors) 167 | return hashlib.md5(src_content).hexdigest() 168 | 169 | 170 | def _get_old_file(new_file: Path) -> Path: 171 | """Return the same file without the .new suffix""" 172 | assert new_file.name.endswith(".new") # noqa 173 | return new_file.with_name(new_file.stem) # this removes the .new suffix 174 | 175 | 176 | def _have_same_md5(file_a, file_b, mode: str = "b") -> bool: 177 | """Return `True` if both files have the same md5, computed using `mode`.""" 178 | return get_md5sum(file_a, mode) == get_md5sum(file_b, mode) 179 | 180 | 181 | def _smart_move_md5(src_file: Path, dst_file: Path, md5_mode: str = "b"): 182 | """Move `src_file` to `dst_file`, overwriting `dst_file` only if md5 has changed. 183 | 184 | Parameters 185 | ---------- 186 | src_file : Path 187 | The source file path. 188 | 189 | dst_file : Path 190 | The destination file path. 191 | 192 | md5_mode : str 193 | A string representing the md5 computation mode, 'b' or 't' 194 | """ 195 | assert src_file.is_absolute() and dst_file.is_absolute() # noqa 196 | assert src_file != dst_file # noqa 197 | 198 | if dst_file.exists() and _have_same_md5(dst_file, src_file, mode=md5_mode): 199 | # Shortcut: destination is already identical, just delete the source 200 | os.remove(src_file) 201 | else: 202 | # Proceed to the move operation 203 | move(str(src_file), dst_file) 204 | assert dst_file.exists() # noqa 205 | 206 | return dst_file 207 | 208 | 209 | def _new_file(file: Path) -> Path: 210 | """Return the same file path with a .new additional extension.""" 211 | return file.with_suffix(f"{file.suffix}.new") 212 | 213 | 214 | def _replace_by_new_if_needed(file_new: Path, md5_mode: str = "b"): 215 | """Use `file_new` (suffix .new) instead of the old file (same path but no suffix). 216 | 217 | If the new file is identical to the old one, the old one will not be touched. 218 | 219 | Parameters 220 | ---------- 221 | file_new : Path 222 | The new file, ending with .new suffix. 223 | 224 | md5_mode : str 225 | A string representing the md5 computation mode, 'b' or 't' 226 | """ 227 | _smart_move_md5(src_file=file_new, dst_file=_get_old_file(file_new), md5_mode=md5_mode) 228 | 229 | 230 | def _smart_copy_md5(src_file: Path, dst_file: Path, src_md5: str = None, md5_mode: str = "b") -> Tuple[Path, str]: 231 | """Copy `src_file` to `dst_file`, overwriting `dst_file`, only if md5 has changed. 232 | 233 | Parameters 234 | ---------- 235 | src_file : Path 236 | The source file path. 237 | 238 | dst_file : Path 239 | The destination file path. 240 | 241 | src_md5 : str 242 | If the source md5 was already computed, users may provide it here to avoid computing it again. 243 | 244 | md5_mode : str 245 | A string representing the md5 computation mode, 'b' or 't' 246 | 247 | Returns 248 | ------- 249 | md5 : str 250 | The md5 of the file, if it has been provided or computed in the process, or None. 251 | """ 252 | assert src_file.is_absolute() and dst_file.is_absolute() # noqa 253 | assert src_file != dst_file # noqa 254 | 255 | if dst_file.exists(): 256 | if src_md5 is None: 257 | src_md5 = get_md5sum(src_file, mode=md5_mode) 258 | 259 | dst_md5 = get_md5sum(dst_file, mode=md5_mode) 260 | if src_md5 == dst_md5: 261 | # Shortcut: nothing to do 262 | return src_md5 263 | 264 | # Proceed to the copy operation 265 | copyfile(src_file, dst_file) 266 | assert dst_file.exists() # noqa 267 | 268 | return src_md5 269 | 270 | 271 | # def check_md5sum_changed(src_file: Path, src_md5: str = None, md5_mode='b') -> Tuple[bool, str]: 272 | # """Checks whether src_file has the same md5 hash as the one on disk on not 273 | # 274 | # Legacy name: md5sum_is_current 275 | # 276 | # Parameters 277 | # ---------- 278 | # src_file : Path 279 | # The file to check 280 | # 281 | # md5_mode : str 282 | # The md5 computation mode 283 | # 284 | # Returns 285 | # ------- 286 | # md5_has_changed : bool 287 | # A boolean indicating if src_file has changed with respect 288 | # 289 | # actual_md5 : str 290 | # The actual md5 of src_file 291 | # """ 292 | # 293 | # # Compute the md5 of the src_file 294 | # actual_md5 = get_md5sum(src_file, mode=mode) 295 | # 296 | # # Grab the already computed md5 if it exists, and compare 297 | # src_md5_file = src_file.with_name(src_file.name + '.md5') 298 | # if src_md5_file.exists(): 299 | # ref_md5 = src_md5_file.read_text() 300 | # md5_has_changed = (actual_md5 != ref_md5) 301 | # else: 302 | # md5_has_changed = True 303 | # 304 | # return md5_has_changed, actual_md5 305 | 306 | 307 | class Bunch(dict): 308 | """Dictionary-like object that exposes its keys as attributes.""" 309 | 310 | def __init__(self, **kwargs): # noqa: D102 311 | dict.__init__(self, kwargs) 312 | self.__dict__ = self 313 | 314 | 315 | def _has_pypandoc(): 316 | """Check if pypandoc package available.""" 317 | try: 318 | import pypandoc # noqa 319 | 320 | # Import error raised only when function called 321 | version = pypandoc.get_pandoc_version() 322 | except (ImportError, OSError): 323 | return None, None 324 | else: 325 | return True, version 326 | 327 | 328 | def matches_filepath_pattern(filepath: Path, pattern: str) -> bool: 329 | """ 330 | Check if filepath matches pattern 331 | 332 | Parameters 333 | ---------- 334 | filepath 335 | The filepath to check 336 | 337 | pattern 338 | The pattern to search 339 | 340 | Returns 341 | ------- 342 | rc 343 | A boolean indicating whether the pattern has been found in the filepath 344 | """ 345 | 346 | assert isinstance(filepath, Path) # noqa 347 | 348 | result = re.search(pattern, str(filepath)) 349 | 350 | return True if result is not None else False 351 | 352 | 353 | def is_relative_to(parentpath: Path, subpath: Path) -> bool: 354 | """ 355 | Check if subpath is relative to parentpath 356 | 357 | Parameters 358 | ---------- 359 | parentpath 360 | The (potential) parent path 361 | 362 | subpath 363 | The (potential) subpath 364 | 365 | Returns 366 | ------- 367 | rc 368 | A boolean indicating whether subpath is relative to parentpath 369 | """ 370 | 371 | if not (isinstance(parentpath, Path) and isinstance(subpath, Path)): 372 | raise TypeError("Arguments must both be pathlib objects") 373 | 374 | try: 375 | subpath.relative_to(parentpath) 376 | return True 377 | 378 | except ValueError: 379 | return False 380 | 381 | 382 | def run_async(coro): 383 | try: 384 | loop = asyncio.get_running_loop() 385 | except RuntimeError: 386 | loop = asyncio.new_event_loop() 387 | 388 | try: 389 | return loop.run_until_complete(coro) 390 | finally: 391 | loop.close() 392 | -------------------------------------------------------------------------------- /src/mkdocs_gallery/backreferences.py: -------------------------------------------------------------------------------- 1 | # Authors: Sylvain MARIE <sylvain.marie@se.com> 2 | # + All contributors to <https://github.com/smarie/mkdocs-gallery> 3 | # 4 | # Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 5 | # License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 6 | """ 7 | Backreferences Generator 8 | ======================== 9 | 10 | Parses example file code in order to keep track of used functions 11 | """ 12 | from __future__ import print_function, unicode_literals 13 | 14 | import ast 15 | import codecs 16 | import collections 17 | import inspect 18 | import os 19 | import re 20 | import warnings 21 | from html import escape 22 | from importlib import import_module 23 | from typing import Set 24 | 25 | from . import mkdocs_compatibility 26 | from .errors import ExtensionError 27 | from .gen_data_model import AllInformation, GalleryScriptResults 28 | from .utils import _new_file, _replace_by_new_if_needed 29 | 30 | 31 | class DummyClass(object): 32 | """Dummy class for testing method resolution.""" 33 | 34 | def run(self): 35 | """Do nothing.""" 36 | pass 37 | 38 | @property 39 | def prop(self): 40 | """Property.""" 41 | return "Property" 42 | 43 | 44 | class NameFinder(ast.NodeVisitor): 45 | """Finds the longest form of variable names and their imports in code. 46 | 47 | Only retains names from imported modules. 48 | """ 49 | 50 | def __init__(self, global_variables=None): 51 | super(NameFinder, self).__init__() 52 | self.imported_names = {} 53 | self.global_variables = global_variables or {} 54 | self.accessed_names = set() 55 | 56 | def visit_Import(self, node, prefix=""): 57 | for alias in node.names: 58 | local_name = alias.asname or alias.name 59 | self.imported_names[local_name] = prefix + alias.name 60 | 61 | def visit_ImportFrom(self, node): 62 | self.visit_Import(node, node.module + ".") 63 | 64 | def visit_Name(self, node): 65 | self.accessed_names.add(node.id) 66 | 67 | def visit_Attribute(self, node): 68 | attrs = [] 69 | while isinstance(node, ast.Attribute): 70 | attrs.append(node.attr) 71 | node = node.value 72 | 73 | if isinstance(node, ast.Name): 74 | # This is a.b, not e.g. a().b 75 | attrs.append(node.id) 76 | self.accessed_names.add(".".join(reversed(attrs))) 77 | else: 78 | # need to get a in a().b 79 | self.visit(node) 80 | 81 | def get_mapping(self): 82 | options = list() 83 | for name in self.accessed_names: 84 | local_name_split = name.split(".") 85 | # first pass: by global variables and object inspection (preferred) 86 | for split_level in range(len(local_name_split)): 87 | local_name = ".".join(local_name_split[: split_level + 1]) 88 | remainder = name[len(local_name) :] 89 | if local_name in self.global_variables: 90 | obj = self.global_variables[local_name] 91 | class_attr, method = False, [] 92 | if remainder: 93 | for level in remainder[1:].split("."): 94 | last_obj = obj 95 | # determine if it's a property 96 | prop = getattr(last_obj.__class__, level, None) 97 | if isinstance(prop, property): 98 | obj = last_obj 99 | class_attr, method = True, [level] 100 | break 101 | try: 102 | obj = getattr(obj, level) 103 | except AttributeError: 104 | break 105 | if inspect.ismethod(obj): 106 | obj = last_obj 107 | class_attr, method = True, [level] 108 | break 109 | del remainder 110 | is_class = inspect.isclass(obj) 111 | if is_class or class_attr: 112 | # Traverse all bases 113 | classes = [obj if is_class else obj.__class__] 114 | offset = 0 115 | while offset < len(classes): 116 | for base in classes[offset].__bases__: 117 | # "object" as a base class is not very useful 118 | if base not in classes and base is not object: 119 | classes.append(base) 120 | offset += 1 121 | else: 122 | classes = [obj.__class__] 123 | for cc in classes: 124 | module = inspect.getmodule(cc) 125 | if module is not None: 126 | module = module.__name__.split(".") 127 | class_name = cc.__qualname__ 128 | # a.b.C.meth could be documented as a.C.meth, 129 | # so go down the list 130 | for depth in range(len(module), 0, -1): 131 | full_name = ".".join(module[:depth] + [class_name] + method) 132 | options.append((name, full_name, class_attr, is_class)) 133 | # second pass: by import (can't resolve as well without doing 134 | # some actions like actually importing the modules, so use it 135 | # as a last resort) 136 | for split_level in range(len(local_name_split)): 137 | local_name = ".".join(local_name_split[: split_level + 1]) 138 | remainder = name[len(local_name) :] 139 | if local_name in self.imported_names: 140 | full_name = self.imported_names[local_name] + remainder 141 | is_class = class_attr = False # can't tell without import 142 | options.append((name, full_name, class_attr, is_class)) 143 | return options 144 | 145 | 146 | def _from_import(a, b): 147 | # imp_line = 'from %s import %s' % (a, b) 148 | # scope = dict() 149 | # with warnings.catch_warnings(record=True): # swallow warnings 150 | # warnings.simplefilter('ignore') 151 | # exec(imp_line, scope, scope) 152 | # return scope 153 | with warnings.catch_warnings(record=True): # swallow warnings 154 | warnings.simplefilter("ignore") 155 | m = import_module(a) 156 | obj = getattr(m, b) 157 | 158 | return obj 159 | 160 | 161 | def _get_short_module_name(module_name, obj_name): 162 | """Get the shortest possible module name.""" 163 | if "." in obj_name: 164 | obj_name, attr = obj_name.split(".") 165 | else: 166 | attr = None 167 | # scope = {} 168 | try: 169 | # Find out what the real object is supposed to be. 170 | imported_obj = _from_import(module_name, obj_name) 171 | except Exception: # wrong object 172 | return None 173 | else: 174 | real_obj = imported_obj 175 | if attr is not None and not hasattr(real_obj, attr): # wrong class 176 | return None # wrong object 177 | 178 | parts = module_name.split(".") 179 | short_name = module_name 180 | for i in range(len(parts) - 1, 0, -1): 181 | short_name = ".".join(parts[:i]) 182 | # scope = {} 183 | try: 184 | imported_obj = _from_import(short_name, obj_name) 185 | # Ensure shortened object is the same as what we expect. 186 | assert real_obj is imported_obj # noqa 187 | except Exception: # libraries can throw all sorts of exceptions... 188 | # get the last working module name 189 | short_name = ".".join(parts[: (i + 1)]) 190 | break 191 | return short_name 192 | 193 | 194 | _regex = re.compile(r":(?:" r"func(?:tion)?|" r"meth(?:od)?|" r"attr(?:ibute)?|" r"obj(?:ect)?|" r"class):`~?(\S*)`") 195 | 196 | 197 | def identify_names(script_blocks, global_variables=None, node=""): 198 | """Build a codeobj summary by identifying and resolving used names.""" 199 | 200 | if node == "": # mostly convenience for testing functions 201 | c = "\n".join(txt for kind, txt, _ in script_blocks if kind == "code") 202 | node = ast.parse(c) 203 | 204 | # Get matches from the code (AST) 205 | finder = NameFinder(global_variables) 206 | if node is not None: 207 | finder.visit(node) 208 | names = list(finder.get_mapping()) 209 | 210 | # Get matches from docstring inspection 211 | text = "\n".join(txt for kind, txt, _ in script_blocks if kind == "text") 212 | names.extend((x, x, False, False) for x in re.findall(_regex, text)) 213 | example_code_obj = collections.OrderedDict() # order is important 214 | 215 | # Make a list of all guesses, in `_embed_code_links` we will break when we find a match 216 | for name, full_name, class_like, is_class in names: 217 | if name not in example_code_obj: 218 | example_code_obj[name] = list() 219 | 220 | # name is as written in file (e.g. np.asarray) 221 | # full_name includes resolved import path (e.g. numpy.asarray) 222 | splitted = full_name.rsplit(".", 1 + class_like) 223 | if len(splitted) == 1: 224 | splitted = ("builtins", splitted[0]) 225 | elif len(splitted) == 3: # class-like 226 | assert class_like # noqa 227 | splitted = (splitted[0], ".".join(splitted[1:])) 228 | else: 229 | assert not class_like # noqa 230 | 231 | module, attribute = splitted 232 | 233 | # get shortened module name 234 | module_short = _get_short_module_name(module, attribute) 235 | cobj = { 236 | "name": attribute, 237 | "module": module, 238 | "module_short": module_short or module, 239 | "is_class": is_class, 240 | } 241 | 242 | example_code_obj[name].append(cobj) 243 | 244 | return example_code_obj 245 | 246 | 247 | # TODO only:: html ? 248 | THUMBNAIL_TEMPLATE = """ 249 | <div class="mkd-glr-thumbcontainer" tooltip="{snippet}"> 250 | <!--div class="figure align-default" id="id1"--> 251 | <img alt="{title}" src="{thumbnail}" /> 252 | <p class="caption"> 253 | <span class="caption-text"> 254 | <a class="reference internal" href="{example_html}"> 255 | <span class="std std-ref">{title}</span> 256 | </a> 257 | </span> 258 | <!--a class="headerlink" href="#id1" title="Permalink to this image"></a--> 259 | </p> 260 | <!--/div--> 261 | </div> 262 | """ 263 | 264 | # TODO something specific here ? 265 | BACKREF_THUMBNAIL_TEMPLATE = THUMBNAIL_TEMPLATE 266 | # + """ 267 | # .. only:: not html 268 | # 269 | # * :ref:`mkd_glr_{ref_name}` 270 | # """ 271 | 272 | 273 | def _thumbnail_div(script_results: GalleryScriptResults, is_backref: bool = False, check: bool = True): 274 | """ 275 | Generate MD to place a thumbnail in a gallery. 276 | 277 | Parameters 278 | ---------- 279 | script_results : GalleryScriptResults 280 | The results from processing a gallery example 281 | 282 | is_backref : bool 283 | ? 284 | 285 | check : bool 286 | ? 287 | 288 | Returns 289 | ------- 290 | md : str 291 | The markdown to integrate in the global gallery readme. Note that this is also the case for subsections. 292 | """ 293 | # Absolute path to the thumbnail 294 | if check and not script_results.thumb.exists(): 295 | # This means we have done something wrong in creating our thumbnail! 296 | raise ExtensionError(f"Could not find internal mkdocs-gallery thumbnail file:\n{script_results.thumb}") 297 | 298 | # Relative path to the thumbnail (relative to the gallery, not the subsection) 299 | thumb = script_results.thumb_rel_root_gallery 300 | 301 | # Relative path to the html tutorial that will be generated from the md 302 | example_html = script_results.script.md_file_rel_root_gallery.with_suffix("") 303 | 304 | template = BACKREF_THUMBNAIL_TEMPLATE if is_backref else THUMBNAIL_TEMPLATE 305 | return template.format( 306 | snippet=escape(script_results.intro), 307 | thumbnail=thumb, 308 | title=script_results.script.title, 309 | example_html=example_html, 310 | ) 311 | 312 | 313 | def _write_backreferences(backrefs: Set, seen_backrefs: Set, script_results: GalleryScriptResults): 314 | """ 315 | Write backreference file including a thumbnail list of examples. 316 | 317 | Parameters 318 | ---------- 319 | backrefs : set 320 | 321 | seen_backrefs : set 322 | 323 | script_results : GalleryScriptResults 324 | 325 | Returns 326 | ------- 327 | 328 | """ 329 | all_info = script_results.script.gallery.all_info 330 | 331 | for backref in backrefs: 332 | # Get the backref file to use for this module, according to config 333 | include_path = _new_file(all_info.get_backreferences_file(backref)) 334 | 335 | # Create new or append to existing file 336 | seen = backref in seen_backrefs 337 | with codecs.open(str(include_path), "a" if seen else "w", encoding="utf-8") as ex_file: 338 | # If first ref: write header 339 | if not seen: 340 | # Be aware that if the number of lines of this heading changes, 341 | # the minigallery directive should be modified accordingly 342 | heading = "Examples using ``%s``" % backref 343 | ex_file.write("\n\n" + heading + "\n") 344 | ex_file.write("^" * len(heading) + "\n") 345 | 346 | # Write the thumbnail 347 | ex_file.write(_thumbnail_div(script_results, is_backref=True)) 348 | seen_backrefs.add(backref) 349 | 350 | 351 | def _finalize_backreferences(seen_backrefs, all_info: AllInformation): 352 | """Replace backref files only if necessary.""" 353 | logger = mkdocs_compatibility.getLogger("mkdocs-gallery") 354 | if all_info.gallery_conf["backreferences_dir"] is None: 355 | return 356 | 357 | for backref in seen_backrefs: 358 | # Get the backref file to use for this module, according to config 359 | path = _new_file(all_info.get_backreferences_file(backref)) 360 | if path.exists(): 361 | # Simply drop the .new suffix 362 | _replace_by_new_if_needed(path, md5_mode="t") 363 | else: 364 | # No file: warn 365 | level = all_info.gallery_conf["log_level"].get("backreference_missing", "warning") 366 | func = getattr(logger, level) 367 | func("Could not find backreferences file: %s" % (path,)) 368 | func("The backreferences are likely to be erroneous " "due to file system case insensitivity.") 369 | --------------------------------------------------------------------------------