├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── prerelease.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── docs ├── conf.py ├── index.rst ├── options.rst ├── setup.rst └── thebelab.rst ├── jupyter_sphinx ├── __init__.py ├── _version.py ├── ast.py ├── css │ └── jupyter-sphinx.css ├── execute.py ├── thebelab.py ├── thebelab │ ├── thebelab-helper.js │ └── thebelab.css └── utils.py ├── pyproject.toml └── tests ├── conftest.py ├── test_execute.py └── test_execute ├── test_bash_kernel.sh ├── test_basic_html_.html ├── test_basic_singlehtml_.html ├── test_builder_priority_html.html ├── test_builder_priority_latex.tex ├── test_cell_output_to_nodes.html ├── test_code_below.html ├── test_continue_linenos_not_automatic.html ├── test_continue_linenos_with_start.html ├── test_continue_lineos_conf_option.html ├── test_emphasize_lines.html ├── test_emphasize_lines_with_dash.html ├── test_execution_environment_carries_over.html ├── test_hide_code.html ├── test_hide_output.html ├── test_input_cell.html ├── test_input_cell_linenos.html ├── test_javascript.html ├── test_jupyter_download_nb_.html ├── test_jupyter_download_notebook_.html ├── test_jupyter_download_script_.html ├── test_kernel_restart.html ├── test_latex.html ├── test_linenos.html ├── test_linenos_code_below.html ├── test_linenos_conf_option.html ├── test_multiple_directives_types.html ├── test_output_cell.html ├── test_raises_incell.html ├── test_raises_specific_error_incell.html ├── test_save_script.py ├── test_stderr.html ├── test_stderr_hidden.html ├── test_stdout.html ├── test_thebe_button_auto.html ├── test_thebe_button_manual.html ├── test_thebe_code_below.html ├── test_thebe_hide_code.html ├── test_thebe_hide_output.html └── test_widgets.html /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 4 | "features": { 5 | "ghcr.io/devcontainers-contrib/features/hatch:2": {}, 6 | "ghcr.io/devcontainers-contrib/features/pre-commit:2": {} 7 | }, 8 | "postCreateCommand": "pre-commit install" 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | # Python 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled pre-release tests 2 | 3 | on: 4 | # Run this workflow once a week (https://crontab.guru/#0_5_*_*_1) 5 | schedule: 6 | - cron: "0 5 * * 1" 7 | workflow_dispatch: 8 | 9 | # env variable to force pip to install pre-released versions 10 | # in hatch envs 11 | env: 12 | PIP_PRE: 1 13 | 14 | jobs: 15 | tests: 16 | strategy: 17 | matrix: 18 | python-version: ["3.8", "3.9", "3.10", "3.11", " 3.12"] 19 | os: [ubuntu-latest] 20 | include: 21 | - os: windows-latest 22 | python-version: "3.12" 23 | - os: macos-latest 24 | python-version: "3.12" 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: pip install hatch 34 | - name: Run tests 35 | run: hatch run test:test -x 36 | 37 | docs: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: "3.10" 44 | - name: Install dependencies 45 | run: pip install hatch 46 | 47 | - name: Build docs 48 | run: hatch run doc:build 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This will run every time a tag is created and pushed to the repository. 2 | # It calls our tests workflow via a `workflow_call`, and if tests pass 3 | # then it triggers our upload to PyPI for a new release. 4 | name: Publish to PyPI 5 | on: 6 | release: 7 | types: ["published"] 8 | 9 | jobs: 10 | tests: 11 | uses: ./.github/workflows/tests.yml 12 | 13 | publish: 14 | needs: [tests] 15 | name: Publish to PyPi 16 | runs-on: ubuntu-latest 17 | environment: release 18 | permissions: 19 | id-token: write 20 | steps: 21 | - name: Checkout source 22 | uses: actions/checkout@v4 23 | - name: Set up Python "3.10" 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: "3.10" 27 | - name: install dependencies 28 | run: pip install build 29 | - name: Build package 30 | run: python -m build 31 | - name: Publish 32 | uses: pypa/gh-action-pypi-publish@v1.9.0 33 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | schedule: 8 | - cron: "0 8 * * *" 9 | workflow_call: 10 | 11 | jobs: 12 | pre-commit: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.10" 19 | - uses: pre-commit/action@v3.0.1 20 | 21 | tests: 22 | strategy: 23 | matrix: 24 | python-version: ["3.8", "3.9", "3.10", "3.11", " 3.12"] 25 | os: [ubuntu-latest] 26 | include: 27 | - os: windows-latest 28 | python-version: "3.12" 29 | - os: macos-latest 30 | python-version: "3.12" 31 | 32 | runs-on: ${{ matrix.os }} 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Install hatch 41 | run: pip install hatch 42 | - name: Run tests 43 | run: hatch run test:test -x 44 | 45 | docs: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v5 50 | with: 51 | python-version: "3.10" 52 | - name: Install hatch 53 | run: pip install hatch 54 | - name: Build docs 55 | run: hatch run doc:build 56 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 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 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: "https://github.com/psf/black" 3 | rev: "22.3.0" 4 | hooks: 5 | - id: black 6 | 7 | - repo: "https://github.com/kynan/nbstripout" 8 | rev: "0.5.0" 9 | hooks: 10 | - id: nbstripout 11 | 12 | - repo: "https://github.com/pre-commit/mirrors-prettier" 13 | rev: "v2.7.1" 14 | hooks: 15 | - id: prettier 16 | exclude: tests\/test_execute\/ 17 | 18 | - repo: https://github.com/charliermarsh/ruff-pre-commit 19 | rev: "v0.0.215" 20 | hooks: 21 | - id: ruff 22 | 23 | - repo: https://github.com/PyCQA/doc8 24 | rev: "v1.1.1" 25 | hooks: 26 | - id: doc8 27 | 28 | - repo: https://github.com/codespell-project/codespell 29 | rev: v2.2.4 30 | hooks: 31 | - id: codespell 32 | stages: [commit] 33 | additional_dependencies: 34 | - tomli 35 | 36 | # Prevent committing inline conflict markers 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: v4.3.0 39 | hooks: 40 | - id: check-merge-conflict 41 | args: [--assume-in-merge] 42 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | version: 2 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | sphinx: 8 | configuration: docs/conf.py 9 | python: 10 | install: 11 | - method: pip 12 | path: . 13 | extra_requirements: 14 | - doc 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # General Jupyter contributor guidelines 2 | 3 | If you're reading this section, you're probably interested in 4 | contributing to Jupyter. Welcome and thanks for your interest in 5 | contributing! 6 | 7 | Please take a look at the Contributor documentation, familiarize 8 | yourself with using the Jupyter Server, and introduce yourself on the 9 | mailing list and share what area of the project you are interested in 10 | working on. 11 | 12 | For general documentation about contributing to Jupyter projects, see 13 | the [Project Jupyter Contributor 14 | Documentation](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html). 15 | 16 | # Setting Up a Development Environment 17 | 18 | ## Installing the Jupyter Server 19 | 20 | The development version of the server requires 21 | [node](https://nodejs.org/en/download/) and 22 | [pip](https://pip.pypa.io/en/stable/installing/). 23 | 24 | Once you have installed the dependencies mentioned above, use the 25 | following steps: 26 | 27 | ``` 28 | pip install --upgrade pip 29 | git clone https://github.com/jupyter/jupyter-sphinx 30 | cd jupyter-server 31 | pip install -e ".[test]" 32 | ``` 33 | 34 | ## Code Styling and Quality Checks 35 | 36 | `jupyter-sphinx` has adopted automatic code formatting so you shouldn't 37 | need to worry too much about your code style. As long as your code is 38 | valid, the pre-commit hook should take care of how it should look. 39 | `pre-commit` and its associated hooks will automatically be installed 40 | when you run `pip install -e ".[test]"` 41 | 42 | To install `pre-commit` hook manually, run the following: 43 | 44 | ``` 45 | pre-commit install 46 | ``` 47 | 48 | You can invoke the pre-commit hook by hand at any time with: 49 | 50 | ``` 51 | pre-commit run 52 | ``` 53 | 54 | which should run any autoformatting on your code and tell you about any 55 | errors it couldn't fix automatically. You may also install [black 56 | integration](https://github.com/psf/black#editor-integration) into your 57 | text editor to format code automatically. 58 | 59 | If you have already committed files before setting up the pre-commit 60 | hook with `pre-commit install`, you can fix everything up using 61 | `pre-commit run --all-files`. You need to make the fixing commit 62 | yourself after that. 63 | 64 | Some of the hooks only run on CI by default, but you can invoke them by 65 | running with the `--hook-stage manual` argument. 66 | 67 | There are three hatch scripts that can be run locally as well: 68 | `hatch run lint:build` will enforce styling. 69 | 70 | # Running Tests 71 | 72 | Install dependencies: 73 | 74 | ``` 75 | pip install -e .[test] 76 | ``` 77 | 78 | To run the Python tests, use: 79 | 80 | ``` 81 | pytest 82 | ``` 83 | 84 | You can also run the tests using `hatch` without installing test 85 | dependencies in your local environment: 86 | 87 | ``` 88 | pip install hatch 89 | hatch run test:test 90 | ``` 91 | 92 | The command takes any argument that you can give to `pytest`, e.g.: 93 | 94 | ``` 95 | hatch run test:test -k name_of_method_to_test 96 | ``` 97 | 98 | You can also drop into a shell in the test environment by running: 99 | 100 | ``` 101 | hatch -e test shell 102 | ``` 103 | 104 | # Building the Docs 105 | 106 | Install the docs requirements using `pip`: 107 | 108 | ``` 109 | pip install .[doc] 110 | ``` 111 | 112 | Once you have installed the required packages, you can build the docs 113 | with: 114 | 115 | ``` 116 | cd docs 117 | make html 118 | ``` 119 | 120 | You can also run the tests using `hatch` without installing test 121 | dependencies in your local environment. 122 | 123 | ```bash 124 | pip install hatch 125 | hatch run docs:build 126 | ``` 127 | 128 | You can also drop into a shell in the docs environment by running: 129 | 130 | ``` 131 | hatch -e docs shell 132 | ``` 133 | 134 | After that, the generated HTML files will be available at 135 | `build/html/index.html`. You may view the docs in your browser. 136 | 137 | You should also have a look at the [Project Jupyter Documentation 138 | Guide](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html). 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Brian E. Granger and Jake Vanderplas 2 | Copyright (c) 2016-2019, Project Jupyter Contributors 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Sphinx Extensions 2 | 3 | [![BSD licence](https://img.shields.io/badge/License-BSD3-yellow.svg?logo=opensourceinitiative&logoColor=white)](LICENSE) 4 | [![black](https://img.shields.io/badge/code%20style-black-000000)](https://github.com/psf/black) 5 | [![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier&logoColor=white)](https://github.com/prettier/prettier) 6 | [![pre-commit](https://img.shields.io/badge/pre--commit-active-yellow?logo=pre-commit&logoColor=white)](https://pre-commit.com/) 7 | [![pypi version](https://img.shields.io/pypi/v/jupyter-sphinx?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/jupyter-sphinx/) 8 | [![conda-forge version badge](https://img.shields.io/conda/vn/conda-forge/jupyter-sphinx?logo=anaconda&logoColor=white&color=blue)](https://anaconda.org/conda-forge/jupyter-sphinx) 9 | [![tests](https://img.shields.io/github/actions/workflow/status/jupyter/jupyter-sphinx/tests.yml?logo=github&logoColor=white)](https://github.com/jupyter/jupyter-sphinx/actions/workflows/tests.yml) 10 | [![docs](https://img.shields.io/readthedocs/jupyter-sphinx?logo=readthedocs&logoColor=white)](https://jupyter-sphinx.readthedocs.io/) 11 | 12 | `jupyter-sphinx` enables running code embedded in Sphinx documentation and 13 | embedding output of that code into the resulting document. It has support 14 | for rich output such as images and even Jupyter interactive widgets. 15 | 16 | ## Installation 17 | 18 | With pip: 19 | 20 | ```bash 21 | pip install jupyter_sphinx 22 | ``` 23 | 24 | with conda: 25 | 26 | ```bash 27 | conda install jupyter_sphinx -c conda-forge 28 | ``` 29 | 30 | ## Usage 31 | 32 | You can check out the documentation on https://jupyter-sphinx.readthedocs.io for up to date 33 | usage information and examples. 34 | 35 | ## License 36 | 37 | We use a shared copyright model that enables all contributors to maintain the 38 | copyright on their contributions. 39 | 40 | All code is licensed under the terms of the revised BSD license. 41 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release instructions for jupyter-sphinx 2 | 3 | Jupyter Sphinx uses a GitHub action to automatically push a new release to 4 | PyPI when a GitHub release is added. 5 | 6 | To cut a new Jupyter Sphinx release, follow these steps: 7 | 8 | - Ensure that all tests are passing on main. 9 | 10 | - Create a pull request to bump the version: 11 | 12 | - In [`_version.py`](https://github.com/jupyter/jupyter-sphinx/blob/main/jupyter_sphinx/_version.py), 13 | update the version number: 14 | 15 | ```python 16 | __version__ = "0.2.3" 17 | ``` 18 | 19 | - Merge the pull request. 20 | 21 | - [Create a new Github Release](https://github.com/jupyter/jupyter-sphinx/releases/new). 22 | The target should be **main**, the tag and the title should be the version number, 23 | e.g. `v0.2.3`. Note that this requires "Maintain" permissions on the repository. 24 | 25 | - Creating the release in GitHub will push a tag commit to the repository, which will 26 | trigger [a GitHub action](https://github.com/jupyter/jupyter-sphinx/blob/main/.github/workflows/artifacts.yml) 27 | to build `jupyter-sphinx` and push the new version to PyPI. 28 | [Confirm that the version has been bumped](https://pypi.org/project/jupyter-sphinx/). 29 | 30 | - That's it! 31 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration file for the Sphinx documentation builder. 3 | 4 | For a full list see the documentation: http://www.sphinx-doc.org/en/master/config 5 | """ 6 | 7 | # -- Path setup ---------------------------------------------------------------- 8 | import os 9 | import sys 10 | 11 | sys.path.insert(0, os.path.abspath("../..")) 12 | 13 | import jupyter_sphinx # noqa: E402 14 | 15 | # -- Project information ------------------------------------------------------- 16 | project = "Jupyter Sphinx" 17 | copyright = "2019, Jupyter Development Team" 18 | author = "Jupyter Development Team" 19 | release = jupyter_sphinx.__version__ 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | extensions = ["sphinx.ext.mathjax", "jupyter_sphinx", "sphinx_design"] 23 | 24 | # -- Options for HTML output --------------------------------------------------- 25 | html_theme = "sphinx_book_theme" 26 | html_title = "jupyter-sphinx" 27 | html_theme_options = { 28 | "repository_url": "https://github.com/jupyter/jupyter-sphinx", 29 | "use_repository_button": True, 30 | "repository_branch": "main", 31 | "use_issues_button": True, 32 | "use_fullscreen_button": False, 33 | } 34 | 35 | # -- Options for LaTeX output -------------------------------------------------- 36 | latex_engine = "xelatex" 37 | 38 | # -- Jupyter Sphinx options ---------------------------------------------------- 39 | jupyter_sphinx_thebelab_config = {"binderOptions": {"repo": "jupyter/jupyter-sphinx"}} 40 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Jupyter Sphinx Extension 2 | ======================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 2 7 | :caption: Contents: 8 | 9 | setup 10 | thebelab 11 | options 12 | changelog 13 | 14 | Jupyter-sphinx is a Sphinx extension that executes embedded code in a Jupyter kernel, and embeds outputs of that code in the document. It has support for rich output such as images, Latex math and even javascript widgets, and it allows to enable `thebelab `_ for live code execution with minimal effort. 15 | 16 | .. code-block:: rst 17 | 18 | .. jupyter-execute:: 19 | 20 | print("Hello world!") 21 | 22 | .. jupyter-execute:: 23 | 24 | print("Hello world!") 25 | 26 | .. grid:: 1 2 2 3 27 | :gutter: 2 28 | 29 | .. grid-item-card:: :fas:`download` Getting started 30 | :link: setup.html 31 | 32 | Learn how to use the lib from different sources. 33 | 34 | .. grid-item-card:: :fas:`book-open` Advance usage 35 | :link: options.html 36 | 37 | Learn advance usage and extra configuration of ``jupyter-sphinx``. 38 | 39 | .. grid-item-card:: :fas:`plug` Thebelab 40 | :link: thebelab.html 41 | 42 | Discover how ``ThebeLab`` is linked to ``jupyter-sphinx``. 43 | 44 | .. seealso:: 45 | 46 | Other extensions exist to display output of IPyhton cells in a Sphinx documentation. If you want to execute entire notebooks you can consider using `nbsphinx `__ or `myst-nb `__. For in-page live execution consider using `sphinx-thebe `__ or `jupyterlite-sphinx `__. For users that don't need to rely on a jupyter kernel the lightweigth `IPython sphinx directive `__ can be used but remember it will only be able to display text outputs. 47 | -------------------------------------------------------------------------------- /docs/options.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Directive options 5 | ----------------- 6 | You may choose to hide the code of a cell, but keep its output visible using ``:hide-code:``: 7 | 8 | .. code-block:: rst 9 | 10 | .. jupyter-execute:: 11 | :hide-code: 12 | 13 | print("this code is invisible") 14 | 15 | produces: 16 | 17 | .. jupyter-execute:: 18 | :hide-code: 19 | 20 | print("this code is invisible") 21 | 22 | this option is particularly useful if you want to embed correctness checks in building your documentation: 23 | 24 | .. code-block:: rst 25 | 26 | .. jupyter-execute:: 27 | :hide-code: 28 | 29 | assert everything_works, "There's a bug somewhere" 30 | 31 | This way even though the code won't make it into the documentation, the build will fail if running the code fails. 32 | 33 | Similarly, outputs are hidden with ``:hide-output:``: 34 | 35 | .. code-block:: rst 36 | 37 | .. jupyter-execute:: 38 | :hide-output: 39 | 40 | print("this output is invisible") 41 | 42 | produces: 43 | 44 | .. jupyter-execute:: 45 | :hide-output: 46 | 47 | print("this output is invisible") 48 | 49 | You may also display the code *below* the output with ``:code-below:``: 50 | 51 | .. code-block:: rst 52 | 53 | .. jupyter-execute:: 54 | :code-below: 55 | 56 | print("this code is below the output") 57 | 58 | produces: 59 | 60 | .. jupyter-execute:: 61 | :code-below: 62 | 63 | print("this code is below the output") 64 | 65 | You may also add *line numbers* to the source code with ``:linenos:``: 66 | 67 | .. code-block:: rst 68 | 69 | .. jupyter-execute:: 70 | :linenos: 71 | 72 | print("A") 73 | print("B") 74 | print("C") 75 | 76 | produces: 77 | 78 | .. jupyter-execute:: 79 | :linenos: 80 | 81 | print("A") 82 | print("B") 83 | print("C") 84 | 85 | To add *line numbers from a specific line* to the source code, use the ``lineno-start`` directive: 86 | 87 | .. code-block:: rst 88 | 89 | .. jupyter-execute:: 90 | :lineno-start: 7 91 | 92 | print("A") 93 | print("B") 94 | print("C") 95 | 96 | produces: 97 | 98 | .. jupyter-execute:: 99 | :lineno-start: 7 100 | 101 | print("A") 102 | print("B") 103 | print("C") 104 | 105 | You may also emphasize particular lines in the source code with ``:emphasize-lines:``: 106 | 107 | .. code-block:: rst 108 | 109 | .. jupyter-execute:: 110 | :emphasize-lines: 2,5-6 111 | 112 | d = { 113 | "a": 1, 114 | "b": 2, 115 | "c": 3, 116 | "d": 4, 117 | "e": 5, 118 | } 119 | 120 | produces: 121 | 122 | .. jupyter-execute:: 123 | :lineno-start: 2 124 | :emphasize-lines: 2,5-6 125 | 126 | d = { 127 | "a": 1, 128 | "b": 2, 129 | "c": 3, 130 | "d": 4, 131 | "e": 5, 132 | } 133 | 134 | Controlling exceptions 135 | ---------------------- 136 | 137 | The default behaviour when jupyter-sphinx encounters an error in the embedded code is just to stop execution of the document and display a stack trace. However, there are many cases where it may be illustrative for execution to continue and for a stack trace to be shown as *output of the cell*. This behaviour can be enabled by using the ``raises`` option: 138 | 139 | .. code-block:: rst 140 | 141 | .. jupyter-execute:: 142 | :raises: 143 | 144 | 1 / 0 145 | 146 | produces: 147 | 148 | .. jupyter-execute:: 149 | :raises: 150 | 151 | 1 / 0 152 | 153 | Note that when given no arguments, ``raises`` will catch all errors. It is also possible to give ``raises`` a list of error types; if an error is raised that is not in the list then execution stops as usual: 154 | 155 | .. code-block:: rst 156 | 157 | .. jupyter-execute:: 158 | :raises: KeyError, ValueError 159 | 160 | a = {"hello": "world!"} 161 | a["jello"] 162 | 163 | produces: 164 | 165 | .. jupyter-execute:: 166 | :raises: KeyError, ValueError 167 | 168 | a = {"hello": "world!"} 169 | a["jello"] 170 | 171 | Additionally, any output sent to the ``stderr`` stream of a cell will result in ``jupyter-sphinx`` producing a warning. This behaviour can be suppressed (and the ``stderr`` stream printed as regular output) by providing the ``stderr`` option: 172 | 173 | .. code-block:: rst 174 | 175 | .. jupyter-execute:: 176 | :stderr: 177 | 178 | import sys 179 | 180 | print("hello, world!", file=sys.stderr) 181 | 182 | produces: 183 | 184 | .. jupyter-execute:: 185 | :stderr: 186 | 187 | import sys 188 | 189 | print("hello, world!", file=sys.stderr) 190 | 191 | Manually forming Jupyter cells 192 | ------------------------------ 193 | 194 | When showing code samples that are computationally expensive, access restricted resources, or have non-deterministic output, it can be preferable to not have them run every time you build. You can simply embed input code without executing it using the ``jupyter-input`` directive expected output with ``jupyter-output``: 195 | 196 | .. code-block:: rst 197 | 198 | .. jupyter-input:: 199 | :linenos: 200 | 201 | import time 202 | 203 | def slow_print(str): 204 | time.sleep(4000) # Simulate an expensive process 205 | print(str) 206 | 207 | slow_print("hello, world!") 208 | 209 | .. jupyter-output:: 210 | 211 | hello, world! 212 | 213 | produces: 214 | 215 | .. jupyter-input:: 216 | :linenos: 217 | 218 | import time 219 | 220 | def slow_print(str): 221 | time.sleep(4000) # Simulate an expensive process 222 | print(str) 223 | 224 | slow_print("hello, world!") 225 | 226 | .. jupyter-output:: 227 | 228 | hello, world! 229 | 230 | Controlling the execution environment 231 | ------------------------------------- 232 | The execution environment can be controlled by using the ``jupyter-kernel`` directive. This directive takes the name of the Jupyter kernel in which all future cells (until the next ``jupyter-kernel`` directive) should be run: 233 | 234 | .. code-block:: rst 235 | 236 | .. jupyter-kernel:: python3 237 | :id: a_unique_name 238 | 239 | ``jupyter-kernel`` can also take a directive option ``:id:`` that names the Jupyter session; it is used in conjunction with the ``jupyter-download`` roles described in the next section. 240 | 241 | Note that putting a ``jupyter-kernel`` directive starts a *new* kernel, so any variables and functions declared in cells *before* a ``jupyter-kernel`` directive will not be available in future cells. 242 | 243 | Note that we are also not limited to working with Python: Jupyter Sphinx supports kernels for any programming language, and we even get proper syntax highlighting thanks to the power of ``Pygments``. 244 | 245 | Downloading the code as a script 246 | -------------------------------- 247 | 248 | Jupyter Sphinx includes 2 roles that can be used to download the code embedded in a document: ``:jupyter-download-script:`` (for a raw script file) and ``:jupyter-download-notebook:`` or ``:jupyter-download-nb:`` (for a Jupyter notebook). 249 | 250 | These roles are equivalent to the standard sphinx `download role `__, **except** the extension of the file should not be given. For example, to download all the code from this document as a script we would use: 251 | 252 | .. code-block:: rst 253 | 254 | :jupyter-download-script:`click to download ` 255 | 256 | Which produces a link like this: :jupyter-download-nb:`click to download `. The target that the role is applied to (``index`` in this case) is the name of the document for which you wish to download the code. If a document contains ``jupyter-kernel`` directives with ``:id:`` specified, then the name provided to ``:id:`` can be used to get the code for the cells belonging to the that Jupyter session. 257 | 258 | Styling options 259 | --------------- 260 | 261 | The CSS (Cascading Style Sheet) class structure of jupyter-sphinx is the following: 262 | 263 | .. code-block:: rst 264 | 265 | - jupyter_container, jupyter_cell 266 | - cell_input 267 | - cell_output 268 | - stderr 269 | - output 270 | 271 | If a code cell is not displayed, the output is provided without the ``jupyter_container``. If you want to adjust the styles, add a new stylesheet, e.g. ``custom.css``, and adjust your ``conf.py`` to load it. How you do so depends on the theme you are using. 272 | 273 | Here is a sample ``custom.css`` file overriding the ``stderr`` background color: 274 | 275 | .. code-block:: css 276 | 277 | .jupyter_container .stderr { 278 | background-color: #7FFF00; 279 | } 280 | 281 | Alternatively, you can also completely overwrite the CSS and JS files that are added by Jupyter Sphinx by providing a full copy of a ``jupyter-sphinx.css`` (which can be empty) file in your ``_static`` folder. This is also possible with the thebelab CSS and JS that is added. 282 | 283 | Configuration options 284 | --------------------- 285 | 286 | Typically you will be using Sphinx to build documentation for a software package. 287 | 288 | If you are building documentation for a Python package you should add the following 289 | lines to your sphinx ``conf.py``:: 290 | 291 | import os 292 | 293 | package_path = os.path.abspath('../..') 294 | os.environ['PYTHONPATH'] = ':'.join((package_path, os.environ.get('PYTHONPATH', ''))) 295 | 296 | This will ensure that your package is importable by any IPython kernels, as they will 297 | inherit the environment variables from the main Sphinx process. 298 | 299 | Here is a list of all the configuration options available to the Jupyter Sphinx extension: 300 | 301 | .. csv-table:: Configuration options 302 | :header-rows: 1 303 | 304 | name, description 305 | ``jupyter_execute_default_kernel``,"The default kernel to launch when executing code in ``jupyter-execute`` directives. Default to ``python3``." 306 | ``render_priority_html``,"The priority of different output mimetypes for displaying in HTML output. Mimetypes earlier in the data priority list are preferred over later ones. This is relevant if a code cell produces an output that has several possible representations (e.g. description text or an image). Please open an issue if you find a mimetype that isn't supported, but should be. Default to ``['application/vnd.jupyter.widget-view+json', 'text/html', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/latex', 'text/plain']``." 307 | ``render_priority_latex``,"Same as ``render_priority_html``, but for latex. The default is ``['image/svg+xml', 'image/png', 'image/jpeg', 'text/latex', 'text/plain']``." 308 | ``jupyter_execute_kwargs``,"Keyword arguments to pass to ``nbconvert.preprocessors.execute.executenb``, which controls how code cells are executed. The default is ``{'timeout':-1, 'allow_errors': True)``." 309 | ``jupyter_sphinx_linenos``,"Whether to show line numbering in all ``jupyter-execute`` sources." 310 | ``jupyter_sphinx_continue_linenos``,"Whether to continue line numbering from previous cell in all ``jupyter-execute`` sources." 311 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Installation 5 | ------------ 6 | Get ``jupyter-sphinx`` from pip: 7 | 8 | .. code-block:: bash 9 | 10 | pip install jupyter-sphinx 11 | 12 | or ``conda``: 13 | 14 | .. code-block:: bash 15 | 16 | conda install jupyter_sphinx -c conda-forge 17 | 18 | Enabling the extension 19 | ---------------------- 20 | 21 | To enable the extension, add ``jupyter_sphinx`` to your enabled extensions in ``conf.py``: 22 | 23 | .. code-block:: python 24 | 25 | extensions = [ 26 | "jupyter_sphinx", 27 | ] 28 | 29 | Basic Usage 30 | ----------- 31 | 32 | You can use the ``jupyter-execute`` directive to embed code into the document: 33 | 34 | .. code-block:: rst 35 | 36 | .. jupyter-execute:: 37 | 38 | name = "world" 39 | print(f"hello {name} !") 40 | 41 | The above is rendered as follows: 42 | 43 | .. jupyter-execute:: 44 | 45 | name = "world" 46 | print(f"hello {name} !") 47 | 48 | Note that the code produces *output* (printing the string ``"hello world!"``), and the output is rendered directly after the code snippet. 49 | 50 | Because all code cells in a document are run in the same kernel, cells later in the document can use variables and functions defined in cells earlier in the document: 51 | 52 | .. jupyter-execute:: 53 | 54 | a = 1 55 | print(f"first cell: a = {a}") 56 | 57 | .. jupyter-execute:: 58 | 59 | a += 1 60 | print("second cell: a = {a}") 61 | 62 | Because ``jupyter-sphinx`` uses the machinery of ``nbconvert``, it is capable of rendering any rich output, for example plots: 63 | 64 | .. jupyter-execute:: 65 | 66 | import numpy as np 67 | from matplotlib import pyplot 68 | %matplotlib inline 69 | 70 | x = np.linspace(1E-3, 2 * np.pi) 71 | 72 | pyplot.plot(x, np.sin(x) / x) 73 | pyplot.plot(x, np.cos(x)) 74 | pyplot.grid() 75 | 76 | LaTeX output: 77 | 78 | .. jupyter-execute:: 79 | 80 | from IPython.display import Latex 81 | Latex("\\int_{-\\infty}^\\infty e^{-x²}dx = \\sqrt{\\pi}") 82 | 83 | or even full-blown javascript widgets: 84 | 85 | .. jupyter-execute:: 86 | 87 | import ipywidgets as w 88 | from IPython.display import display 89 | 90 | a = w.IntSlider() 91 | b = w.IntText() 92 | w.jslink((a, "value"), (b, "value")) 93 | display(a, b) 94 | 95 | It is also possible to include code from a regular file by passing the filename as argument to ``jupyter-execute``: 96 | 97 | .. code-block:: rst 98 | 99 | .. jupyter-execute:: some_code.py 100 | 101 | ``jupyter-execute`` may also be used in docstrings within your Python code, and will be executed 102 | when they are included with Sphinx autodoc. 103 | -------------------------------------------------------------------------------- /docs/thebelab.rst: -------------------------------------------------------------------------------- 1 | Thebelab support 2 | ================ 3 | 4 | To turn on `thebelab `_, specify its configuration directly in ``conf.py``: 5 | 6 | .. code-block:: python 7 | 8 | jupyter_sphinx_thebelab_config = { 9 | "requestKernel": True, 10 | "binderOptions": { 11 | "repo": "binder-examples/requirements", 12 | }, 13 | } 14 | 15 | With this configuration, thebelab is activated with a button click: 16 | 17 | .. thebe-button:: Activate Thebelab 18 | 19 | By default the button is added at the end of the document, but it may also be inserted anywhere using 20 | 21 | .. code-block:: rst 22 | 23 | .. thebe-button:: Optional title 24 | -------------------------------------------------------------------------------- /jupyter_sphinx/__init__.py: -------------------------------------------------------------------------------- 1 | """Simple sphinx extension that executes code in jupyter and inserts output.""" 2 | 3 | from pathlib import Path 4 | 5 | import docutils 6 | import ipywidgets 7 | from IPython.lib.lexers import IPython3Lexer, IPythonTracebackLexer 8 | from sphinx.application import Sphinx 9 | from sphinx.errors import ExtensionError 10 | from sphinx.util import logging 11 | from sphinx.util.fileutil import copy_asset_file 12 | 13 | from ._version import __version__ 14 | from .ast import ( 15 | WIDGET_VIEW_MIMETYPE, 16 | CellInput, 17 | CellInputNode, 18 | CellOutput, 19 | CellOutputNode, 20 | CombineCellInputOutput, 21 | JupyterCell, 22 | JupyterCellNode, 23 | JupyterDownloadRole, 24 | JupyterKernelNode, 25 | JupyterWidgetStateNode, 26 | JupyterWidgetViewNode, 27 | MimeBundleNode, 28 | ) 29 | from .execute import ExecuteJupyterCells, JupyterKernel 30 | from .thebelab import ThebeButton, ThebeButtonNode, ThebeOutputNode, ThebeSourceNode 31 | 32 | REQUIRE_URL_DEFAULT = "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js" 33 | THEBELAB_URL_DEFAULT = "https://unpkg.com/thebelab@^0.4.0" 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | # Constants and functions we'll use later 38 | 39 | # Used for nodes that do not need to be rendered 40 | 41 | 42 | def skip(self, node): 43 | raise docutils.nodes.SkipNode 44 | 45 | 46 | # Used for nodes that should be gone by rendering time (OutputMimeBundleNode) 47 | def halt(self, node): 48 | raise ExtensionError( 49 | "Rendering encountered a node type that should " 50 | "have been removed before rendering: %s" % type(node) 51 | ) 52 | 53 | 54 | # Renders the children of a container 55 | render_container = ( 56 | lambda self, node: self.visit_container(node), 57 | lambda self, node: self.depart_container(node), 58 | ) 59 | 60 | 61 | # Used to render the container and its children as HTML 62 | def visit_container_html(self, node): 63 | self.body.append(node.visit_html()) 64 | self.visit_container(node) 65 | 66 | 67 | def depart_container_html(self, node): 68 | self.depart_container(node) 69 | self.body.append(node.depart_html()) 70 | 71 | 72 | # Used to render an element node as HTML 73 | def visit_element_html(self, node): 74 | self.body.append(node.html()) 75 | raise docutils.nodes.SkipNode 76 | 77 | 78 | # Used to render the ThebeSourceNode conditionally for non-HTML builders 79 | def visit_thebe_source(self, node): 80 | if node["hide_code"]: 81 | raise docutils.nodes.SkipNode 82 | else: 83 | self.visit_container(node) 84 | 85 | 86 | render_thebe_source = ( 87 | visit_thebe_source, 88 | lambda self, node: self.depart_container(node), 89 | ) 90 | 91 | 92 | # Sphinx callback functions 93 | def builder_inited(app: Sphinx): 94 | """Init the build. 95 | 96 | 2 cases 97 | case 1: ipywidgets 7, with require 98 | case 2: ipywidgets 7, no require. 99 | """ 100 | require_url = app.config.jupyter_sphinx_require_url 101 | if require_url: 102 | app.add_js_file(require_url) 103 | embed_url = ( 104 | app.config.jupyter_sphinx_embed_url or ipywidgets.embed.DEFAULT_EMBED_REQUIREJS_URL 105 | ) 106 | else: 107 | embed_url = app.config.jupyter_sphinx_embed_url or ipywidgets.embed.DEFAULT_EMBED_SCRIPT_URL 108 | if embed_url: 109 | app.add_js_file(embed_url) 110 | 111 | 112 | def copy_file(src: Path, dst: Path): 113 | """Wrapper of copy_asset_file to handle path.""" 114 | copy_asset_file(str(src.resolve()), str(dst.resolve())) 115 | 116 | 117 | def build_finished(app: Sphinx, env): 118 | if app.builder.format != "html": 119 | return 120 | 121 | module_dir = Path(__file__).parent 122 | static = Path(app.builder.outdir) / "_static" 123 | 124 | # Copy stylesheet 125 | src = module_dir / "css" / "jupyter-sphinx.css" 126 | copy_file(src, static) 127 | 128 | thebe_config = app.config.jupyter_sphinx_thebelab_config 129 | if not thebe_config: 130 | return 131 | 132 | # Copy all thebelab related assets 133 | src_dir = module_dir / "thebelab" 134 | for fname in ["thebelab-helper.js", "thebelab.css"]: 135 | copy_file(src_dir / fname, static) 136 | 137 | 138 | ############################################################################## 139 | # Main setup 140 | def setup(app: Sphinx): 141 | """A temporary setup function so that we can use it here and in execute. 142 | 143 | This should be removed and converted into `setup` after a deprecation 144 | cycle. 145 | """ 146 | # Configuration 147 | 148 | app.add_config_value( 149 | "jupyter_execute_kwargs", 150 | dict(timeout=-1, allow_errors=True, store_widget_state=True), 151 | "env", 152 | ) 153 | app.add_config_value("jupyter_execute_default_kernel", "python3", "env") 154 | app.add_config_value( 155 | "render_priority_html", 156 | [ 157 | WIDGET_VIEW_MIMETYPE, 158 | "application/javascript", 159 | "text/html", 160 | "image/svg+xml", 161 | "image/png", 162 | "image/jpeg", 163 | "text/latex", 164 | "text/plain", 165 | ], 166 | "env", 167 | ) 168 | app.add_config_value( 169 | "render_priority_latex", 170 | [ 171 | "image/svg+xml", 172 | "image/png", 173 | "image/jpeg", 174 | "text/latex", 175 | "text/plain", 176 | ], 177 | "env", 178 | ) 179 | 180 | # ipywidgets config 181 | app.add_config_value("jupyter_sphinx_require_url", REQUIRE_URL_DEFAULT, "html") 182 | app.add_config_value("jupyter_sphinx_embed_url", None, "html") 183 | 184 | # thebelab config, can be either a filename or a dict 185 | app.add_config_value("jupyter_sphinx_thebelab_config", None, "html") 186 | app.add_config_value("jupyter_sphinx_thebelab_url", THEBELAB_URL_DEFAULT, "html") 187 | 188 | # linenos config 189 | app.add_config_value("jupyter_sphinx_linenos", False, "env") 190 | app.add_config_value("jupyter_sphinx_continue_linenos", False, "env") 191 | 192 | # JupyterKernelNode is just a doctree marker for the 193 | # ExecuteJupyterCells transform, so we don't actually render it. 194 | app.add_node( 195 | JupyterKernelNode, 196 | html=(skip, None), 197 | latex=(skip, None), 198 | textinfo=(skip, None), 199 | text=(skip, None), 200 | man=(skip, None), 201 | ) 202 | 203 | # Register our container nodes, these should behave just like a regular container 204 | for node in [JupyterCellNode, CellInputNode, CellOutputNode, MimeBundleNode]: 205 | app.add_node( 206 | node, 207 | override=True, 208 | html=(render_container), 209 | latex=(render_container), 210 | textinfo=(render_container), 211 | text=(render_container), 212 | man=(render_container), 213 | ) 214 | 215 | # JupyterWidgetViewNode holds widget view JSON, 216 | # but is only rendered properly in HTML documents. 217 | app.add_node( 218 | JupyterWidgetViewNode, 219 | html=(visit_element_html, None), 220 | latex=(skip, None), 221 | textinfo=(skip, None), 222 | text=(skip, None), 223 | man=(skip, None), 224 | ) 225 | # JupyterWidgetStateNode holds the widget state JSON, 226 | # but is only rendered in HTML documents. 227 | app.add_node( 228 | JupyterWidgetStateNode, 229 | html=(visit_element_html, None), 230 | latex=(skip, None), 231 | textinfo=(skip, None), 232 | text=(skip, None), 233 | man=(skip, None), 234 | ) 235 | 236 | # ThebeSourceNode holds the source code and is rendered if 237 | # hide-code is not specified. For HTML it is always rendered, 238 | # but hidden using the stylesheet 239 | app.add_node( 240 | ThebeSourceNode, 241 | html=(visit_container_html, depart_container_html), 242 | latex=render_thebe_source, 243 | textinfo=render_thebe_source, 244 | text=render_thebe_source, 245 | man=render_thebe_source, 246 | ) 247 | 248 | # ThebeOutputNode holds the output of the Jupyter cells 249 | # and is rendered if hide-output is not specified. 250 | app.add_node( 251 | ThebeOutputNode, 252 | html=(visit_container_html, depart_container_html), 253 | latex=render_container, 254 | textinfo=render_container, 255 | text=render_container, 256 | man=render_container, 257 | ) 258 | 259 | # ThebeButtonNode is the button that activates thebelab 260 | # and is only rendered for the HTML builder 261 | app.add_node( 262 | ThebeButtonNode, 263 | html=(visit_element_html, None), 264 | latex=(skip, None), 265 | textinfo=(skip, None), 266 | text=(skip, None), 267 | man=(skip, None), 268 | ) 269 | 270 | app.add_directive("jupyter-execute", JupyterCell) 271 | app.add_directive("jupyter-kernel", JupyterKernel) 272 | app.add_directive("jupyter-input", CellInput) 273 | app.add_directive("jupyter-output", CellOutput) 274 | app.add_directive("thebe-button", ThebeButton) 275 | for sep in [":", "-"]: 276 | # Since Sphinx 4.0.0 using ":" inside of a role/directive does not work. 277 | # Therefore, we add "-" as separator to get e.g., jupyter-download-notebook 278 | # We leave the ":" syntax for backward compatibility reasons. 279 | app.add_role(f"jupyter-download{sep}notebook", JupyterDownloadRole()) 280 | app.add_role(f"jupyter-download{sep}nb", JupyterDownloadRole()) 281 | app.add_role(f"jupyter-download{sep}script", JupyterDownloadRole()) 282 | app.add_transform(CombineCellInputOutput) 283 | app.add_transform(ExecuteJupyterCells) 284 | 285 | # For syntax highlighting 286 | app.add_lexer("ipythontb", IPythonTracebackLexer) 287 | app.add_lexer("ipython3", IPython3Lexer) 288 | 289 | app.connect("builder-inited", builder_inited) 290 | app.connect("build-finished", build_finished) 291 | 292 | # add jupyter-sphinx and thebelab js and css 293 | app.add_css_file("jupyter-sphinx.css") 294 | app.add_js_file("thebelab-helper.js") 295 | app.add_css_file("thebelab.css") 296 | 297 | return {"version": __version__, "parallel_read_safe": True} 298 | -------------------------------------------------------------------------------- /jupyter_sphinx/_version.py: -------------------------------------------------------------------------------- 1 | """store the current version info of the project.""" 2 | import re 3 | from typing import List 4 | 5 | # Version string must appear intact for automatic versioning 6 | __version__ = "0.5.3" 7 | 8 | # Build up version_info tuple for backwards compatibility 9 | pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" 10 | match = re.match(pattern, __version__) 11 | assert match is not None 12 | parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]] 13 | if match["rest"]: 14 | parts.append(match["rest"]) 15 | version_info = tuple(parts) 16 | -------------------------------------------------------------------------------- /jupyter_sphinx/ast.py: -------------------------------------------------------------------------------- 1 | """Manipulating the Sphinx AST with Jupyter objects.""" 2 | 3 | import json 4 | import warnings 5 | from pathlib import Path 6 | 7 | import docutils 8 | import ipywidgets.embed 9 | import nbconvert 10 | from docutils.nodes import literal, math_block 11 | from docutils.parsers.rst import Directive, directives 12 | from sphinx.addnodes import download_reference 13 | from sphinx.errors import ExtensionError 14 | from sphinx.transforms import SphinxTransform 15 | from sphinx.util import parselinenos 16 | from sphinx.util.docutils import ReferenceRole 17 | 18 | from .thebelab import ThebeOutputNode, ThebeSourceNode 19 | from .utils import sphinx_abs_dir, strip_latex_delimiters 20 | 21 | WIDGET_VIEW_MIMETYPE = "application/vnd.jupyter.widget-view+json" 22 | WIDGET_STATE_MIMETYPE = "application/vnd.jupyter.widget-state+json" 23 | 24 | 25 | def csv_option(s): 26 | return [p.strip() for p in s.split(",")] if s else [] 27 | 28 | 29 | def load_content(cell, location, logger): 30 | if cell.arguments: 31 | # As per 'sphinx.directives.code.LiteralInclude' 32 | env = cell.state.document.settings.env 33 | rel_filename, filename = env.relfn2path(cell.arguments[0]) 34 | env.note_dependency(rel_filename) 35 | if cell.content: 36 | logger.warning( 37 | 'Ignoring inline code in Jupyter cell included from "{}"'.format(rel_filename), 38 | location=location, 39 | ) 40 | try: 41 | with Path(filename).open() as f: 42 | content = [line.rstrip() for line in f.readlines()] 43 | except OSError: 44 | raise OSError(f"File {filename} not found or reading it failed") 45 | else: 46 | cell.assert_has_content() 47 | content = cell.content 48 | return content 49 | 50 | 51 | def get_highlights(cell, content, location, logger): 52 | # The code fragment is taken from CodeBlock directive almost unchanged: 53 | # https://github.com/sphinx-doc/sphinx/blob/0319faf8f1503453b6ce19020819a8cf44e39f13/sphinx/directives/code.py#L134-L148 54 | 55 | emphasize_linespec = cell.options.get("emphasize-lines") 56 | if emphasize_linespec: 57 | nlines = len(content) 58 | hl_lines = parselinenos(emphasize_linespec, nlines) 59 | if any(i >= nlines for i in hl_lines): 60 | logger.warning( 61 | "Line number spec is out of range(1-{}): {}".format(nlines, emphasize_linespec), 62 | location=location, 63 | ) 64 | hl_lines = [i + 1 for i in hl_lines if i < nlines] 65 | else: 66 | hl_lines = [] 67 | return hl_lines 68 | 69 | 70 | class JupyterCell(Directive): 71 | """Define a code cell to be later executed in a Jupyter kernel. 72 | 73 | The content of the directive is the code to execute. Code is not 74 | executed when the directive is parsed, but later during a doctree 75 | transformation. 76 | 77 | Arguments: 78 | --------- 79 | filename : str (optional) 80 | If provided, a path to a file containing code. 81 | 82 | Options 83 | ------- 84 | hide-code : bool 85 | If provided, the code will not be displayed in the output. 86 | hide-output : bool 87 | If provided, the cell output will not be displayed in the output. 88 | code-below : bool 89 | If provided, the code will be shown below the cell output. 90 | linenos : bool 91 | If provided, the code will be shown with line numbering. 92 | lineno-start: nonnegative int 93 | If provided, the code will be show with line numbering beginning from 94 | specified line. 95 | emphasize-lines : comma separated list of line numbers 96 | If provided, the specified lines will be highlighted. 97 | raises : comma separated list of exception types 98 | If provided, a comma-separated list of exception type names that 99 | the cell may raise. If one of the listed exception types is raised 100 | then the traceback is printed in place of the cell output. If an 101 | exception of another type is raised then we raise a RuntimeError 102 | when executing. 103 | 104 | Content 105 | ------- 106 | code : str 107 | A code cell. 108 | """ 109 | 110 | required_arguments = 0 111 | optional_arguments = 1 112 | final_argument_whitespace = True 113 | has_content = True 114 | 115 | option_spec = { 116 | "hide-code": directives.flag, 117 | "hide-output": directives.flag, 118 | "code-below": directives.flag, 119 | "linenos": directives.flag, 120 | "lineno-start": directives.nonnegative_int, 121 | "emphasize-lines": directives.unchanged_required, 122 | "raises": csv_option, 123 | "stderr": directives.flag, 124 | } 125 | 126 | def run(self): 127 | # This only works lazily because the logger is inited by Sphinx 128 | from . import logger 129 | 130 | location = self.state_machine.get_source_and_line(self.lineno) 131 | 132 | content = load_content(self, location, logger) 133 | 134 | try: 135 | hl_lines = get_highlights(self, content, location, logger) 136 | except ValueError as err: 137 | return [self.state.document.reporter.warning(err, line=self.lineno)] 138 | 139 | # A top-level placeholder for our cell 140 | cell_node = JupyterCellNode( 141 | execute=True, 142 | hide_code=("hide-code" in self.options), 143 | hide_output=("hide-output" in self.options), 144 | code_below=("code-below" in self.options), 145 | emphasize_lines=hl_lines, 146 | raises=self.options.get("raises"), 147 | stderr=("stderr" in self.options), 148 | classes=["jupyter_cell"], 149 | ) 150 | 151 | # Add the input section of the cell, we'll add output at execution time 152 | cell_input = CellInputNode(classes=["cell_input"]) 153 | cell_input += docutils.nodes.literal_block( 154 | text="\n".join(content), 155 | linenos=("linenos" in self.options), 156 | linenostart=(self.options.get("lineno-start")), 157 | ) 158 | cell_node += cell_input 159 | return [cell_node] 160 | 161 | 162 | class CellInput(Directive): 163 | """Define a code cell to be included verbatim but not executed. 164 | 165 | Arguments: 166 | --------- 167 | filename : str (optional) 168 | If provided, a path to a file containing code. 169 | 170 | Options 171 | ------- 172 | linenos : bool 173 | If provided, the code will be shown with line numbering. 174 | lineno-start: nonnegative int 175 | If provided, the code will be show with line numbering beginning from 176 | specified line. 177 | emphasize-lines : comma separated list of line numbers 178 | If provided, the specified lines will be highlighted. 179 | 180 | Content 181 | ------- 182 | code : str 183 | A code cell. 184 | """ 185 | 186 | required_arguments = 0 187 | optional_arguments = 1 188 | final_argument_whitespace = True 189 | has_content = True 190 | 191 | option_spec = { 192 | "linenos": directives.flag, 193 | "lineno-start": directives.nonnegative_int, 194 | "emphasize-lines": directives.unchanged_required, 195 | } 196 | 197 | def run(self): 198 | # This only works lazily because the logger is inited by Sphinx 199 | from . import logger 200 | 201 | location = self.state_machine.get_source_and_line(self.lineno) 202 | 203 | content = load_content(self, location, logger) 204 | 205 | try: 206 | hl_lines = get_highlights(self, content, location, logger) 207 | except ValueError as err: 208 | return [self.state.document.reporter.warning(err, line=self.lineno)] 209 | 210 | # A top-level placeholder for our cell 211 | cell_node = JupyterCellNode( 212 | execute=False, 213 | hide_code=False, 214 | hide_output=True, 215 | code_below=False, 216 | emphasize_lines=hl_lines, 217 | raises=False, 218 | stderr=False, 219 | classes=["jupyter_cell"], 220 | ) 221 | 222 | # Add the input section of the cell, we'll add output when jupyter-execute cells are run 223 | cell_input = CellInputNode(classes=["cell_input"]) 224 | cell_input += docutils.nodes.literal_block( 225 | text="\n".join(content), 226 | linenos=("linenos" in self.options), 227 | linenostart=(self.options.get("lineno-start")), 228 | ) 229 | cell_node += cell_input 230 | return [cell_node] 231 | 232 | 233 | class CellOutput(Directive): 234 | """Define an output cell to be included verbatim. 235 | 236 | Arguments: 237 | --------- 238 | filename : str (optional) 239 | If provided, a path to a file containing output. 240 | 241 | Content 242 | ------- 243 | code : str 244 | An output cell. 245 | """ 246 | 247 | required_arguments = 0 248 | optional_arguments = 1 249 | final_argument_whitespace = True 250 | has_content = True 251 | 252 | option_spec = {} 253 | 254 | def run(self): 255 | # This only works lazily because the logger is inited by Sphinx 256 | from . import logger 257 | 258 | location = self.state_machine.get_source_and_line(self.lineno) 259 | 260 | content = load_content(self, location, logger) 261 | 262 | # A top-level placeholder for our cell 263 | cell_node = JupyterCellNode( 264 | execute=False, 265 | hide_code=True, 266 | hide_output=False, 267 | code_below=False, 268 | emphasize_lines=[], 269 | raises=False, 270 | stderr=False, 271 | ) 272 | 273 | # Add a blank input and the given output to the cell 274 | cell_input = CellInputNode(classes=["cell_input"]) 275 | cell_input += docutils.nodes.literal_block( 276 | text="", 277 | linenos=False, 278 | linenostart=None, 279 | ) 280 | cell_node += cell_input 281 | content_str = "\n".join(content) 282 | cell_output = CellOutputNode(classes=["cell_output"]) 283 | cell_output += docutils.nodes.literal_block( 284 | text=content_str, 285 | rawsource=content_str, 286 | language="none", 287 | classes=["output", "stream"], 288 | ) 289 | cell_node += cell_output 290 | return [cell_node] 291 | 292 | 293 | class JupyterCellNode(docutils.nodes.container): 294 | """Inserted into doctree wherever a JupyterCell directive is encountered. 295 | 296 | Contains code that will be executed in a Jupyter kernel at a later 297 | doctree-transformation step. 298 | """ 299 | 300 | 301 | class CellInputNode(docutils.nodes.container): 302 | """Represent an input cell in the Sphinx AST.""" 303 | 304 | def __init__(self, rawsource="", *children, **attributes): 305 | super().__init__("", **attributes) 306 | 307 | 308 | class CellOutputNode(docutils.nodes.container): 309 | """Represent an output cell in the Sphinx AST.""" 310 | 311 | def __init__(self, rawsource="", *children, **attributes): 312 | super().__init__("", **attributes) 313 | 314 | 315 | class MimeBundleNode(docutils.nodes.container): 316 | """A node with multiple representations rendering as the highest priority one.""" 317 | 318 | def __init__(self, rawsource="", *children, **attributes): 319 | super().__init__("", *children, mimetypes=attributes["mimetypes"]) 320 | 321 | def render_as(self, visitor): 322 | """Determine which node to show based on the visitor.""" 323 | try: 324 | # Or should we go to config via the node? 325 | priority = visitor.builder.env.app.config["render_priority_" + visitor.builder.format] 326 | except (AttributeError, KeyError): 327 | # Not sure what do to, act as a container and show everything just in case. 328 | return super() 329 | for mimetype in priority: 330 | try: 331 | return self.children[self.attributes["mimetypes"].index(mimetype)] 332 | except ValueError: 333 | pass 334 | # Same 335 | return super() 336 | 337 | def walk(self, visitor): 338 | return self.render_as(visitor).walk(visitor) 339 | 340 | def walkabout(self, visitor): 341 | return self.render_as(visitor).walkabout(visitor) 342 | 343 | 344 | class JupyterKernelNode(docutils.nodes.Element): 345 | """Inserted into doctree whenever a JupyterKernel directive is encountered. 346 | 347 | Used as a marker to signal that the following JupyterCellNodes (until the 348 | next, if any, JupyterKernelNode) should be executed in a separate kernel. 349 | """ 350 | 351 | 352 | class JupyterWidgetViewNode(docutils.nodes.Element): 353 | """Inserted into doctree whenever a Jupyter cell produces a widget as output. 354 | 355 | Contains a unique ID for this widget; enough information for the widget 356 | embedding javascript to render it, given the widget state. For non-HTML 357 | outputs this doctree node is rendered generically. 358 | """ 359 | 360 | def __init__(self, rawsource="", *children, **attributes): 361 | super().__init__("", view_spec=attributes["view_spec"]) 362 | 363 | def html(self): 364 | return ipywidgets.embed.widget_view_template.format(view_spec=json.dumps(self["view_spec"])) 365 | 366 | 367 | class JupyterWidgetStateNode(docutils.nodes.Element): 368 | """Appended to doctree if any Jupyter cell produced a widget as output. 369 | 370 | Contains the state needed to render a collection of Jupyter widgets. 371 | 372 | Per doctree there is 1 JupyterWidgetStateNode per kernel that produced 373 | Jupyter widgets when running. This is fine as (presently) the 374 | 'html-manager' Javascript library, which embeds widgets, loads the state 375 | from all script tags on the page of the correct mimetype. 376 | """ 377 | 378 | def __init__(self, rawsource="", *children, **attributes): 379 | super().__init__("", state=attributes["state"]) 380 | 381 | def html(self): 382 | # escape to avoid early closing of the tag in the html page 383 | json_data = json.dumps(self["state"]).replace("", r"<\/script>") 384 | 385 | # TODO: render into a separate file if 'html-manager' starts fully 386 | # parsing script tags, and not just grabbing their innerHTML 387 | # https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed.ts#L36 388 | return ipywidgets.embed.snippet_template.format( 389 | load="", widget_views="", json_data=json_data 390 | ) 391 | 392 | 393 | def cell_output_to_nodes(outputs, write_stderr, out_dir, thebe_config, inline=False): 394 | """Convert a jupyter cell with outputs and filenames to doctree nodes. 395 | 396 | Parameters 397 | ---------- 398 | outputs : a list of outputs from a Jupyter cell 399 | write_stderr : bool 400 | If True include stderr in cell output 401 | out_dir : string 402 | Sphinx "absolute path" to the output folder, so it is a relative path 403 | to the source folder prefixed with ``/``. 404 | thebe_config: dict 405 | Thebelab configuration object or None 406 | inline: False 407 | Whether the nodes will be placed in-line with the text. 408 | 409 | Returns: 410 | ------- 411 | to_add : list of docutils nodes 412 | Each output, converted into a docutils node. 413 | """ 414 | # If we're in `inline` mode, ensure that we don't add block-level nodes 415 | literal_node = docutils.nodes.literal if inline else docutils.nodes.literal_block 416 | 417 | to_add = [] 418 | for output in outputs: 419 | output_type = output["output_type"] 420 | if output_type == "stream": 421 | if output["name"] == "stderr": 422 | if not write_stderr: 423 | continue 424 | else: 425 | # Output a container with an unhighlighted literal block for 426 | # `stderr` messages. 427 | # 428 | # Adds a "stderr" class that can be customized by the user for both 429 | # the container and the literal_block. 430 | # 431 | # Not setting "rawsource" disables Pygment highlighting, which 432 | # would otherwise add a
. 433 | 434 | literal = literal_node( 435 | text=output["text"], 436 | rawsource="", # disables Pygment highlighting 437 | language="none", 438 | classes=["stderr"], 439 | ) 440 | if inline: 441 | # In this case, we don't wrap the text in containers 442 | to_add.append(literal) 443 | else: 444 | container = docutils.nodes.container(classes=["stderr"]) 445 | container.append(literal) 446 | to_add.append(container) 447 | else: 448 | to_add.append( 449 | literal_node( 450 | text=output["text"], 451 | rawsource=output["text"], 452 | language="none", 453 | classes=["output", "stream"], 454 | ) 455 | ) 456 | elif output_type == "error": 457 | traceback = "\n".join(output["traceback"]) 458 | text = nbconvert.filters.strip_ansi(traceback) 459 | to_add.append( 460 | literal_node( 461 | text=text, 462 | rawsource=text, 463 | language="ipythontb", 464 | classes=["output", "traceback"], 465 | ) 466 | ) 467 | elif output_type in ("display_data", "execute_result"): 468 | children_by_mimetype = { 469 | mime_type: output2sphinx(data, mime_type, output["metadata"], out_dir) 470 | for mime_type, data in output["data"].items() 471 | } 472 | # Filter out unknown mimetypes 473 | # TODO: rewrite this using walrus once we depend on Python 3.8 474 | children_by_mimetype = { 475 | mime_type: node 476 | for mime_type, node in children_by_mimetype.items() 477 | if node is not None 478 | } 479 | to_add.append( 480 | MimeBundleNode( 481 | "", 482 | *list(children_by_mimetype.values()), 483 | mimetypes=list(children_by_mimetype.keys()), 484 | ) 485 | ) 486 | 487 | return to_add 488 | 489 | 490 | def output2sphinx(data, mime_type, metadata, out_dir, inline=False): 491 | """Convert a Jupyter output with a specific mimetype to its sphinx representation.""" 492 | # This only works lazily because the logger is inited by Sphinx 493 | from . import logger 494 | 495 | # If we're in `inline` mode, ensure that we don't add block-level nodes 496 | if inline: 497 | literal_node = docutils.nodes.literal 498 | math_node = docutils.nodes.math 499 | else: 500 | literal_node = docutils.nodes.literal_block 501 | math_node = math_block 502 | 503 | if mime_type == "text/html": 504 | return docutils.nodes.raw(text=data, format="html", classes=["output", "text_html"]) 505 | elif mime_type == "text/plain": 506 | return literal_node( 507 | text=data, 508 | rawsource=data, 509 | language="none", 510 | classes=["output", "text_plain"], 511 | ) 512 | elif mime_type == "text/latex": 513 | return math_node( 514 | text=strip_latex_delimiters(data), 515 | nowrap=False, 516 | number=None, 517 | classes=["output", "text_latex"], 518 | ) 519 | elif mime_type == "application/javascript": 520 | return docutils.nodes.raw( 521 | text=''.format( 522 | mime_type=mime_type, data=data 523 | ), 524 | format="html", 525 | ) 526 | elif mime_type == WIDGET_VIEW_MIMETYPE: 527 | return JupyterWidgetViewNode(view_spec=data) 528 | elif mime_type.startswith("image"): 529 | file_path = Path(metadata["filenames"][mime_type]) 530 | out_dir = Path(out_dir) 531 | # Sphinx treats absolute paths as being rooted at the source 532 | # directory, so make a relative path, which Sphinx treats 533 | # as being relative to the current working directory. 534 | filename = file_path.name 535 | 536 | if out_dir in file_path.parents: 537 | out_dir = file_path.parent 538 | 539 | uri = (out_dir / filename).as_posix() 540 | return docutils.nodes.image(uri=uri) 541 | else: 542 | logger.debug(f"Unknown mime type in cell output: {mime_type}") 543 | 544 | 545 | def apply_styling(node, thebe_config): 546 | """Change the cell node appearance, according to its settings.""" 547 | if not node.attributes["hide_code"]: # only add css if code is displayed 548 | classes = node.attributes.get("classes", []) 549 | classes += ["jupyter_container"] 550 | 551 | (input_node, output_node) = node.children 552 | if thebe_config: 553 | # Move the source from the input node into the thebe_source node 554 | source = input_node.children.pop(0) 555 | thebe_source = ThebeSourceNode( 556 | hide_code=node.attributes["hide_code"], 557 | code_below=node.attributes["code_below"], 558 | language=node.attributes["cm_language"], 559 | ) 560 | thebe_source.children = [source] 561 | input_node.children = [thebe_source] 562 | 563 | thebe_output = ThebeOutputNode() 564 | thebe_output.children = output_node.children 565 | output_node.children = [thebe_output] 566 | else: 567 | if node.attributes["hide_code"]: 568 | node.children.pop(0) 569 | 570 | if node.attributes["hide_output"]: 571 | output_node.children = [] 572 | 573 | # Swap inputs and outputs if we want the code below 574 | if node.attributes["code_below"]: 575 | node.children = node.children[::-1] 576 | 577 | 578 | class JupyterDownloadRole(ReferenceRole): 579 | def run(self): 580 | sep = ":" if ":" in self.name else "-" 581 | name, filetype = self.name.rsplit(sep, maxsplit=1) 582 | if sep == ":": 583 | warnings.warn( 584 | f"The {self.name} syntax is deprecated and " 585 | f"will be removed in 0.5.0, please use {name}-{filetype}", 586 | category=DeprecationWarning, 587 | ) 588 | 589 | assert filetype in ("notebook", "nb", "script") 590 | ext = ".ipynb" if filetype in ("notebook", "nb") else ".py" 591 | download_file = self.target + ext 592 | reftarget = sphinx_abs_dir(self.env, download_file) 593 | node = download_reference(self.rawtext, reftarget=reftarget) 594 | self.set_source_info(node) 595 | title = self.title if self.has_explicit_title else download_file 596 | node += literal(self.rawtext, title, classes=["xref", "download"]) 597 | return [node], [] 598 | 599 | 600 | def get_widgets(notebook): 601 | try: 602 | return notebook.metadata.widgets[WIDGET_STATE_MIMETYPE] 603 | except AttributeError: 604 | # Don't catch KeyError because it's a bug if 'widgets' does 605 | # not contain 'WIDGET_STATE_MIMETYPE' 606 | return None 607 | 608 | 609 | class CombineCellInputOutput(SphinxTransform): 610 | """Merge nodes from CellOutput with the preceding CellInput node.""" 611 | 612 | default_priority = 120 613 | 614 | def apply(self): 615 | moved_outputs = set() 616 | 617 | for cell_node in self.document.findall(JupyterCellNode): 618 | if not cell_node.attributes["execute"]: 619 | if not cell_node.attributes["hide_code"]: 620 | # Cell came from jupyter-input 621 | sibling = cell_node.next_node(descend=False, siblings=True) 622 | if ( 623 | isinstance(sibling, JupyterCellNode) 624 | and not sibling.attributes["execute"] 625 | and sibling.attributes["hide_code"] 626 | ): 627 | # Sibling came from jupyter-output, so we merge 628 | cell_node += sibling.children[1] 629 | cell_node.attributes["hide_output"] = False 630 | moved_outputs.update({sibling}) 631 | else: 632 | # Call came from jupyter-output 633 | if cell_node not in moved_outputs: 634 | raise ExtensionError( 635 | "Found a jupyter-output node without a preceding jupyter-input" 636 | ) 637 | 638 | for output_node in moved_outputs: 639 | output_node.replace_self([]) 640 | -------------------------------------------------------------------------------- /jupyter_sphinx/css/jupyter-sphinx.css: -------------------------------------------------------------------------------- 1 | /* Stylesheet for jupyter-sphinx 2 | 3 | These styles mimic the Jupyter HTML styles. 4 | 5 | The default CSS (Cascading Style Sheet) class structure of jupyter-sphinx 6 | is the following: 7 | 8 | jupyter_container 9 | code_cell (optional) 10 | stderr (optional) 11 | output (optional) 12 | 13 | If the code_cell is not displayed, then there is not a jupyter_container, and 14 | the output is provided without CSS. 15 | 16 | This stylesheet attempts to override the defaults of all packaged Sphinx themes 17 | to display jupter-sphinx cells in a Jupyter-like style. 18 | 19 | If you want to adjust the styles, add additional custom CSS to override these 20 | styles. 21 | 22 | After a build, this stylesheet is loaded from ./_static/jupyter-sphinx.css . 23 | 24 | */ 25 | 26 | div.jupyter_container { 27 | padding: 0.4em; 28 | margin: 0 0 0.4em 0; 29 | background-color: #ffff; 30 | border: 1px solid #ccc; 31 | -moz-box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2); 32 | -webkit-box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2); 33 | box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2); 34 | } 35 | .jupyter_container div.code_cell { 36 | border: 1px solid #cfcfcf; 37 | border-radius: 2px; 38 | background-color: #f7f7f7; 39 | margin: 0 0; 40 | overflow: auto; 41 | } 42 | 43 | .jupyter_container div.code_cell pre { 44 | padding: 4px; 45 | margin: 0 0; 46 | background-color: #f7f7f7; 47 | border: none; 48 | background: none; 49 | box-shadow: none; 50 | -webkit-box-shadow: none; /* for nature */ 51 | -moz-box-shadow: none; /* for nature */ 52 | } 53 | 54 | .jupyter_container div.code_cell * { 55 | margin: 0 0; 56 | } 57 | div.jupyter_container div.highlight { 58 | background-color: #f7f7f7; /* for haiku */ 59 | } 60 | div.jupyter_container { 61 | padding: 0; 62 | margin: 0; 63 | } 64 | 65 | /* Prevent alabaster breaking highlight alignment */ 66 | div.jupyter_container .hll { 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | /* overrides for sphinx_rtd_theme */ 72 | .rst-content .jupyter_container div[class^="highlight"], 73 | .document .jupyter_container div[class^="highlight"], 74 | .rst-content .jupyter_container pre.literal-block { 75 | border: none; 76 | margin: 0; 77 | padding: 0; 78 | background: none; 79 | padding: 3px; 80 | background-color: transparent; 81 | } 82 | /* restore Mathjax CSS, as it assumes a vertical margin. */ 83 | .jupyter_container .MathJax_Display { 84 | margin: 1em 0em; 85 | text-align: center; 86 | } 87 | .jupyter_container .stderr { 88 | background-color: #fcc; 89 | border: none; 90 | padding: 3px; 91 | } 92 | .jupyter_container .output { 93 | border: none; 94 | } 95 | .jupyter_container div.output pre { 96 | background-color: white; 97 | background: none; 98 | padding: 4px; 99 | border: none; 100 | box-shadow: none; 101 | -webkit-box-shadow: none; /* for nature */ 102 | -moz-box-shadow: none; /* for nature */ 103 | } 104 | .jupyter_container .code_cell td.linenos { 105 | text-align: right; 106 | padding: 4px 4px 4px 8px; 107 | border-right: 1px solid #cfcfcf; 108 | color: #999; 109 | } 110 | .jupyter_container .output .highlight { 111 | background-color: #ffffff; 112 | } 113 | /* combine sequential jupyter cells, 114 | by moving sequential ones up higher on y-axis */ 115 | div.jupyter_container + div.jupyter_container { 116 | margin: -0.5em 0 0.4em 0; 117 | } 118 | 119 | /* Fix for sphinx_rtd_theme spacing after jupyter_container #91 */ 120 | .rst-content .jupyter_container { 121 | margin: 0 0 24px 0; 122 | } 123 | -------------------------------------------------------------------------------- /jupyter_sphinx/execute.py: -------------------------------------------------------------------------------- 1 | """Execution and managing kernels.""" 2 | 3 | import os 4 | import warnings 5 | from logging import Logger 6 | from pathlib import Path 7 | 8 | import nbconvert 9 | from docutils.parsers.rst import Directive, directives 10 | from nbconvert.preprocessors import ExtractOutputPreprocessor 11 | from nbconvert.writers import FilesWriter 12 | from sphinx.errors import ExtensionError 13 | from sphinx.transforms import SphinxTransform 14 | 15 | if nbconvert.version_info < (6,): 16 | from nbconvert.preprocessors.execute import executenb 17 | else: 18 | from nbclient.client import execute as executenb 19 | 20 | import traitlets 21 | 22 | # Workaround of https://github.com/ipython/traitlets/issues/606 23 | if traitlets.version_info < (5, 1): 24 | 25 | class LoggerAdapterWrapper(Logger): 26 | """Wrap a logger adapter, while pretending to be a logger.""" 27 | 28 | def __init__(self, wrapped): 29 | self._wrapped = wrapped 30 | 31 | def __getattribute__(self, attr): 32 | if attr == "_wrapped": 33 | return object.__getattribute__(self, attr) 34 | return self._wrapped.__getattribute__(attr) 35 | 36 | else: 37 | 38 | def LoggerAdapterWrapper(logger_adapter): 39 | return logger_adapter 40 | 41 | 42 | import nbformat 43 | 44 | import jupyter_sphinx as js 45 | 46 | from .ast import ( 47 | CellOutputNode, 48 | JupyterCellNode, 49 | JupyterKernelNode, 50 | JupyterWidgetStateNode, 51 | apply_styling, 52 | cell_output_to_nodes, 53 | get_widgets, 54 | ) 55 | from .thebelab import ThebeButtonNode, add_thebelab_library 56 | from .utils import ( 57 | blank_nb, 58 | default_notebook_names, 59 | output_directory, 60 | sphinx_abs_dir, 61 | split_on, 62 | ) 63 | 64 | 65 | class JupyterKernel(Directive): 66 | """Specify a new Jupyter Kernel. 67 | 68 | Arguments: 69 | --------- 70 | kernel_name : str (optional) 71 | The name of the kernel in which to execute future Jupyter cells, as 72 | reported by executing 'jupyter kernelspec list' on the command line. 73 | 74 | Options 75 | ------- 76 | id : str 77 | An identifier for *this kernel instance*. Used to name any output 78 | files generated when executing the Jupyter cells (e.g. images 79 | produced by cells, or a script containing the cell inputs). 80 | 81 | Content 82 | ------- 83 | None 84 | """ 85 | 86 | optional_arguments = 1 87 | final_argument_whitespace = False 88 | has_content = False 89 | 90 | option_spec = {"id": directives.unchanged} 91 | 92 | def run(self): 93 | return [ 94 | JupyterKernelNode( 95 | "", 96 | kernel_name=self.arguments[0].strip() if self.arguments else "", 97 | kernel_id=self.options.get("id", "").strip(), 98 | ) 99 | ] 100 | 101 | 102 | # Doctree transformations 103 | class ExecuteJupyterCells(SphinxTransform): 104 | """Execute code cells in Jupyter kernels. 105 | 106 | Traverses the doctree to find JupyterKernel and JupyterCell nodes, 107 | then executes the code in the JupyterCell nodes in sequence, starting 108 | a new kernel every time a JupyterKernel node is encountered. The output 109 | from each code cell is inserted into the doctree. 110 | """ 111 | 112 | # Beginning of main transforms. Not 100% sure it's the correct time. 113 | default_priority = 400 114 | 115 | def apply(self): 116 | doctree = self.document 117 | docname_path = Path(self.env.docname) 118 | doc_dir_relpath = docname_path.parent # relative to src dir 119 | docname = docname_path.name 120 | default_kernel = self.config.jupyter_execute_default_kernel 121 | default_names = default_notebook_names(docname) 122 | thebe_config = self.config.jupyter_sphinx_thebelab_config 123 | linenos_config = self.config.jupyter_sphinx_linenos 124 | continue_linenos = self.config.jupyter_sphinx_continue_linenos 125 | # Check if we have anything to execute. 126 | if not next(doctree.findall(JupyterCellNode), False): 127 | return 128 | 129 | if thebe_config: 130 | # Add the button at the bottom if it is not present 131 | if not next(doctree.findall(ThebeButtonNode), False): 132 | doctree.append(ThebeButtonNode()) 133 | 134 | add_thebelab_library(doctree, self.env) 135 | 136 | js.logger.info(f"executing {docname}") 137 | output_dir = Path(output_directory(self.env)) / doc_dir_relpath 138 | 139 | # Start new notebook whenever a JupyterKernelNode is encountered 140 | jupyter_nodes = (JupyterCellNode, JupyterKernelNode) 141 | nodes_by_notebook = split_on( 142 | lambda n: isinstance(n, JupyterKernelNode), 143 | list(doctree.findall(lambda n: isinstance(n, jupyter_nodes))), 144 | ) 145 | 146 | for first, *nodes in nodes_by_notebook: 147 | if isinstance(first, JupyterKernelNode): 148 | kernel_name = first["kernel_name"] or default_kernel 149 | file_name = first["kernel_id"] or next(default_names) 150 | else: 151 | nodes = (first, *nodes) 152 | kernel_name = default_kernel 153 | file_name = next(default_names) 154 | 155 | # Add empty placeholder cells for non-executed nodes so nodes 156 | # and cells can be zipped and the provided input/output 157 | # can be inserted later 158 | notebook = execute_cells( 159 | kernel_name, 160 | [ 161 | nbformat.v4.new_code_cell(node.astext() if node["execute"] else "") 162 | for node in nodes 163 | ], 164 | self.config.jupyter_execute_kwargs, 165 | ) 166 | 167 | # Raise error if cells raised exceptions and were not marked as doing so 168 | for node, cell in zip(nodes, notebook.cells): 169 | errors = [output for output in cell.outputs if output["output_type"] == "error"] 170 | allowed_errors = node.attributes.get("raises") or [] 171 | raises_provided = node.attributes["raises"] is not None 172 | if raises_provided and not allowed_errors: # empty 'raises': suppress all errors 173 | pass 174 | elif errors and not any(e["ename"] in allowed_errors for e in errors): 175 | raise ExtensionError( 176 | "Cell raised uncaught exception:\n{}".format( 177 | "\n".join(errors[0]["traceback"]) 178 | ) 179 | ) 180 | 181 | # Raise error if cells print to stderr 182 | for node, cell in zip(nodes, notebook.cells): 183 | stderr = [ 184 | output 185 | for output in cell.outputs 186 | if output["output_type"] == "stream" and output["name"] == "stderr" 187 | ] 188 | if stderr and not node.attributes["stderr"]: 189 | js.logger.warning(f"Cell printed to stderr:\n{stderr[0]['text']}") 190 | 191 | # Insert input/output into placeholders for non-executed cells 192 | for node, cell in zip(nodes, notebook.cells): 193 | if not node["execute"]: 194 | cell.source = node.children[0].astext() 195 | if len(node.children) == 2: 196 | output = nbformat.v4.new_output("stream") 197 | output.text = node.children[1].astext() 198 | cell.outputs = [output] 199 | node.children.pop() 200 | 201 | try: 202 | lexer = notebook.metadata.language_info.pygments_lexer 203 | except AttributeError: 204 | lexer = notebook.metadata.kernelspec.language 205 | 206 | # Highlight the code cells now that we know what language they are 207 | for node in nodes: 208 | source = node.children[0].children[0] 209 | source.attributes["language"] = lexer 210 | 211 | # Add line numbering 212 | 213 | linenostart = 1 214 | 215 | for node in nodes: 216 | # The literal_block node with the source 217 | source = node.children[0].children[0] 218 | nlines = source.rawsource.count("\n") + 1 219 | show_numbering = linenos_config or source["linenos"] or source["linenostart"] 220 | 221 | if show_numbering: 222 | source["linenos"] = True 223 | if source["linenostart"]: 224 | linenostart = source["linenostart"] 225 | if source["linenostart"] or continue_linenos: 226 | source["highlight_args"] = {"linenostart": linenostart} 227 | else: 228 | linenostart = 1 229 | linenostart += nlines 230 | 231 | hl_lines = node["emphasize_lines"] 232 | if hl_lines: 233 | highlight_args = source.setdefault("highlight_args", {}) 234 | highlight_args["hl_lines"] = hl_lines 235 | 236 | # Add code cell CSS class 237 | for node in nodes: 238 | source = node.children[0] 239 | source.attributes["classes"].append("code_cell") 240 | 241 | # Write certain cell outputs (e.g. images) to separate files, and 242 | # modify the metadata of the associated cells in 'notebook' to 243 | # include the path to the output file. 244 | write_notebook_output(notebook, str(output_dir), file_name, self.env.docname) 245 | 246 | try: 247 | cm_language = notebook.metadata.language_info.codemirror_mode.name 248 | except AttributeError: 249 | cm_language = notebook.metadata.kernelspec.language 250 | for node in nodes: 251 | node.attributes["cm_language"] = cm_language 252 | 253 | # Add doctree nodes for cell outputs. 254 | for node, cell in zip(nodes, notebook.cells): 255 | # Add the outputs as children 256 | output = CellOutputNode(classes=["cell_output"]) 257 | output.children = cell_output_to_nodes( 258 | cell.outputs, 259 | bool(node.attributes["stderr"]), 260 | sphinx_abs_dir(self.env), 261 | thebe_config, 262 | ) 263 | node += output 264 | 265 | apply_styling(node, thebe_config) 266 | 267 | if contains_widgets(notebook): 268 | doctree.append(JupyterWidgetStateNode(state=get_widgets(notebook))) 269 | 270 | 271 | # Roles 272 | 273 | 274 | def execute_cells(kernel_name, cells, execute_kwargs): 275 | """Execute Jupyter cells in the specified kernel and return the notebook.""" 276 | notebook = blank_nb(kernel_name) 277 | notebook.cells = cells 278 | # Modifies 'notebook' in-place 279 | try: 280 | executenb(notebook, **execute_kwargs) 281 | except Exception as e: 282 | raise ExtensionError("Notebook execution failed", orig_exc=e) 283 | 284 | return notebook 285 | 286 | 287 | def write_notebook_output(notebook, output_dir, notebook_name, location=None): 288 | """Extract output from notebook cells and write to files in output_dir. 289 | 290 | This also modifies 'notebook' in-place, adding metadata to each cell that 291 | maps output mime-types to the filenames the output was saved under. 292 | """ 293 | resources = dict(unique_key=os.path.join(output_dir, notebook_name), outputs={}) 294 | 295 | # Modifies 'resources' in-place 296 | ExtractOutputPreprocessor().preprocess(notebook, resources) 297 | # Write the cell outputs to files where we can (images and PDFs), 298 | # as well as the notebook file. 299 | FilesWriter(build_directory=output_dir).write( 300 | nbformat.writes(notebook), 301 | resources, 302 | os.path.join(output_dir, notebook_name + ".ipynb"), 303 | ) 304 | 305 | exporter = nbconvert.exporters.ScriptExporter(log=LoggerAdapterWrapper(js.logger)) 306 | with warnings.catch_warnings(): 307 | # See https://github.com/jupyter/nbconvert/issues/1388 308 | warnings.simplefilter("ignore", DeprecationWarning) 309 | contents, resources = exporter.from_notebook_node(notebook) 310 | 311 | notebook_file = notebook_name + resources["output_extension"] 312 | output_dir = Path(output_dir) 313 | # utf-8 is the de-facto standard encoding for notebooks. 314 | (output_dir / notebook_file).write_text(contents, encoding="utf8") 315 | 316 | 317 | def contains_widgets(notebook): 318 | widgets = get_widgets(notebook) 319 | return widgets and widgets["state"] 320 | -------------------------------------------------------------------------------- /jupyter_sphinx/thebelab.py: -------------------------------------------------------------------------------- 1 | """Inserting interactive links with Thebelab.""" 2 | import json 3 | from pathlib import Path 4 | 5 | import docutils 6 | from docutils.parsers.rst import Directive 7 | 8 | import jupyter_sphinx as js 9 | 10 | 11 | class ThebeSourceNode(docutils.nodes.container): 12 | """Container that holds the cell source when thebelab is enabled.""" 13 | 14 | def __init__(self, rawsource="", *children, **attributes): 15 | super().__init__("", **attributes) 16 | 17 | def visit_html(self): 18 | code_class = "thebelab-code" 19 | if self["hide_code"]: 20 | code_class += " thebelab-hidden" 21 | if self["code_below"]: 22 | code_class += " thebelab-below" 23 | language = self["language"] 24 | return '
'.format( 25 | code_class, language 26 | ) 27 | 28 | def depart_html(self): 29 | return "
" 30 | 31 | 32 | class ThebeOutputNode(docutils.nodes.container): 33 | """Container that holds all the output nodes when thebelab is enabled.""" 34 | 35 | def visit_html(self): 36 | return '
' 37 | 38 | def depart_html(self): 39 | return "
" 40 | 41 | 42 | class ThebeButtonNode(docutils.nodes.Element): 43 | """Appended to the doctree by the ThebeButton directive. 44 | 45 | Renders as a button to enable thebelab on the page. 46 | 47 | If no ThebeButton directive is found in the document but thebelab 48 | is enabled, the node is added at the bottom of the document. 49 | """ 50 | 51 | def __init__(self, rawsource="", *children, text="Make live", **attributes): 52 | super().__init__("", text=text) 53 | 54 | def html(self): 55 | text = self["text"] 56 | return ( 57 | ''.format(text=text) 60 | ) 61 | 62 | 63 | class ThebeButton(Directive): 64 | """Specify a button to activate thebelab on the page. 65 | 66 | Arguments: 67 | --------- 68 | text : str (optional) 69 | If provided, the button text to display 70 | 71 | Content 72 | ------- 73 | None 74 | """ 75 | 76 | optional_arguments = 1 77 | final_argument_whitespace = True 78 | has_content = False 79 | 80 | def run(self): 81 | kwargs = {"text": self.arguments[0]} if self.arguments else {} 82 | return [ThebeButtonNode(**kwargs)] 83 | 84 | 85 | def add_thebelab_library(doctree, env): 86 | """Adds the thebelab configuration and library to the doctree.""" 87 | thebe_config = env.config.jupyter_sphinx_thebelab_config 88 | if isinstance(thebe_config, dict): 89 | pass 90 | elif isinstance(thebe_config, str): 91 | thebe_config = Path(thebe_config) 92 | if thebe_config.is_absolute(): 93 | filename = thebe_config 94 | else: 95 | filename = Path(env.app.srcdir).resolve() / thebe_config 96 | 97 | if not filename.exists(): 98 | js.logger.warning("The supplied thebelab configuration file does not exist") 99 | return 100 | 101 | try: 102 | thebe_config = json.loads(filename.read_text()) 103 | except ValueError: 104 | js.logger.warning("The supplied thebelab configuration file is not in JSON format.") 105 | return 106 | else: 107 | js.logger.warning( 108 | "The supplied thebelab configuration should be either" " a filename or a dictionary." 109 | ) 110 | return 111 | 112 | # Force config values to make thebelab work correctly 113 | thebe_config["predefinedOutput"] = True 114 | thebe_config["requestKernel"] = True 115 | 116 | # Specify the thebelab config inline, a separate file is not supported 117 | doctree.append( 118 | docutils.nodes.raw( 119 | text='\n'.format( 120 | json.dumps(thebe_config) 121 | ), 122 | format="html", 123 | ) 124 | ) 125 | 126 | # Add thebelab library after the config is specified 127 | doctree.append( 128 | docutils.nodes.raw( 129 | text='\n'.format( 130 | env.config.jupyter_sphinx_thebelab_url 131 | ), 132 | format="html", 133 | ) 134 | ) 135 | -------------------------------------------------------------------------------- /jupyter_sphinx/thebelab/thebelab-helper.js: -------------------------------------------------------------------------------- 1 | function initThebelab() { 2 | let activateButton = document.getElementById("thebelab-activate-button"); 3 | if (activateButton.classList.contains("thebelab-active")) { 4 | return; 5 | } 6 | 7 | // Place all outputs below the source where this was not the case 8 | // to make them recognizable by thebelab 9 | let codeBelows = document.getElementsByClassName("thebelab-below"); 10 | for (var i = 0; i < codeBelows.length; i++) { 11 | let prev = codeBelows[i]; 12 | // Find previous sibling element, compatible with IE8 13 | do prev = prev.previousSibling; 14 | while (prev && prev.nodeType !== 1); 15 | swapSibling(prev, codeBelows[i]); 16 | } 17 | 18 | thebelab.bootstrap(); 19 | activateButton.classList.add("thebelab-active"); 20 | } 21 | 22 | function swapSibling(node1, node2) { 23 | node1.parentNode.replaceChild(node1, node2); 24 | node1.parentNode.insertBefore(node2, node1); 25 | } 26 | -------------------------------------------------------------------------------- /jupyter_sphinx/thebelab/thebelab.css: -------------------------------------------------------------------------------- 1 | .thebelab-cell .thebelab-input pre { 2 | z-index: 0; 3 | } 4 | 5 | .thebelab-hidden { 6 | display: none; 7 | } 8 | 9 | .thebelab-button { 10 | position: relative; 11 | display: inline-block; 12 | box-sizing: border-box; 13 | border: none; 14 | border-radius: 0.1rem; 15 | padding: 0 2rem; 16 | margin: 0.5rem 0.1rem; 17 | min-width: 64px; 18 | height: 1.6rem; 19 | vertical-align: middle; 20 | text-align: center; 21 | font-size: 0.8rem; 22 | color: rgba(0, 0, 0, 0.8); 23 | background-color: rgba(0, 0, 0, 0.07); 24 | overflow: hidden; 25 | outline: none; 26 | cursor: pointer; 27 | transition: background-color 0.2s; 28 | } 29 | 30 | .thebelab-button:hover { 31 | background-color: rgba(0, 0, 0, 0.12); 32 | } 33 | 34 | .thebelab-button:active { 35 | background-color: rgba(0, 0, 0, 0.15); 36 | color: rgba(0, 0, 0, 1); 37 | } 38 | -------------------------------------------------------------------------------- /jupyter_sphinx/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions and helpers.""" 2 | import os 3 | from itertools import count, groupby 4 | from pathlib import Path 5 | 6 | import nbformat 7 | from jupyter_client.kernelspec import NoSuchKernel, get_kernel_spec 8 | from sphinx.errors import ExtensionError 9 | 10 | 11 | def blank_nb(kernel_name): 12 | try: 13 | spec = get_kernel_spec(kernel_name) 14 | except NoSuchKernel as e: 15 | raise ExtensionError("Unable to find kernel", orig_exc=e) 16 | return nbformat.v4.new_notebook( 17 | metadata={ 18 | "kernelspec": { 19 | "display_name": spec.display_name, 20 | "language": spec.language, 21 | "name": kernel_name, 22 | } 23 | } 24 | ) 25 | 26 | 27 | def split_on(pred, it): 28 | """Split an iterator wherever a predicate is True.""" 29 | counter = 0 30 | 31 | def count(x): 32 | nonlocal counter 33 | if pred(x): 34 | counter += 1 35 | return counter 36 | 37 | # Return iterable of lists to ensure that we don't lose our 38 | # place in the iterator 39 | return (list(x) for _, x in groupby(it, count)) 40 | 41 | 42 | def strip_latex_delimiters(source): 43 | r"""Remove LaTeX math delimiters that would be rendered by the math block. 44 | 45 | These are: ``\(…\)``, ``\[…\]``, ``$…$``, and ``$$…$$``. 46 | This is necessary because sphinx does not have a dedicated role for 47 | generic LaTeX, while Jupyter only defines generic LaTeX output, see 48 | https://github.com/jupyter/jupyter-sphinx/issues/90 for discussion. 49 | """ 50 | source = source.strip() 51 | delimiter_pairs = (pair.split() for pair in r"\( \),\[ \],$$ $$,$ $".split(",")) 52 | for start, end in delimiter_pairs: 53 | if source.startswith(start) and source.endswith(end): 54 | return source[len(start) : -len(end)] 55 | 56 | return source 57 | 58 | 59 | def default_notebook_names(basename): 60 | """Return an iterator yielding notebook names based off 'basename'.""" 61 | yield basename 62 | for i in count(1): 63 | yield "_".join((basename, str(i))) 64 | 65 | 66 | def language_info(executor): 67 | # Can only run this function inside 'setup_preprocessor' 68 | assert hasattr(executor, "kc") 69 | info_msg = executor._wait_for_reply(executor.kc.kernel_info()) 70 | return info_msg["content"]["language_info"] 71 | 72 | 73 | def sphinx_abs_dir(env, *paths): 74 | # We write the output files into 75 | # output_directory / jupyter_execute / path relative to source directory 76 | # Sphinx expects download links relative to source file or relative to 77 | # source dir and prepended with '/'. We use the latter option. 78 | out_path = (output_directory(env) / Path(env.docname).parent / Path(*paths)).resolve() 79 | 80 | if os.name == "nt": 81 | # Can't get relative path between drives on Windows 82 | return out_path.as_posix() 83 | 84 | # Path().relative_to() doesn't work when not a direct subpath 85 | return "/" + os.path.relpath(out_path, env.app.srcdir) 86 | 87 | 88 | def output_directory(env): 89 | # Put output images inside the sphinx build directory to avoid 90 | # polluting the current working directory. We don't use a 91 | # temporary directory, as sphinx may cache the doctree with 92 | # references to the images that we write 93 | 94 | # Note: we are using an implicit fact that sphinx output directories are 95 | # direct subfolders of the build directory. 96 | return (Path(env.app.outdir) / os.path.pardir / "jupyter_execute").resolve() 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyter-sphinx" 7 | dynamic = ["version"] 8 | description = "Jupyter Sphinx Extensions" 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: Science/Research", 13 | "License :: OSI Approved :: BSD License", 14 | "Operating System :: OS Independent", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Topic :: Scientific/Engineering" 24 | ] 25 | dependencies = [ 26 | "ipykernel>=4.5.1", 27 | "IPython", 28 | "ipywidgets>=7.0.0", 29 | "nbconvert>=5.5", 30 | "nbformat", 31 | "Sphinx>=7", 32 | ] 33 | 34 | [[project.authors]] 35 | name = "Jupyter Development Team" 36 | email = "jupyter@googlegroups.com" 37 | 38 | [project.license] 39 | file = "LICENSE" 40 | 41 | [project.readme] 42 | file = "README.md" 43 | content-type = "text/markdown" 44 | 45 | [project.urls] 46 | "Bug Tracker" = "https://github.com/jupyter/jupyter-sphinx/issues/" 47 | Documentation = "https://jupyter-sphinx.readthedocs.io" 48 | Homepage = "https://jupyter.org" 49 | "Source Code" = "https://github.com/jupyter/jupyter-sphinx/" 50 | 51 | [project.optional-dependencies] 52 | test = [ 53 | "pytest", 54 | "bash_kernel", 55 | "pytest-regressions", 56 | "beautifulsoup4", 57 | "matplotlib", 58 | ] 59 | doc = [ 60 | "sphinx-design", 61 | "sphinx-book-theme", 62 | "matplotlib" 63 | ] 64 | 65 | [tool.hatch.version] 66 | path = "jupyter_sphinx/_version.py" 67 | 68 | [tool.hatch.build.targets.sdist] 69 | include = [ 70 | "/jupyter_sphinx", 71 | ] 72 | 73 | [tool.hatch.envs.lint] 74 | detached = true 75 | dependencies = ["pre-commit"] 76 | [tool.hatch.envs.lint.scripts] 77 | build = "pre-commit run --all-files" 78 | 79 | [tool.hatch.envs.doc] 80 | features = ["doc"] 81 | [tool.hatch.envs.doc.scripts] 82 | build = "sphinx-build -v -b html docs docs/_build/html" 83 | 84 | [tool.hatch.envs.test] 85 | features = ["test"] 86 | [tool.hatch.envs.test.env-vars] 87 | JUPYTER_PLATFORM_DIRS = "1" 88 | [tool.hatch.envs.test.scripts] 89 | test = ["python -m bash_kernel.install", "python -m pytest -vv {args}"] 90 | nowarn = "test -W default {args}" 91 | 92 | [tool.pytest.ini_options] 93 | minversion = "7.0" 94 | xfail_strict = true 95 | log_cli_level = "info" 96 | addopts = [ 97 | "-ra", "--durations=10", "--color=yes", "--strict-config", "--strict-markers" 98 | ] 99 | testpaths = ["tests/"] 100 | filterwarnings = [ 101 | "error", 102 | # https://github.com/dateutil/dateutil/issues/1314 103 | "module:datetime.datetime.utc:DeprecationWarning" 104 | ] 105 | 106 | [tool.ruff] 107 | ignore-init-module-imports = true 108 | fix = true 109 | select = ["E", "F", "W", "I", "D", "RUF"] 110 | ignore = [ 111 | "E501", # line too long | Black take care of it 112 | "D212", # Multi-line docstring 113 | "D100", # Missing docstring in public module 114 | "D101", # Missing docstring in public class 115 | "D102", # Missing docstring in public method 116 | "D103", # Missing docstring in public function 117 | "D105", # Missing docstring in magic method 118 | "D107", # Missing docstring in `__init__` 119 | ] 120 | 121 | [tool.ruff.flake8-quotes] 122 | docstring-quotes = "double" 123 | 124 | [tool.ruff.pydocstyle] 125 | convention = "google" 126 | 127 | [tool.black] 128 | line-length = 100 129 | force-exclude = "tests/test_execute/*" 130 | 131 | [tool.doc8] 132 | ignore = [ 133 | "D001" # we follow a 1 line = 1 paragraph style 134 | ] 135 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Global configuration of the test session.""" 2 | import asyncio 3 | import os 4 | import shutil 5 | import sys 6 | import tempfile 7 | from io import StringIO 8 | from pathlib import Path 9 | from typing import Callable, List, Tuple, Union 10 | 11 | import pytest 12 | import sphinx 13 | from bs4 import BeautifulSoup 14 | from sphinx.testing.util import SphinxTestApp 15 | 16 | if os.name == "nt": 17 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 18 | 19 | 20 | @pytest.fixture() 21 | def doctree(): 22 | source_trees = [] 23 | apps = [] 24 | syspath = sys.path[:] 25 | 26 | def doctree( 27 | source, 28 | config=None, 29 | return_all=False, 30 | entrypoint="jupyter_sphinx", 31 | buildername="html", 32 | ): 33 | src_dir = Path(tempfile.mkdtemp()) 34 | source_trees.append(src_dir) 35 | 36 | conf_contents = "extensions = ['%s']" % entrypoint 37 | if config is not None: 38 | conf_contents += "\n" + config 39 | (src_dir / "conf.py").write_text(conf_contents, encoding="utf8") 40 | (src_dir / "index.rst").write_text(source, encoding="utf8") 41 | 42 | warnings = StringIO() 43 | app = SphinxTestApp( 44 | srcdir=src_dir, 45 | status=StringIO(), 46 | warning=warnings, 47 | buildername=buildername, 48 | ) 49 | apps.append(app) 50 | app.build() 51 | 52 | doctree = app.env.get_and_resolve_doctree("index", app.builder) 53 | if return_all: 54 | return doctree, app, warnings.getvalue() 55 | else: 56 | return doctree 57 | 58 | yield doctree 59 | 60 | sys.path[:] = syspath 61 | [app.cleanup() for app in reversed(apps)] 62 | [shutil.rmtree(tree) for tree in source_trees] 63 | 64 | 65 | class SphinxBuild: 66 | """Helper class to build a test documentation.""" 67 | 68 | def __init__(self, app: SphinxTestApp, src: Path): 69 | self.app = app 70 | self.src = src 71 | 72 | def build(self, no_warning: bool = False): 73 | """Build the application.""" 74 | self.app.build() 75 | if no_warning is True: 76 | assert self.warnings == "", self.status 77 | return self 78 | 79 | @property 80 | def status(self) -> str: 81 | """Returns the status of the current build.""" 82 | return self.app._status.getvalue() 83 | 84 | @property 85 | def warnings(self) -> str: 86 | """Returns the warnings raised by the current build.""" 87 | return self.app._warning.getvalue() 88 | 89 | @property 90 | def outdir(self) -> Path: 91 | """Returns the output directory of the current build.""" 92 | return Path(self.app.outdir) 93 | 94 | @property 95 | def index_html(self) -> BeautifulSoup: 96 | """Returns the html tree of the current build.""" 97 | path_page = self.outdir.joinpath("index.html") 98 | return BeautifulSoup(path_page.read_text("utf8"), "html.parser") 99 | 100 | 101 | @pytest.fixture() 102 | def sphinx_build_factory(tmp_path: Path) -> Callable: 103 | """Return a factory builder.""" 104 | 105 | def _func( 106 | source, 107 | config: str = "", 108 | entrypoint: str = "jupyter_sphinx", 109 | buildername: str = "html", 110 | ) -> SphinxBuild: 111 | """Create the Sphinxbuild from the source folder.""" 112 | src_dir = tmp_path 113 | conf_contents = f"extensions = ['{entrypoint}']" 114 | conf_contents += "\n" + config 115 | (src_dir / "conf.py").write_text(conf_contents, encoding="utf8") 116 | (src_dir / "index.rst").write_text(source, encoding="utf8") 117 | 118 | # api inconsistency from sphinx 119 | if sphinx.version_info < (7, 2): 120 | from sphinx.testing.path import path as sphinx_path 121 | 122 | src_dir = sphinx_path(src_dir) 123 | 124 | app = SphinxTestApp(srcdir=src_dir, buildername=buildername) 125 | 126 | return SphinxBuild(app, tmp_path) 127 | 128 | return _func 129 | 130 | 131 | @pytest.fixture(scope="session") 132 | def directive() -> Callable: 133 | """A function to build the directive string.""" 134 | 135 | def _func( 136 | type: str, 137 | code: List[str], 138 | options: List[Union[str, Tuple]] = [], 139 | parameter: str = "", 140 | ) -> str: 141 | """Return the formatted string of the required directive. 142 | 143 | Args: 144 | type: the type of directive to build, one of [execute, input, output, kernel] 145 | code: the list of code instructions to write in the cell 146 | options: the list of options of the directive if option requires a parameter, use a tuple 147 | parameter: The parameter of the directive (written on the first line) 148 | """ 149 | # parse all the options as tuple 150 | options = [(o, "") if isinstance(o, str) else o for o in options] 151 | 152 | # create the output string 153 | s = f".. jupyter-{type}:: {parameter}" 154 | s += "".join([f"\n\t:{o[0]}: {o[1]}" for o in options]) 155 | s += "\n" 156 | s += "".join([f"\n\t{c}" for c in code]) 157 | s += "\n" 158 | 159 | return s 160 | 161 | return _func 162 | -------------------------------------------------------------------------------- /tests/test_execute.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import warnings 5 | from datetime import date 6 | 7 | import pytest 8 | from sphinx.errors import ExtensionError 9 | 10 | 11 | @pytest.mark.parametrize("buildername", ["html", "singlehtml"]) 12 | def test_basic(sphinx_build_factory, directive, file_regression, buildername): 13 | source = directive("execute", ["2 + 2"]) 14 | 15 | sphinx_build = sphinx_build_factory(source, buildername=buildername).build() 16 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 17 | file_regression.check(html.prettify(), extension=".html") 18 | 19 | 20 | def test_hide_output(sphinx_build_factory, directive, file_regression): 21 | source = directive("execute", ["2 + 2"], ["hide-output"]) 22 | 23 | sphinx_build = sphinx_build_factory(source).build() 24 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 25 | file_regression.check(html.prettify(), extension=".html") 26 | 27 | 28 | def test_hide_code(sphinx_build_factory, directive, file_regression): 29 | source = directive("execute", ["2 + 2"], ["hide-code"]) 30 | 31 | sphinx_build = sphinx_build_factory(source).build() 32 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 33 | file_regression.check(html.prettify(), extension=".html") 34 | 35 | 36 | def test_code_below(sphinx_build_factory, directive, file_regression): 37 | source = directive("execute", ["2 + 2"], ["code-below"]) 38 | 39 | sphinx_build = sphinx_build_factory(source).build() 40 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 41 | file_regression.check(html.prettify(), extension=".html") 42 | 43 | 44 | def test_linenos(sphinx_build_factory, directive, file_regression): 45 | source = directive("execute", ["2 + 2"], ["linenos"]) 46 | 47 | sphinx_build = sphinx_build_factory(source).build() 48 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 49 | file_regression.check(html.prettify(), extension=".html") 50 | 51 | 52 | def test_linenos_code_below(sphinx_build_factory, directive, file_regression): 53 | source = directive("execute", ["2 + 2"], ["linenos", "code-below"]) 54 | 55 | sphinx_build = sphinx_build_factory(source).build() 56 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 57 | file_regression.check(html.prettify(), extension=".html") 58 | 59 | 60 | def test_linenos_conf_option(sphinx_build_factory, directive, file_regression): 61 | source = directive("execute", ["2 + 2"]) 62 | config = "jupyter_sphinx_linenos = True" 63 | 64 | sphinx_build = sphinx_build_factory(source, config=config).build() 65 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 66 | file_regression.check(html.prettify(), extension=".html") 67 | 68 | 69 | def test_continue_linenos_not_automatic(sphinx_build_factory, directive, file_regression): 70 | source = directive("execute", ["2 + 2"]) 71 | config = "jupyter_sphinx_continue_linenos = True" 72 | 73 | sphinx_build = sphinx_build_factory(source, config=config).build() 74 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 75 | file_regression.check(html.prettify(), extension=".html") 76 | 77 | 78 | def test_continue_lineos_conf_option(sphinx_build_factory, directive, file_regression): 79 | source = directive("execute", ["2 + 2"]) 80 | source += "\n" + directive("execute", ["3 + 3"]) 81 | 82 | config = "jupyter_sphinx_linenos = True" 83 | config += "\n" + "jupyter_sphinx_continue_linenos = True" 84 | 85 | sphinx_build = sphinx_build_factory(source, config=config).build() 86 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 87 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 88 | 89 | 90 | def test_continue_linenos_with_start(sphinx_build_factory, directive, file_regression): 91 | source = directive("execute", ["2 + 2"], [("lineno-start", "7")]) 92 | source += "\n" + directive("execute", ["3 + 3"]) 93 | 94 | config = "jupyter_sphinx_linenos = True" 95 | config += "\n" + "jupyter_sphinx_continue_linenos = True" 96 | 97 | sphinx_build = sphinx_build_factory(source, config=config).build() 98 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 99 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 100 | 101 | 102 | def test_emphasize_lines(sphinx_build_factory, directive, file_regression): 103 | source = directive("execute", [f"{i} + {i}" for i in range(1, 6)], [("emphasize-lines", "2,4")]) 104 | 105 | sphinx_build = sphinx_build_factory(source).build() 106 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 107 | file_regression.check(html.prettify(), extension=".html") 108 | 109 | 110 | def test_emphasize_lines_with_dash(sphinx_build_factory, directive, file_regression): 111 | source = directive( 112 | "execute", [f"{i} + {i}" for i in range(1, 6)], [("emphasize-lines", "2,3-5")] 113 | ) 114 | 115 | sphinx_build = sphinx_build_factory(source).build() 116 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 117 | file_regression.check(html.prettify(), extension=".html") 118 | 119 | 120 | def test_execution_environment_carries_over(sphinx_build_factory, directive, file_regression): 121 | source = directive("execute", ["a = 1"]) 122 | source += "\n" + directive("execute", ["a += 1", "a"]) 123 | 124 | sphinx_build = sphinx_build_factory(source).build() 125 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 126 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 127 | 128 | 129 | def test_kernel_restart(sphinx_build_factory, directive, file_regression): 130 | source = directive("execute", ["a = 1"]) 131 | source += "\n" + directive("kernel", [], [("id", "new-kernel")]) 132 | source += "\n" + directive("execute", ["a += 1", "a"], ["raises"]) 133 | 134 | sphinx_build = sphinx_build_factory(source).build() 135 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 136 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 137 | 138 | 139 | def test_raises(sphinx_build_factory, directive): 140 | source = directive("execute", ["raise ValueError()"]) 141 | 142 | with pytest.raises(ExtensionError): 143 | sphinx_build_factory(source).build() 144 | 145 | 146 | def test_raises_incell(sphinx_build_factory, directive, file_regression): 147 | source = directive("execute", ["raise ValueError()"], ["raises"]) 148 | 149 | sphinx_build = sphinx_build_factory(source).build() 150 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 151 | file_regression.check(html.prettify(), extension=".html") 152 | 153 | 154 | def test_raises_specific_error_incell(sphinx_build_factory, directive, file_regression): 155 | source = directive("execute", ["raise ValueError()"], [("raises", "ValueError")]) 156 | 157 | sphinx_build = sphinx_build_factory(source).build() 158 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 159 | file_regression.check(html.prettify(), extension=".html") 160 | 161 | 162 | def test_widgets(sphinx_build_factory, directive, file_regression): 163 | source = directive("execute", ["import ipywidgets", "ipywidgets.Button()"]) 164 | 165 | sphinx_build = sphinx_build_factory(source).build() 166 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 167 | 168 | # replace model_id value as it changes every time the test suit is run 169 | script = json.loads(html.find("script").string) 170 | script["model_id"] = "toto" 171 | html.find("script").string = json.dumps(script) 172 | file_regression.check(html.prettify(), extension=".html") 173 | 174 | 175 | def test_javascript(sphinx_build_factory, directive, file_regression): 176 | source = directive( 177 | "execute", 178 | [ 179 | "from IPython.display import Javascript", 180 | "Javascript('window.alert(\"Hello there!\")')", 181 | ], 182 | ) 183 | 184 | sphinx_build = sphinx_build_factory(source).build() 185 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 186 | file_regression.check(html.prettify(), extension=".html") 187 | 188 | 189 | def test_stdout(sphinx_build_factory, directive, file_regression): 190 | source = directive("execute", ["print('Hello there!')"]) 191 | 192 | sphinx_build = sphinx_build_factory(source).build() 193 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 194 | file_regression.check(html.prettify(), extension=".html") 195 | 196 | 197 | def test_stderr_hidden(sphinx_build_factory, directive, file_regression): 198 | source = directive("execute", ["import sys", "print('Hello there!', file=sys.stderr)"]) 199 | 200 | sphinx_build = sphinx_build_factory(source).build() 201 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 202 | file_regression.check(html.prettify(), extension=".html") 203 | 204 | 205 | def test_stderr(sphinx_build_factory, directive, file_regression): 206 | source = directive( 207 | "execute", ["import sys", "print('Hello there!', file=sys.stderr)"], ["stderr"] 208 | ) 209 | 210 | sphinx_build = sphinx_build_factory(source).build() 211 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 212 | file_regression.check(html.prettify(), extension=".html") 213 | 214 | 215 | def test_thebe_hide_output(sphinx_build_factory, directive, file_regression): 216 | source = directive("execute", ["2 +2"], ["hide-output"]) 217 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 218 | 219 | sphinx_build = sphinx_build_factory(source, config=config).build() 220 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 221 | file_regression.check(html.prettify(), extension=".html") 222 | 223 | 224 | def test_thebe_hide_code(sphinx_build_factory, directive, file_regression): 225 | source = directive("execute", ["2 + 2"], ["hide-code"]) 226 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 227 | 228 | sphinx_build = sphinx_build_factory(source, config=config).build() 229 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 230 | file_regression.check(html.prettify(), extension=".html") 231 | 232 | 233 | def test_thebe_code_below(sphinx_build_factory, directive, file_regression): 234 | source = directive("execute", ["2 + 2"], ["code-below"]) 235 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 236 | 237 | sphinx_build = sphinx_build_factory(source, config=config).build() 238 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 239 | file_regression.check(html.prettify(), extension=".html") 240 | 241 | 242 | def test_thebe_button_auto(sphinx_build_factory, directive, file_regression): 243 | source = directive("execute", ["1 + 1"]) 244 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 245 | 246 | sphinx_build = sphinx_build_factory(source, config=config).build() 247 | # the button should fall after the cell i.e. index == 1 248 | html = sphinx_build.index_html.select("div.jupyter_cell,button.thebelab-button")[1] 249 | file_regression.check(html.prettify(), extension=".html") 250 | 251 | 252 | def test_thebe_button_manual(sphinx_build_factory, directive, file_regression): 253 | source = ".. thebe-button::" 254 | source += "\n" + directive("execute", ["1 + 1"]) 255 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 256 | 257 | sphinx_build = sphinx_build_factory(source, config=config).build() 258 | # the button should fall before the cell i.e. index == 0 259 | html = sphinx_build.index_html.select("div.jupyter_cell,button.thebelab-button")[0] 260 | file_regression.check(html.prettify(), extension=".html") 261 | 262 | 263 | def test_thebe_button_none(sphinx_build_factory, directive): 264 | source = "No Jupyter cells" 265 | config = 'jupyter_sphinx_thebelab_config = {"dummy": True}' 266 | 267 | sphinx_build = sphinx_build_factory(source, config=config).build() 268 | html = sphinx_build.index_html.select("button.thebelab-button") 269 | assert len(list(html)) == 0 270 | 271 | 272 | def test_latex(sphinx_build_factory, directive, file_regression): 273 | source = directive("execute", ["from IPython.display import Latex", r"Latex(r'$$\int$$')"]) 274 | 275 | sphinx_build = sphinx_build_factory(source).build() 276 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 277 | file_regression.check(html.prettify(), extension=".html") 278 | 279 | 280 | @pytest.mark.xfail 281 | def test_cell_output_to_nodes(sphinx_build_factory, directive, file_regression): 282 | source = directive("execute", ["import matplotlib.pyplot as plt", "plt.plot([1, 2], [1, 4])"]) 283 | 284 | sphinx_build = sphinx_build_factory(source).build() 285 | 286 | # workaround to rename the trace ID as it's changed for each session 287 | # it's currently not working even though it's the same code as in test_widgets 288 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 289 | text = r"[<matplotlib.lines.Line2D at toto>]" 290 | html.find(string=re.compile(r".*matplotlib\.lines\.Line2D.*")).string = text 291 | file_regression.check(html.prettify(), extension=".html") 292 | 293 | 294 | @pytest.mark.parametrize("type", ["script", "notebook", "nb"]) 295 | def test_jupyter_download(sphinx_build_factory, file_regression, type): 296 | source = f"This is a script: :jupyter-download-{type}:`a file `" 297 | 298 | sphinx_build = sphinx_build_factory(source).build() 299 | html = sphinx_build.index_html.select("div.body")[0] 300 | file_regression.check(html.prettify(), extension=".html") 301 | 302 | 303 | def test_save_script(sphinx_build_factory, directive, file_regression): 304 | source = directive("kernel", [], [("id", "test")], "python3") 305 | source += "\n" + directive("execute", ["a = 1", "print(a)", ""]) 306 | 307 | sphinx_build = sphinx_build_factory(source).build() 308 | saved_text = (sphinx_build.outdir / "../jupyter_execute/test.py").read_text() 309 | file_regression.check(saved_text, extension=".py") 310 | 311 | 312 | @pytest.mark.skipif(os.name == "nt", reason="No bash test on windows") 313 | def test_bash_kernel(sphinx_build_factory, directive, file_regression): 314 | source = directive("kernel", [], [("id", "test")], "bash") 315 | source += "\n" + directive("execute", ['echo "foo"']) 316 | 317 | # See https://github.com/takluyver/bash_kernel/issues/105 318 | with warnings.catch_warnings(): 319 | warnings.simplefilter("ignore", DeprecationWarning) 320 | sphinx_build = sphinx_build_factory(source).build() 321 | 322 | saved_text = (sphinx_build.outdir / "../jupyter_execute/test.sh").read_text() 323 | file_regression.check(saved_text, extension=".sh") 324 | 325 | 326 | def test_input_cell(sphinx_build_factory, directive, file_regression): 327 | source = directive("input", ("2 + 2")) 328 | 329 | sphinx_build = sphinx_build_factory(source).build() 330 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 331 | file_regression.check(html.prettify(), extension=".html") 332 | 333 | 334 | def test_input_cell_linenos(sphinx_build_factory, directive, file_regression): 335 | source = directive("input", ["2 + 2"], ["linenos"]) 336 | 337 | sphinx_build = sphinx_build_factory(source).build() 338 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 339 | file_regression.check(html.prettify(), extension=".html") 340 | 341 | 342 | def test_output_cell(sphinx_build_factory, directive, file_regression): 343 | source = directive("input", ["3 + 2"]) 344 | source += "\n" + directive("output", ["4"]) 345 | 346 | sphinx_build = sphinx_build_factory(source).build() 347 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 348 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 349 | 350 | 351 | def test_output_only_error(sphinx_build_factory, directive): 352 | source = directive("output", ["4"]) 353 | 354 | with pytest.raises(ExtensionError): 355 | sphinx_build_factory(source).build() 356 | 357 | 358 | def test_multiple_directives_types(sphinx_build_factory, directive, file_regression): 359 | source = directive("execute", ["2 + 2"]) 360 | source += "\n" + directive("input", ["3 + 3"]) 361 | source += "\n" + directive("output", ["6"]) 362 | 363 | sphinx_build = sphinx_build_factory(source).build() 364 | htmls = sphinx_build.index_html.select("div.jupyter_cell") 365 | file_regression.check("\n".join([e.prettify() for e in htmls]), extension=".html") 366 | 367 | 368 | def test_builder_priority_html(sphinx_build_factory, directive, file_regression): 369 | source = directive( 370 | "execute", 371 | ['display({"text/plain": "I am html output", "text/latex": "I am latex"})'], 372 | ) 373 | config = "render_priority_html = ['text/plain', 'text/latex']" 374 | 375 | sphinx_build = sphinx_build_factory(source, config=config).build() 376 | html = sphinx_build.index_html.select("div.jupyter_cell")[0] 377 | file_regression.check(html.prettify(), extension=".html") 378 | 379 | 380 | def test_builder_priority_latex(sphinx_build_factory, directive, file_regression): 381 | source = directive( 382 | "execute", 383 | ['display({"text/plain": "I am html output", "text/latex": "I am latex"})'], 384 | ) 385 | "render_priority_latex = ['text/latex', 'text/plain']" 386 | 387 | sphinx_build = sphinx_build_factory(source, buildername="latex").build() 388 | latex = (sphinx_build.outdir / "python.tex").read_text() 389 | # workaround to remove the date line from the output (It will change for every build) 390 | latex = latex.replace(f"\\date{date.today().strftime('{%b %d, %Y}')}\n", "") 391 | file_regression.check(latex, extension=".tex") 392 | -------------------------------------------------------------------------------- /tests/test_execute/test_bash_kernel.sh: -------------------------------------------------------------------------------- 1 | echo "foo" 2 | -------------------------------------------------------------------------------- /tests/test_execute/test_basic_html_.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_basic_singlehtml_.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_builder_priority_html.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
display({"text/plain": "I am html output", "text/latex": "I am latex"})
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{'text/plain': 'I am html output', 'text/latex': 'I am latex'}
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_builder_priority_latex.tex: -------------------------------------------------------------------------------- 1 | %% Generated by Sphinx. 2 | \def\sphinxdocclass{report} 3 | \documentclass[letterpaper,10pt,english]{sphinxmanual} 4 | \ifdefined\pdfpxdimen 5 | \let\sphinxpxdimen\pdfpxdimen\else\newdimen\sphinxpxdimen 6 | \fi \sphinxpxdimen=.75bp\relax 7 | \ifdefined\pdfimageresolution 8 | \pdfimageresolution= \numexpr \dimexpr1in\relax/\sphinxpxdimen\relax 9 | \fi 10 | %% let collapsible pdf bookmarks panel have high depth per default 11 | \PassOptionsToPackage{bookmarksdepth=5}{hyperref} 12 | 13 | \PassOptionsToPackage{booktabs}{sphinx} 14 | \PassOptionsToPackage{colorrows}{sphinx} 15 | 16 | \PassOptionsToPackage{warn}{textcomp} 17 | \usepackage[utf8]{inputenc} 18 | \ifdefined\DeclareUnicodeCharacter 19 | % support both utf8 and utf8x syntaxes 20 | \ifdefined\DeclareUnicodeCharacterAsOptional 21 | \def\sphinxDUC#1{\DeclareUnicodeCharacter{"#1}} 22 | \else 23 | \let\sphinxDUC\DeclareUnicodeCharacter 24 | \fi 25 | \sphinxDUC{00A0}{\nobreakspace} 26 | \sphinxDUC{2500}{\sphinxunichar{2500}} 27 | \sphinxDUC{2502}{\sphinxunichar{2502}} 28 | \sphinxDUC{2514}{\sphinxunichar{2514}} 29 | \sphinxDUC{251C}{\sphinxunichar{251C}} 30 | \sphinxDUC{2572}{\textbackslash} 31 | \fi 32 | \usepackage{cmap} 33 | \usepackage[T1]{fontenc} 34 | \usepackage{amsmath,amssymb,amstext} 35 | \usepackage{babel} 36 | 37 | 38 | 39 | \usepackage{tgtermes} 40 | \usepackage{tgheros} 41 | \renewcommand{\ttdefault}{txtt} 42 | 43 | 44 | 45 | \usepackage[Bjarne]{fncychap} 46 | \usepackage{sphinx} 47 | 48 | \fvset{fontsize=auto} 49 | \usepackage{geometry} 50 | 51 | 52 | % Include hyperref last. 53 | \usepackage{hyperref} 54 | % Fix anchor placement for figures with captions. 55 | \usepackage{hypcap}% it must be loaded after hyperref. 56 | % Set up styles of URL: it should be placed after hyperref. 57 | \urlstyle{same} 58 | 59 | 60 | \usepackage{sphinxmessages} 61 | 62 | 63 | 64 | 65 | \title{Python} 66 | \release{} 67 | \author{unknown} 68 | \newcommand{\sphinxlogo}{\vbox{}} 69 | \renewcommand{\releasename}{} 70 | \makeindex 71 | \begin{document} 72 | 73 | \ifdefined\shorthandoff 74 | \ifnum\catcode`\=\string=\active\shorthandoff{=}\fi 75 | \ifnum\catcode`\"=\active\shorthandoff{"}\fi 76 | \fi 77 | 78 | \pagestyle{empty} 79 | \sphinxmaketitle 80 | \pagestyle{plain} 81 | \sphinxtableofcontents 82 | \pagestyle{normal} 83 | \phantomsection\label{\detokenize{index::doc}} 84 | \begin{sphinxuseclass}{jupyter_cell} 85 | \begin{sphinxuseclass}{jupyter_container} 86 | \begin{sphinxuseclass}{cell_input} 87 | \begin{sphinxuseclass}{code_cell} 88 | \begin{sphinxVerbatim}[commandchars=\\\{\}] 89 | \PYG{n}{display}\PYG{p}{(}\PYG{p}{\PYGZob{}}\PYG{l+s+s2}{\PYGZdq{}}\PYG{l+s+s2}{text/plain}\PYG{l+s+s2}{\PYGZdq{}}\PYG{p}{:} \PYG{l+s+s2}{\PYGZdq{}}\PYG{l+s+s2}{I am html output}\PYG{l+s+s2}{\PYGZdq{}}\PYG{p}{,} \PYG{l+s+s2}{\PYGZdq{}}\PYG{l+s+s2}{text/latex}\PYG{l+s+s2}{\PYGZdq{}}\PYG{p}{:} \PYG{l+s+s2}{\PYGZdq{}}\PYG{l+s+s2}{I am latex}\PYG{l+s+s2}{\PYGZdq{}}\PYG{p}{\PYGZcb{}}\PYG{p}{)} 90 | \end{sphinxVerbatim} 91 | 92 | \end{sphinxuseclass} 93 | \end{sphinxuseclass} 94 | \begin{sphinxuseclass}{cell_output} 95 | \begin{sphinxVerbatim}[commandchars=\\\{\}] 96 | \PYGZob{}\PYGZsq{}text/plain\PYGZsq{}: \PYGZsq{}I am html output\PYGZsq{}, \PYGZsq{}text/latex\PYGZsq{}: \PYGZsq{}I am latex\PYGZsq{}\PYGZcb{} 97 | \end{sphinxVerbatim} 98 | 99 | \end{sphinxuseclass} 100 | \end{sphinxuseclass} 101 | \end{sphinxuseclass} 102 | 103 | 104 | \renewcommand{\indexname}{Index} 105 | \printindex 106 | \end{document} -------------------------------------------------------------------------------- /tests/test_execute/test_cell_output_to_nodes.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
import matplotlib.pyplot as plt
 6 | plt.plot([1, 2], [1, 4])
 7 | 
8 |
9 |
10 |
11 |
12 |
13 |
14 |
[<matplotlib.lines.Line2D at 0x7f18113d7bb0>]
15 | 
16 |
17 |
18 | _images/index_0_1.png 19 |
20 |
21 | -------------------------------------------------------------------------------- /tests/test_execute/test_code_below.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
4
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
2 + 2
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_continue_linenos_not_automatic.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_continue_linenos_with_start.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
72 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
83 + 3
25 | 
26 |
27 |
28 |
29 |
30 |
31 |
32 |
6
33 | 
34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /tests/test_execute/test_continue_lineos_conf_option.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
12 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
23 + 3
25 | 
26 |
27 |
28 |
29 |
30 |
31 |
32 |
6
33 | 
34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /tests/test_execute/test_emphasize_lines.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
1 + 1
 6 | 2 + 2
 7 | 3 + 3
 8 | 4 + 4
 9 | 5 + 5
10 | 
11 |
12 |
13 |
14 |
15 |
16 |
17 |
10
18 | 
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /tests/test_execute/test_emphasize_lines_with_dash.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
1 + 1
 6 | 2 + 2
 7 | 3 + 3
 8 | 4 + 4
 9 | 5 + 5
10 | 
11 |
12 |
13 |
14 |
15 |
16 |
17 |
10
18 | 
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /tests/test_execute/test_execution_environment_carries_over.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
a = 1
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
a += 1
19 | a
20 | 
21 |
22 |
23 |
24 |
25 |
26 |
27 |
2
28 | 
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /tests/test_execute/test_hide_code.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
4
 6 | 
7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tests/test_execute/test_hide_output.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /tests/test_execute/test_input_cell.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2
 6 | 
 7 | +
 8 | 
 9 | 2
10 | 
11 |
12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /tests/test_execute/test_input_cell_linenos.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
12 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /tests/test_execute/test_javascript.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
from IPython.display import Javascript
 6 | Javascript('window.alert("Hello there!")')
 7 | 
8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /tests/test_execute/test_jupyter_download_nb_.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | This is a script: 4 | 5 | 6 | a 7 | 8 | 9 | file 10 | 11 | 12 |

13 |
14 | -------------------------------------------------------------------------------- /tests/test_execute/test_jupyter_download_notebook_.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | This is a script: 4 | 5 | 6 | a 7 | 8 | 9 | file 10 | 11 | 12 |

13 |
14 | -------------------------------------------------------------------------------- /tests/test_execute/test_jupyter_download_script_.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | This is a script: 4 | 5 | 6 | a 7 | 8 | 9 | file 10 | 11 | 12 |

13 |
14 | -------------------------------------------------------------------------------- /tests/test_execute/test_kernel_restart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
a = 1
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
a += 1
19 | a
20 | 
21 |
22 |
23 |
24 |
25 |
26 |
27 |
---------------------------------------------------------------------------
28 | NameError                                 Traceback (most recent call last)
29 | Cell In[1], line 1
30 | ----> 1 a += 1
31 |       2 a
32 | 
33 | NameError: name 'a' is not defined
34 | 
35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /tests/test_execute/test_latex.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
from IPython.display import Latex
 6 | Latex(r'$$\int$$')
 7 | 
8 |
9 |
10 |
11 |
12 |
13 | \[\int\] 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /tests/test_execute/test_linenos.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
12 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_linenos_code_below.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
4
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
12 + 2
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_linenos_conf_option.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
12 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_multiple_directives_types.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
2 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
3 + 3
25 | 
26 |
27 |
28 |
29 |
30 |
31 |
32 |
6
33 | 
34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /tests/test_execute/test_output_cell.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
3 + 2
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
4
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_raises_incell.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
raise ValueError()
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
---------------------------------------------------------------------------
14 | ValueError                                Traceback (most recent call last)
15 | Cell In[1], line 1
16 | ----> 1 raise ValueError()
17 | 
18 | ValueError: 
19 | 
20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /tests/test_execute/test_raises_specific_error_incell.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
raise ValueError()
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
---------------------------------------------------------------------------
14 | ValueError                                Traceback (most recent call last)
15 | Cell In[1], line 1
16 | ----> 1 raise ValueError()
17 | 
18 | ValueError: 
19 | 
20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /tests/test_execute/test_save_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # In[1]: 5 | 6 | 7 | a = 1 8 | print(a) 9 | 10 | -------------------------------------------------------------------------------- /tests/test_execute/test_stderr.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
import sys
 6 | print('Hello there!', file=sys.stderr)
 7 | 
8 |
9 |
10 |
11 |
12 |
13 |
Hello there!
14 | 
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /tests/test_execute/test_stderr_hidden.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
import sys
 6 | print('Hello there!', file=sys.stderr)
 7 | 
8 |
9 |
10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /tests/test_execute/test_stdout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
print('Hello there!')
 6 | 
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Hello there!
14 | 
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/test_execute/test_thebe_button_auto.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/test_execute/test_thebe_button_manual.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/test_execute/test_thebe_code_below.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
4
 8 | 
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
2 + 2
20 | 
21 |
22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /tests/test_execute/test_thebe_hide_code.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
2 + 2
 8 | 
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
4
20 | 
21 |
22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /tests/test_execute/test_thebe_hide_output.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
2 +2
 8 | 
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /tests/test_execute/test_widgets.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
import ipywidgets
 6 | ipywidgets.Button()
 7 | 
8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | --------------------------------------------------------------------------------