pax_global_header 0000666 0000000 0000000 00000000064 14543263320 0014514 g ustar 00root root 0000000 0000000 52 comment=0d6b26c677334dbbd0f4d1935f50dcf3888c82c1
jupyter-sphinx-0.5.3/ 0000775 0000000 0000000 00000000000 14543263320 0014532 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/.github/ 0000775 0000000 0000000 00000000000 14543263320 0016072 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/.github/dependabot.yml 0000664 0000000 0000000 00000000353 14543263320 0020723 0 ustar 00root root 0000000 0000000 version: 2
updates:
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Python
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
jupyter-sphinx-0.5.3/.github/workflows/ 0000775 0000000 0000000 00000000000 14543263320 0020127 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/.github/workflows/prerelease.yml 0000664 0000000 0000000 00000002200 14543263320 0022773 0 ustar 00root root 0000000 0000000 name: Scheduled pre-release tests
on:
# Run this workflow once a week (https://crontab.guru/#0_5_*_*_1)
schedule:
- cron: "0 5 * * 1"
workflow_dispatch:
# env variable to force pip to install pre-released versions
# in hatch envs
env:
PIP_PRE: 1
jobs:
tests:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", " 3.12"]
os: [ubuntu-latest]
include:
- os: windows-latest
python-version: "3.12"
- os: macos-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install hatch
- name: Run tests
run: hatch run test:test -x
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install dependencies
run: pip install hatch
- name: Build docs
run: hatch run doc:build
jupyter-sphinx-0.5.3/.github/workflows/publish.yml 0000664 0000000 0000000 00000001556 14543263320 0022327 0 ustar 00root root 0000000 0000000 # This will run every time a tag is created and pushed to the repository.
# It calls our tests workflow via a `workflow_call`, and if tests pass
# then it triggers our upload to PyPI for a new release.
name: Publish to PyPI
on:
release:
types: ["published"]
jobs:
tests:
uses: ./.github/workflows/tests.yml
publish:
needs: [tests]
name: Publish to PyPi
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Set up Python "3.10"
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: install dependencies
run: pip install build
- name: Build package
run: python -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.8.11
with:
user: __token__
password: ${{ secrets.PYPI_KEY }}
jupyter-sphinx-0.5.3/.github/workflows/tests.yml 0000664 0000000 0000000 00000002267 14543263320 0022023 0 ustar 00root root 0000000 0000000 name: tests
on:
push:
branches: ["main"]
pull_request:
schedule:
- cron: "0 8 * * *"
workflow_call:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: pre-commit/action@v3.0.0
tests:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", " 3.12"]
os: [ubuntu-latest]
include:
- os: windows-latest
python-version: "3.12"
- os: macos-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install hatch
run: pip install hatch
- name: Run tests
run: hatch run test:test -x
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install hatch
run: pip install hatch
- name: Build docs
run: hatch run doc:build
jupyter-sphinx-0.5.3/.gitignore 0000664 0000000 0000000 00000000327 14543263320 0016524 0 ustar 00root root 0000000 0000000 .venv
*.py[cod]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__
# Jupyter notebook checkpoints
.ipynb_checkpoints/
# OS X
.DS_Store
# git
*.orig
jupyter-sphinx-0.5.3/.pre-commit-config.yaml 0000664 0000000 0000000 00000003651 14543263320 0021020 0 ustar 00root root 0000000 0000000 ci:
autoupdate_schedule: monthly
autoupdate_commit_msg: "chore: update pre-commit hooks"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-case-conflict
- id: check-ast
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.27.1
hooks:
- id: check-github-workflows
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.17
hooks:
- id: mdformat
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.3"
hooks:
- id: prettier
types_or: [yaml, html, json]
- repo: https://github.com/adamchainz/blacken-docs
rev: "1.16.0"
hooks:
- id: blacken-docs
additional_dependencies: [black==23.7.0]
exclude: |
(?x)^(
doc/source/index.rst|
tests/test_execute.py
)$(|)
- repo: https://github.com/codespell-project/codespell
rev: "v2.2.6"
hooks:
- id: codespell
args: ["-L", "sur,nd"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: "v1.10.0"
hooks:
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.5
hooks:
- id: ruff
types_or: [python, jupyter]
args: ["--fix", "--show-fixes"]
- id: ruff-format
types_or: [python, jupyter]
- repo: https://github.com/scientific-python/cookie
rev: "2023.10.27"
hooks:
- id: sp-repo-review
additional_dependencies: ["repo-review[cli]"]
jupyter-sphinx-0.5.3/.readthedocs.yml 0000664 0000000 0000000 00000000357 14543263320 0017625 0 ustar 00root root 0000000 0000000 version: 2
build:
os: ubuntu-22.04
tools:
python: "3.8"
sphinx:
configuration: doc/source/conf.py
python:
install:
# install itself with pip install .
- method: pip
path: .
extra_requirements:
- doc
jupyter-sphinx-0.5.3/CONTRIBUTING.md 0000664 0000000 0000000 00000006637 14543263320 0016777 0 ustar 00root root 0000000 0000000 # General Jupyter contributor guidelines
If you're reading this section, you're probably interested in
contributing to Jupyter. Welcome and thanks for your interest in
contributing!
Please take a look at the Contributor documentation, familiarize
yourself with using the Jupyter Server, and introduce yourself on the
mailing list and share what area of the project you are interested in
working on.
For general documentation about contributing to Jupyter projects, see
the [Project Jupyter Contributor
Documentation](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html).
# Setting Up a Development Environment
## Installing the Jupyter Server
The development version of the server requires
[node](https://nodejs.org/en/download/) and
[pip](https://pip.pypa.io/en/stable/installing/).
Once you have installed the dependencies mentioned above, use the
following steps:
```
pip install --upgrade pip
git clone https://github.com/jupyter/jupyter-sphinx
cd jupyter-server
pip install -e ".[test]"
```
## Code Styling and Quality Checks
`jupyter-sphinx` has adopted automatic code formatting so you shouldn't
need to worry too much about your code style. As long as your code is
valid, the pre-commit hook should take care of how it should look.
`pre-commit` and its associated hooks will automatically be installed
when you run `pip install -e ".[test]"`
To install `pre-commit` hook manually, run the following:
```
pre-commit install
```
You can invoke the pre-commit hook by hand at any time with:
```
pre-commit run
```
which should run any autoformatting on your code and tell you about any
errors it couldn't fix automatically. You may also install [black
integration](https://github.com/psf/black#editor-integration) into your
text editor to format code automatically.
If you have already committed files before setting up the pre-commit
hook with `pre-commit install`, you can fix everything up using
`pre-commit run --all-files`. You need to make the fixing commit
yourself after that.
Some of the hooks only run on CI by default, but you can invoke them by
running with the `--hook-stage manual` argument.
There are three hatch scripts that can be run locally as well:
`hatch run lint:build` will enforce styling.
# Running Tests
Install dependencies:
```
pip install -e .[test]
```
To run the Python tests, use:
```
pytest
```
You can also run the tests using `hatch` without installing test
dependencies in your local environment:
```
pip install hatch
hatch run test:test
```
The command takes any argument that you can give to `pytest`, e.g.:
```
hatch run test:test -k name_of_method_to_test
```
You can also drop into a shell in the test environment by running:
```
hatch -e test shell
```
# Building the Docs
Install the docs requirements using `pip`:
```
pip install .[doc]
```
Once you have installed the required packages, you can build the docs
with:
```
cd docs
make html
```
You can also run the tests using `hatch` without installing test
dependencies in your local environment.
```bash
pip install hatch
hatch run docs:build
```
You can also drop into a shell in the docs environment by running:
```
hatch -e docs shell
```
After that, the generated HTML files will be available at
`build/html/index.html`. You may view the docs in your browser.
You should also have a look at the [Project Jupyter Documentation
Guide](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html).
jupyter-sphinx-0.5.3/LICENSE 0000664 0000000 0000000 00000003055 14543263320 0015542 0 ustar 00root root 0000000 0000000 Copyright (c) 2015-2016, Brian E. Granger and Jake Vanderplas
Copyright (c) 2016-2019, Project Jupyter Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
jupyter-sphinx-0.5.3/README.md 0000664 0000000 0000000 00000001307 14543263320 0016012 0 ustar 00root root 0000000 0000000 # Jupyter Sphinx Extensions
`jupyter-sphinx` enables running code embedded in Sphinx documentation and
embedding output of that code into the resulting document. It has support
for rich output such as images and even Jupyter interactive widgets.
## Installation
With pip:
```bash
pip install jupyter_sphinx
```
with conda:
```bash
conda install jupyter_sphinx -c conda-forge
```
## Usage
You can check out the documentation on https://jupyter-sphinx.readthedocs.io for up to date
usage information and examples.
## License
We use a shared copyright model that enables all contributors to maintain the
copyright on their contributions.
All code is licensed under the terms of the revised BSD license.
jupyter-sphinx-0.5.3/RELEASE.md 0000664 0000000 0000000 00000002703 14543263320 0016136 0 ustar 00root root 0000000 0000000 # Release instructions for jupyter-sphinx
Jupyter Sphinx uses a GitHub action to automatically push a new release to
PyPI when a GitHub release is added.
To cut a new Jupyter Sphinx release, follow these steps:
- Ensure that all tests are passing on master.
- In [`_version.py`](https://github.com/jupyter/jupyter-sphinx/blob/main/jupyter_sphinx/_version.py),
change the "release type" section to "final" e.g.:
```python
version_info = (0, 2, 3, "final")
```
- Make a release commit and push to main
```
git add jupyter_sphinx/_version.py
git commit -m "RLS: 0.2.3"
git push upstream main
```
- [Create a new github release](https://github.com/jupyter/jupyter-sphinx/releases/new).
The target should be **main**, the tag and the title should be the version number,
e.g. `0.2.3`.
- Creating the release in GitHub will push a tag commit to the repository, which will
trigger [a GitHub action](https://github.com/jupyter/jupyter-sphinx/blob/main/.github/workflows/artifacts.yml)
to build `jupyter-sphinx` and push the new version to PyPI.
[Confirm that the version has been bumped](https://pypi.org/project/jupyter-sphinx/).
- In [`_version.py`](https://github.com/jupyter/jupyter-sphinx/blob/main/jupyter_sphinx/_version.py),
bump the minor version and change the "release type" section to "alpha". **make sure to
include a number after the release type**, e.g.:
```python
version_info = (0, 2, 4, "alpha", 1)
```
- That's it!
jupyter-sphinx-0.5.3/doc/ 0000775 0000000 0000000 00000000000 14543263320 0015277 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/doc/Makefile 0000664 0000000 0000000 00000001354 14543263320 0016742 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# raise warnings to errors
html-strict:
@$(SPHINXBUILD) -b html -nW --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
clean:
rm -r $(BUILDDIR)
jupyter-sphinx-0.5.3/doc/source/ 0000775 0000000 0000000 00000000000 14543263320 0016577 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/doc/source/conf.py 0000664 0000000 0000000 00000001703 14543263320 0020077 0 ustar 00root root 0000000 0000000 #
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
import os
import sys
sys.path.insert(0, os.path.abspath("../.."))
import jupyter_sphinx # noqa: E402
project = "Jupyter Sphinx"
copyright = "2019, Jupyter Development Team"
author = "Jupyter Development Team"
# The full version, including alpha/beta/rc tags
release = jupyter_sphinx.__version__
# The short X.Y version
version = release[: len(release) - len(release.lstrip("0123456789."))].rstrip(".")
master_doc = "index"
extensions = ["sphinx.ext.mathjax", "jupyter_sphinx"]
html_theme = "alabaster"
html_theme_options = {
"github_user": "jupyter",
"github_repo": "jupyter-sphinx",
"github_banner": True,
}
jupyter_sphinx_thebelab_config = {"binderOptions": {"repo": "jupyter/jupyter-sphinx"}}
latex_engine = "xelatex"
jupyter-sphinx-0.5.3/doc/source/index.rst 0000664 0000000 0000000 00000032117 14543263320 0020444 0 ustar 00root root 0000000 0000000 Jupyter Sphinx Extension
========================
.. toctree::
:maxdepth: 2
:caption: Contents:
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.
Installation
------------
Get jupyter-sphinx from pip:
.. code-block:: bash
pip install jupyter-sphinx
or conda:
.. code-block:: bash
conda install jupyter_sphinx -c conda-forge
Enabling the extension
----------------------
To enable the extension, add ``jupyter_sphinx`` to your enabled extensions in
``conf.py``:
.. code-block:: python
extensions = [
'jupyter_sphinx',
]
Basic Usage
-----------
You can use the ``jupyter-execute`` directive to embed code into the document::
.. jupyter-execute::
name = 'world'
print('hello ' + name + '!')
The above is rendered as follows:
.. jupyter-execute::
name = 'world'
print('hello ' + name + '!')
Note that the code produces *output* (printing the string 'hello world!'), and the output
is rendered directly after the code snippet.
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:
.. jupyter-execute::
a = 1
print('first cell: a = {}'.format(a))
.. jupyter-execute::
a += 1
print('second cell: a = {}'.format(a))
Because jupyter-sphinx uses the machinery of ``nbconvert``, it is capable of rendering
any rich output, for example plots:
.. jupyter-execute::
import numpy as np
from matplotlib import pyplot
%matplotlib inline
x = np.linspace(1E-3, 2 * np.pi)
pyplot.plot(x, np.sin(x) / x)
pyplot.plot(x, np.cos(x))
pyplot.grid()
LaTeX output:
.. jupyter-execute::
from IPython.display import Latex
Latex('\\int_{-\\infty}^\\infty e^{-x²}dx = \\sqrt{\\pi}')
or even full-blown javascript widgets:
.. jupyter-execute::
import ipywidgets as w
from IPython.display import display
a = w.IntSlider()
b = w.IntText()
w.jslink((a, 'value'), (b, 'value'))
display(a, b)
It is also possible to include code from a regular file by passing the filename as argument
to ``jupyter-execute``::
.. jupyter-execute:: some_code.py
``jupyter-execute`` may also be used in docstrings within your Python code, and will be executed
when they are included with Sphinx autodoc.
Thebelab support
----------------
To turn on `thebelab `_, specify its configuration directly
in ``conf.py``:
.. code-block:: python
jupyter_sphinx_thebelab_config = {
'requestKernel': True,
'binderOptions': {
'repo': "binder-examples/requirements",
},
}
With this configuration, thebelab is activated with a button click:
.. thebe-button:: Activate Thebelab
By default the button is added at the end of the document, but it may also be inserted anywhere using
.. code-block:: ReST
.. thebe-button:: Optional title
Directive options
-----------------
You may choose to hide the code of a cell, but keep its output visible using ``:hide-code:``::
.. jupyter-execute::
:hide-code:
print('this code is invisible')
produces:
.. jupyter-execute::
:hide-code:
print('this code is invisible')
this option is particularly useful if you want to embed correctness checks in building your documentation::
.. jupyter-execute::
:hide-code:
assert everything_works, "There's a bug somewhere"
This way even though the code won't make it into the documentation, the build will fail if running the code fails.
Similarly, outputs are hidden with ``:hide-output:``::
.. jupyter-execute::
:hide-output:
print('this output is invisible')
produces:
.. jupyter-execute::
:hide-output:
print('this output is invisible')
You may also display the code *below* the output with ``:code-below:``::
.. jupyter-execute::
:code-below:
print('this code is below the output')
produces:
.. jupyter-execute::
:code-below:
print('this code is below the output')
You may also add *line numbers* to the source code with ``:linenos:``::
.. jupyter-execute::
:linenos:
print('A')
print('B')
print('C')
produces:
.. jupyter-execute::
:linenos:
print('A')
print('B')
print('C')
To add *line numbers from a specific line* to the source code, use the
``lineno-start`` directive::
.. jupyter-execute::
:lineno-start: 7
print('A')
print('B')
print('C')
produces:
.. jupyter-execute::
:lineno-start: 7
print('A')
print('B')
print('C')
You may also emphasize particular lines in the source code with ``:emphasize-lines:``::
.. jupyter-execute::
:emphasize-lines: 2,5-6
d = {
'a': 1,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
}
produces:
.. jupyter-execute::
:lineno-start: 2
:emphasize-lines: 2,5-6
d = {
'a': 1,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
}
Controlling exceptions
----------------------
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::
.. jupyter-execute::
:raises:
1 / 0
produces:
.. jupyter-execute::
:raises:
1 / 0
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::
.. jupyter-execute::
:raises: KeyError, ValueError
a = {'hello': 'world!'}
a['jello']
produces:
.. jupyter-execute::
:raises: KeyError, ValueError
a = {'hello': 'world!'}
a['jello']
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::
.. jupyter-execute::
:stderr:
import sys
print("hello, world!", file=sys.stderr)
produces:
.. jupyter-execute::
:stderr:
import sys
print("hello, world!", file=sys.stderr)
Manually forming Jupyter cells
------------------------------
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``::
.. jupyter-input::
:linenos:
import time
def slow_print(str):
time.sleep(4000) # Simulate an expensive process
print(str)
slow_print("hello, world!")
.. jupyter-output::
hello, world!
produces:
.. jupyter-input::
:linenos:
import time
def slow_print(str):
time.sleep(4000) # Simulate an expensive process
print(str)
slow_print("hello, world!")
.. jupyter-output::
hello, world!
Controlling the execution environment
-------------------------------------
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::
.. jupyter-kernel:: python3
:id: a_unique_name
``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.
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.
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.
Downloading the code as a script
--------------------------------
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).
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::
:jupyter-download-script:`click to download `
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.
Styling options
---------------
The CSS (Cascading Style Sheet) class structure of jupyter-sphinx is the
following::
- jupyter_container, jupyter_cell
- cell_input
- cell_output
- stderr
- output
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.
Here is a sample ``custom.css`` file overriding the ``stderr`` background color:
.. code-block:: css
.jupyter_container .stderr {
background-color: #7FFF00;
}
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.
Configuration options
---------------------
Typically you will be using Sphinx to build documentation for a software package.
If you are building documentation for a Python package you should add the following
lines to your sphinx ``conf.py``::
import os
package_path = os.path.abspath('../..')
os.environ['PYTHONPATH'] = ':'.join((package_path, os.environ.get('PYTHONPATH', '')))
This will ensure that your package is importable by any IPython kernels, as they will
inherit the environment variables from the main Sphinx process.
Here is a list of all the configuration options available to the Jupyter Sphinx extension:
jupyter_execute_default_kernel
The default kernel to launch when executing code in ``jupyter-execute`` directives.
The default is ``python3``.
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.
The default is
``['application/vnd.jupyter.widget-view+json', 'text/html', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/latex', 'text/plain']``.
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']``.
jupyter_execute_kwargs
Keyword arguments to pass to ``nbconvert.preprocessors.execute.executenb``, which controls how
code cells are executed. The default is ``dict(timeout=-1, allow_errors=True)``.
jupyter_sphinx_linenos
Whether to show line numbering in all ``jupyter-execute`` sources.
jupyter_sphinx_continue_linenos
Whether to continue line numbering from previous cell in all ``jupyter-execute``
sources.
Changelog
---------
Release 0.4.0
~~~~~~~~~~~~~
- Allow adding inputs and outputs that are not executed using ``jupyter-input`` and ``jupyter-output`` directives.
- Improve script handling by using ``nbconvert`` directly.
- Remove deprecated enabling of the extension as ``jupyter_sphinx.execute``.
- Implement different output priorities in HTML and LaTeX builders. In practice this allows to provide a better fallback in PDF output.
- Introduce new ``jupyter-download`` syntax compatible with Sphinx≥4, ``jupyter-download-nb``, ``jupyter-download-notebook``, and ``jupyter-download-script``
- Do not overwrite CSS and JS if files already exist in _static/, this allows to customize the CSS and JS file.
Release 0.3.0
~~~~~~~~~~~~~
- Switch the extension name to ``jupyter-sphinx``, deprecate ``jupyter-sphinx.execute``.
- Miscellaneous bugfixes following the restructuring of the codebase.
jupyter-sphinx-0.5.3/jupyter_sphinx/ 0000775 0000000 0000000 00000000000 14543263320 0017625 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/jupyter_sphinx/__init__.py 0000664 0000000 0000000 00000021464 14543263320 0021745 0 ustar 00root root 0000000 0000000 """Simple sphinx extension that executes code in jupyter and inserts output."""
from pathlib import Path
import docutils
import ipywidgets
from IPython.lib.lexers import IPython3Lexer, IPythonTracebackLexer
from sphinx.errors import ExtensionError
from sphinx.util import logging
from sphinx.util.fileutil import copy_asset
from ._version import __version__
from .ast import (
WIDGET_VIEW_MIMETYPE,
CellInput,
CellInputNode,
CellOutput,
CellOutputNode,
CombineCellInputOutput,
JupyterCell,
JupyterCellNode,
JupyterDownloadRole,
JupyterKernelNode,
JupyterWidgetStateNode,
JupyterWidgetViewNode,
MimeBundleNode,
)
from .execute import ExecuteJupyterCells, JupyterKernel
from .thebelab import ThebeButton, ThebeButtonNode, ThebeOutputNode, ThebeSourceNode
REQUIRE_URL_DEFAULT = (
"https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js"
)
THEBELAB_URL_DEFAULT = "https://unpkg.com/thebelab@^0.4.0"
logger = logging.getLogger(__name__)
# Constants and functions we'll use later
# Used for nodes that do not need to be rendered
def skip(self, node):
raise docutils.nodes.SkipNode
# Used for nodes that should be gone by rendering time (OutputMimeBundleNode)
def halt(self, node):
raise ExtensionError(
"Rendering encountered a node type that should "
"have been removed before rendering: %s" % type(node)
)
# Renders the children of a container
render_container = (
lambda self, node: self.visit_container(node),
lambda self, node: self.depart_container(node),
)
# Used to render the container and its children as HTML
def visit_container_html(self, node):
self.body.append(node.visit_html())
self.visit_container(node)
def depart_container_html(self, node):
self.depart_container(node)
self.body.append(node.depart_html())
# Used to render an element node as HTML
def visit_element_html(self, node):
self.body.append(node.html())
raise docutils.nodes.SkipNode
# Used to render the ThebeSourceNode conditionally for non-HTML builders
def visit_thebe_source(self, node):
if node["hide_code"]:
raise docutils.nodes.SkipNode
else:
self.visit_container(node)
render_thebe_source = (
visit_thebe_source,
lambda self, node: self.depart_container(node),
)
# Sphinx callback functions
def builder_inited(app):
"""
2 cases
case 1: ipywidgets 7, with require
case 2: ipywidgets 7, no require
"""
require_url = app.config.jupyter_sphinx_require_url
if require_url:
app.add_js_file(require_url)
embed_url = (
app.config.jupyter_sphinx_embed_url
or ipywidgets.embed.DEFAULT_EMBED_REQUIREJS_URL
)
else:
embed_url = (
app.config.jupyter_sphinx_embed_url
or ipywidgets.embed.DEFAULT_EMBED_SCRIPT_URL
)
if embed_url:
app.add_js_file(embed_url)
def copy_file(src, dst):
if not (dst / src.name).exists():
copy_asset(str(src), str(dst))
def build_finished(app, env):
if app.builder.format != "html":
return
module_dir = Path(__file__).parent
static = Path(app.outdir) / "_static"
# Copy stylesheet
src = module_dir / "css" / "jupyter-sphinx.css"
copy_file(src, static)
thebe_config = app.config.jupyter_sphinx_thebelab_config
if not thebe_config:
return
# Copy all thebelab related assets
src_dir = module_dir / "thebelab"
for fname in ["thebelab-helper.js", "thebelab.css"]:
copy_file(src_dir / fname, static)
##############################################################################
# Main setup
def setup(app):
"""A temporary setup function so that we can use it here and in execute.
This should be removed and converted into `setup` after a deprecation
cycle.
"""
# Configuration
app.add_config_value(
"jupyter_execute_kwargs",
dict(timeout=-1, allow_errors=True, store_widget_state=True),
"env",
)
app.add_config_value("jupyter_execute_default_kernel", "python3", "env")
app.add_config_value(
"render_priority_html",
[
WIDGET_VIEW_MIMETYPE,
"application/javascript",
"text/html",
"image/svg+xml",
"image/png",
"image/jpeg",
"text/latex",
"text/plain",
],
"env",
)
app.add_config_value(
"render_priority_latex",
[
"image/svg+xml",
"image/png",
"image/jpeg",
"text/latex",
"text/plain",
],
"env",
)
# ipywidgets config
app.add_config_value("jupyter_sphinx_require_url", REQUIRE_URL_DEFAULT, "html")
app.add_config_value("jupyter_sphinx_embed_url", None, "html")
# thebelab config, can be either a filename or a dict
app.add_config_value("jupyter_sphinx_thebelab_config", None, "html")
app.add_config_value("jupyter_sphinx_thebelab_url", THEBELAB_URL_DEFAULT, "html")
# linenos config
app.add_config_value("jupyter_sphinx_linenos", False, "env")
app.add_config_value("jupyter_sphinx_continue_linenos", False, "env")
# JupyterKernelNode is just a doctree marker for the
# ExecuteJupyterCells transform, so we don't actually render it.
app.add_node(
JupyterKernelNode,
html=(skip, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# Register our container nodes, these should behave just like a regular container
for node in [JupyterCellNode, CellInputNode, CellOutputNode, MimeBundleNode]:
app.add_node(
node,
override=True,
html=(render_container),
latex=(render_container),
textinfo=(render_container),
text=(render_container),
man=(render_container),
)
# JupyterWidgetViewNode holds widget view JSON,
# but is only rendered properly in HTML documents.
app.add_node(
JupyterWidgetViewNode,
html=(visit_element_html, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# JupyterWidgetStateNode holds the widget state JSON,
# but is only rendered in HTML documents.
app.add_node(
JupyterWidgetStateNode,
html=(visit_element_html, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# ThebeSourceNode holds the source code and is rendered if
# hide-code is not specified. For HTML it is always rendered,
# but hidden using the stylesheet
app.add_node(
ThebeSourceNode,
html=(visit_container_html, depart_container_html),
latex=render_thebe_source,
textinfo=render_thebe_source,
text=render_thebe_source,
man=render_thebe_source,
)
# ThebeOutputNode holds the output of the Jupyter cells
# and is rendered if hide-output is not specified.
app.add_node(
ThebeOutputNode,
html=(visit_container_html, depart_container_html),
latex=render_container,
textinfo=render_container,
text=render_container,
man=render_container,
)
# ThebeButtonNode is the button that activates thebelab
# and is only rendered for the HTML builder
app.add_node(
ThebeButtonNode,
html=(visit_element_html, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
app.add_directive("jupyter-execute", JupyterCell)
app.add_directive("jupyter-kernel", JupyterKernel)
app.add_directive("jupyter-input", CellInput)
app.add_directive("jupyter-output", CellOutput)
app.add_directive("thebe-button", ThebeButton)
for sep in [":", "-"]:
# Since Sphinx 4.0.0 using ":" inside of a role/directive does not work.
# Therefore, we add "-" as separator to get e.g., jupyter-download-notebook
# We leave the ":" syntax for backward compatibility reasons.
app.add_role(f"jupyter-download{sep}notebook", JupyterDownloadRole())
app.add_role(f"jupyter-download{sep}nb", JupyterDownloadRole())
app.add_role(f"jupyter-download{sep}script", JupyterDownloadRole())
app.add_transform(CombineCellInputOutput)
app.add_transform(ExecuteJupyterCells)
# For syntax highlighting
app.add_lexer("ipythontb", IPythonTracebackLexer)
app.add_lexer("ipython3", IPython3Lexer)
app.connect("builder-inited", builder_inited)
app.connect("build-finished", build_finished)
# add jupyter-sphinx and thebelab js and css
app.add_css_file("jupyter-sphinx.css")
app.add_js_file("thebelab-helper.js")
app.add_css_file("thebelab.css")
return {"version": __version__, "parallel_read_safe": True}
jupyter-sphinx-0.5.3/jupyter_sphinx/_version.py 0000664 0000000 0000000 00000001016 14543263320 0022021 0 ustar 00root root 0000000 0000000 """
store the current version info of the project.
"""
import re
from typing import List
# Version string must appear intact for automatic versioning
__version__ = "0.5.3"
# Build up version_info tuple for backwards compatibility
pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)"
match = re.match(pattern, __version__)
assert match is not None
parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]]
if match["rest"]:
parts.append(match["rest"])
version_info = tuple(parts)
jupyter-sphinx-0.5.3/jupyter_sphinx/ast.py 0000664 0000000 0000000 00000054635 14543263320 0021003 0 ustar 00root root 0000000 0000000 """Manipulating the Sphinx AST with Jupyter objects"""
import json
import warnings
from pathlib import Path
import docutils
import ipywidgets.embed
import nbconvert
from docutils.nodes import literal, math_block
from docutils.parsers.rst import Directive, directives
from sphinx.addnodes import download_reference
from sphinx.errors import ExtensionError
from sphinx.transforms import SphinxTransform
from sphinx.util import parselinenos
from sphinx.util.docutils import ReferenceRole
from .thebelab import ThebeOutputNode, ThebeSourceNode
from .utils import sphinx_abs_dir, strip_latex_delimiters
WIDGET_VIEW_MIMETYPE = "application/vnd.jupyter.widget-view+json"
WIDGET_STATE_MIMETYPE = "application/vnd.jupyter.widget-state+json"
def csv_option(s):
return [p.strip() for p in s.split(",")] if s else []
def load_content(cell, location, logger):
if cell.arguments:
# As per 'sphinx.directives.code.LiteralInclude'
env = cell.state.document.settings.env
rel_filename, filename = env.relfn2path(cell.arguments[0])
env.note_dependency(rel_filename)
if cell.content:
logger.warning(
'Ignoring inline code in Jupyter cell included from "{}"'.format(
rel_filename
),
location=location,
)
try:
with Path(filename).open() as f:
content = [line.rstrip() for line in f.readlines()]
except OSError:
raise OSError(f"File {filename} not found or reading it failed")
else:
cell.assert_has_content()
content = cell.content
return content
def get_highlights(cell, content, location, logger):
# The code fragment is taken from CodeBlock directive almost unchanged:
# https://github.com/sphinx-doc/sphinx/blob/0319faf8f1503453b6ce19020819a8cf44e39f13/sphinx/directives/code.py#L134-L148
emphasize_linespec = cell.options.get("emphasize-lines")
if emphasize_linespec:
nlines = len(content)
hl_lines = parselinenos(emphasize_linespec, nlines)
if any(i >= nlines for i in hl_lines):
logger.warning(
"Line number spec is out of range(1-{}): {}".format(
nlines, emphasize_linespec
),
location=location,
)
hl_lines = [i + 1 for i in hl_lines if i < nlines]
else:
hl_lines = []
return hl_lines
class JupyterCell(Directive):
"""Define a code cell to be later executed in a Jupyter kernel.
The content of the directive is the code to execute. Code is not
executed when the directive is parsed, but later during a doctree
transformation.
Arguments
---------
filename : str (optional)
If provided, a path to a file containing code.
Options
-------
hide-code : bool
If provided, the code will not be displayed in the output.
hide-output : bool
If provided, the cell output will not be displayed in the output.
code-below : bool
If provided, the code will be shown below the cell output.
linenos : bool
If provided, the code will be shown with line numbering.
lineno-start: nonnegative int
If provided, the code will be show with line numbering beginning from
specified line.
emphasize-lines : comma separated list of line numbers
If provided, the specified lines will be highlighted.
raises : comma separated list of exception types
If provided, a comma-separated list of exception type names that
the cell may raise. If one of the listed exception types is raised
then the traceback is printed in place of the cell output. If an
exception of another type is raised then we raise a RuntimeError
when executing.
Content
-------
code : str
A code cell.
"""
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
has_content = True
option_spec = {
"hide-code": directives.flag,
"hide-output": directives.flag,
"code-below": directives.flag,
"linenos": directives.flag,
"lineno-start": directives.nonnegative_int,
"emphasize-lines": directives.unchanged_required,
"raises": csv_option,
"stderr": directives.flag,
}
def run(self):
# This only works lazily because the logger is inited by Sphinx
from . import logger
location = self.state_machine.get_source_and_line(self.lineno)
content = load_content(self, location, logger)
try:
hl_lines = get_highlights(self, content, location, logger)
except ValueError as err:
return [self.state.document.reporter.warning(err, line=self.lineno)]
# A top-level placeholder for our cell
cell_node = JupyterCellNode(
execute=True,
hide_code=("hide-code" in self.options),
hide_output=("hide-output" in self.options),
code_below=("code-below" in self.options),
emphasize_lines=hl_lines,
raises=self.options.get("raises"),
stderr=("stderr" in self.options),
classes=["jupyter_cell"],
)
# Add the input section of the cell, we'll add output at execution time
cell_input = CellInputNode(classes=["cell_input"])
cell_input += docutils.nodes.literal_block(
text="\n".join(content),
linenos=("linenos" in self.options),
linenostart=(self.options.get("lineno-start")),
)
cell_node += cell_input
return [cell_node]
class CellInput(Directive):
"""Define a code cell to be included verbatim but not executed.
Arguments
---------
filename : str (optional)
If provided, a path to a file containing code.
Options
-------
linenos : bool
If provided, the code will be shown with line numbering.
lineno-start: nonnegative int
If provided, the code will be show with line numbering beginning from
specified line.
emphasize-lines : comma separated list of line numbers
If provided, the specified lines will be highlighted.
Content
-------
code : str
A code cell.
"""
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
has_content = True
option_spec = {
"linenos": directives.flag,
"lineno-start": directives.nonnegative_int,
"emphasize-lines": directives.unchanged_required,
}
def run(self):
# This only works lazily because the logger is inited by Sphinx
from . import logger
location = self.state_machine.get_source_and_line(self.lineno)
content = load_content(self, location, logger)
try:
hl_lines = get_highlights(self, content, location, logger)
except ValueError as err:
return [self.state.document.reporter.warning(err, line=self.lineno)]
# A top-level placeholder for our cell
cell_node = JupyterCellNode(
execute=False,
hide_code=False,
hide_output=True,
code_below=False,
emphasize_lines=hl_lines,
raises=False,
stderr=False,
classes=["jupyter_cell"],
)
# Add the input section of the cell, we'll add output when jupyter-execute cells are run
cell_input = CellInputNode(classes=["cell_input"])
cell_input += docutils.nodes.literal_block(
text="\n".join(content),
linenos=("linenos" in self.options),
linenostart=(self.options.get("lineno-start")),
)
cell_node += cell_input
return [cell_node]
class CellOutput(Directive):
"""Define an output cell to be included verbatim.
Arguments
---------
filename : str (optional)
If provided, a path to a file containing output.
Content
-------
code : str
An output cell.
"""
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
has_content = True
option_spec = {}
def run(self):
# This only works lazily because the logger is inited by Sphinx
from . import logger
location = self.state_machine.get_source_and_line(self.lineno)
content = load_content(self, location, logger)
# A top-level placeholder for our cell
cell_node = JupyterCellNode(
execute=False,
hide_code=True,
hide_output=False,
code_below=False,
emphasize_lines=[],
raises=False,
stderr=False,
)
# Add a blank input and the given output to the cell
cell_input = CellInputNode(classes=["cell_input"])
cell_input += docutils.nodes.literal_block(
text="",
linenos=False,
linenostart=None,
)
cell_node += cell_input
content_str = "\n".join(content)
cell_output = CellOutputNode(classes=["cell_output"])
cell_output += docutils.nodes.literal_block(
text=content_str,
rawsource=content_str,
language="none",
classes=["output", "stream"],
)
cell_node += cell_output
return [cell_node]
class JupyterCellNode(docutils.nodes.container):
"""Inserted into doctree wherever a JupyterCell directive is encountered.
Contains code that will be executed in a Jupyter kernel at a later
doctree-transformation step.
"""
class CellInputNode(docutils.nodes.container):
"""Represent an input cell in the Sphinx AST."""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", **attributes)
class CellOutputNode(docutils.nodes.container):
"""Represent an output cell in the Sphinx AST."""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", **attributes)
class MimeBundleNode(docutils.nodes.container):
"""A node with multiple representations rendering as the highest priority one."""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", *children, mimetypes=attributes["mimetypes"])
def render_as(self, visitor):
"""Determine which node to show based on the visitor"""
try:
# Or should we go to config via the node?
priority = visitor.builder.env.app.config[
"render_priority_" + visitor.builder.format
]
except (AttributeError, KeyError):
# Not sure what do to, act as a container and show everything just in case.
return super()
for mimetype in priority:
try:
return self.children[self.attributes["mimetypes"].index(mimetype)]
except ValueError:
pass
# Same
return super()
def walk(self, visitor):
return self.render_as(visitor).walk(visitor)
def walkabout(self, visitor):
return self.render_as(visitor).walkabout(visitor)
class JupyterKernelNode(docutils.nodes.Element):
"""Inserted into doctree whenever a JupyterKernel directive is encountered.
Used as a marker to signal that the following JupyterCellNodes (until the
next, if any, JupyterKernelNode) should be executed in a separate kernel.
"""
class JupyterWidgetViewNode(docutils.nodes.Element):
"""Inserted into doctree whenever a Jupyter cell produces a widget as output.
Contains a unique ID for this widget; enough information for the widget
embedding javascript to render it, given the widget state. For non-HTML
outputs this doctree node is rendered generically.
"""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", view_spec=attributes["view_spec"])
def html(self):
return ipywidgets.embed.widget_view_template.format(
view_spec=json.dumps(self["view_spec"])
)
class JupyterWidgetStateNode(docutils.nodes.Element):
"""Appended to doctree if any Jupyter cell produced a widget as output.
Contains the state needed to render a collection of Jupyter widgets.
Per doctree there is 1 JupyterWidgetStateNode per kernel that produced
Jupyter widgets when running. This is fine as (presently) the
'html-manager' Javascript library, which embeds widgets, loads the state
from all script tags on the page of the correct mimetype.
"""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", state=attributes["state"])
def html(self):
# escape to avoid early closing of the tag in the html page
json_data = json.dumps(self["state"]).replace("", r"<\/script>")
# TODO: render into a separate file if 'html-manager' starts fully
# parsing script tags, and not just grabbing their innerHTML
# https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed.ts#L36
return ipywidgets.embed.snippet_template.format(
load="", widget_views="", json_data=json_data
)
def cell_output_to_nodes(outputs, write_stderr, out_dir, thebe_config, inline=False):
"""Convert a jupyter cell with outputs and filenames to doctree nodes.
Parameters
----------
outputs : a list of outputs from a Jupyter cell
write_stderr : bool
If True include stderr in cell output
out_dir : string
Sphinx "absolute path" to the output folder, so it is a relative path
to the source folder prefixed with ``/``.
thebe_config: dict
Thebelab configuration object or None
inline: False
Whether the nodes will be placed in-line with the text.
Returns
-------
to_add : list of docutils nodes
Each output, converted into a docutils node.
"""
# If we're in `inline` mode, ensure that we don't add block-level nodes
literal_node = docutils.nodes.literal if inline else docutils.nodes.literal_block
to_add = []
for output in outputs:
output_type = output["output_type"]
if output_type == "stream":
if output["name"] == "stderr":
if not write_stderr:
continue
else:
# Output a container with an unhighlighted literal block for
# `stderr` messages.
#
# Adds a "stderr" class that can be customized by the user for both
# the container and the literal_block.
#
# Not setting "rawsource" disables Pygment highlighting, which
# would otherwise add a
.
literal = literal_node(
text=output["text"],
rawsource="", # disables Pygment highlighting
language="none",
classes=["stderr"],
)
if inline:
# In this case, we don't wrap the text in containers
to_add.append(literal)
else:
container = docutils.nodes.container(classes=["stderr"])
container.append(literal)
to_add.append(container)
else:
to_add.append(
literal_node(
text=output["text"],
rawsource=output["text"],
language="none",
classes=["output", "stream"],
)
)
elif output_type == "error":
traceback = "\n".join(output["traceback"])
text = nbconvert.filters.strip_ansi(traceback)
to_add.append(
literal_node(
text=text,
rawsource=text,
language="ipythontb",
classes=["output", "traceback"],
)
)
elif output_type in ("display_data", "execute_result"):
children_by_mimetype = {
mime_type: output2sphinx(data, mime_type, output["metadata"], out_dir)
for mime_type, data in output["data"].items()
}
# Filter out unknown mimetypes
# TODO: rewrite this using walrus once we depend on Python 3.8
children_by_mimetype = {
mime_type: node
for mime_type, node in children_by_mimetype.items()
if node is not None
}
to_add.append(
MimeBundleNode(
"",
*list(children_by_mimetype.values()),
mimetypes=list(children_by_mimetype.keys()),
)
)
return to_add
def output2sphinx(data, mime_type, metadata, out_dir, inline=False):
"""Convert a Jupyter output with a specific mimetype to its sphinx representation."""
# This only works lazily because the logger is inited by Sphinx
from . import logger
# If we're in `inline` mode, ensure that we don't add block-level nodes
if inline:
literal_node = docutils.nodes.literal
math_node = docutils.nodes.math
else:
literal_node = docutils.nodes.literal_block
math_node = math_block
if mime_type == "text/html":
return docutils.nodes.raw(
text=data, format="html", classes=["output", "text_html"]
)
elif mime_type == "text/plain":
return literal_node(
text=data,
rawsource=data,
language="none",
classes=["output", "text_plain"],
)
elif mime_type == "text/latex":
return math_node(
text=strip_latex_delimiters(data),
nowrap=False,
number=None,
classes=["output", "text_latex"],
)
elif mime_type == "application/javascript":
return docutils.nodes.raw(
text=''.format(
mime_type=mime_type, data=data
),
format="html",
)
elif mime_type == WIDGET_VIEW_MIMETYPE:
return JupyterWidgetViewNode(view_spec=data)
elif mime_type.startswith("image"):
file_path = Path(metadata["filenames"][mime_type])
out_dir = Path(out_dir)
# Sphinx treats absolute paths as being rooted at the source
# directory, so make a relative path, which Sphinx treats
# as being relative to the current working directory.
filename = file_path.name
if out_dir in file_path.parents:
out_dir = file_path.parent
uri = (out_dir / filename).as_posix()
return docutils.nodes.image(uri=uri)
else:
logger.debug(f"Unknown mime type in cell output: {mime_type}")
def apply_styling(node, thebe_config):
"""Change the cell node appearance, according to its settings."""
if not node.attributes["hide_code"]: # only add css if code is displayed
classes = node.attributes.get("classes", [])
classes += ["jupyter_container"]
(input_node, output_node) = node.children
if thebe_config:
# Move the source from the input node into the thebe_source node
source = input_node.children.pop(0)
thebe_source = ThebeSourceNode(
hide_code=node.attributes["hide_code"],
code_below=node.attributes["code_below"],
language=node.attributes["cm_language"],
)
thebe_source.children = [source]
input_node.children = [thebe_source]
thebe_output = ThebeOutputNode()
thebe_output.children = output_node.children
output_node.children = [thebe_output]
else:
if node.attributes["hide_code"]:
node.children.pop(0)
if node.attributes["hide_output"]:
output_node.children = []
# Swap inputs and outputs if we want the code below
if node.attributes["code_below"]:
node.children = node.children[::-1]
class JupyterDownloadRole(ReferenceRole):
def run(self):
sep = ":" if ":" in self.name else "-"
name, filetype = self.name.rsplit(sep, maxsplit=1)
if sep == ":":
warnings.warn(
f"The {self.name} syntax is deprecated and "
f"will be removed in 0.5.0, please use {name}-{filetype}",
category=DeprecationWarning,
)
assert filetype in ("notebook", "nb", "script")
ext = ".ipynb" if filetype in ("notebook", "nb") else ".py"
download_file = self.target + ext
reftarget = sphinx_abs_dir(self.env, download_file)
node = download_reference(self.rawtext, reftarget=reftarget)
self.set_source_info(node)
title = self.title if self.has_explicit_title else download_file
node += literal(self.rawtext, title, classes=["xref", "download"])
return [node], []
def get_widgets(notebook):
try:
return notebook.metadata.widgets[WIDGET_STATE_MIMETYPE]
except AttributeError:
# Don't catch KeyError because it's a bug if 'widgets' does
# not contain 'WIDGET_STATE_MIMETYPE'
return None
class CombineCellInputOutput(SphinxTransform):
"""Merge nodes from CellOutput with the preceding CellInput node."""
default_priority = 120
def apply(self):
moved_outputs = set()
for cell_node in self.document.findall(JupyterCellNode):
if not cell_node.attributes["execute"]:
if not cell_node.attributes["hide_code"]:
# Cell came from jupyter-input
sibling = cell_node.next_node(descend=False, siblings=True)
if (
isinstance(sibling, JupyterCellNode)
and not sibling.attributes["execute"]
and sibling.attributes["hide_code"]
):
# Sibling came from jupyter-output, so we merge
cell_node += sibling.children[1]
cell_node.attributes["hide_output"] = False
moved_outputs.update({sibling})
else:
# Call came from jupyter-output
if cell_node not in moved_outputs:
raise ExtensionError(
"Found a jupyter-output node without a preceding jupyter-input"
)
for output_node in moved_outputs:
output_node.replace_self([])
jupyter-sphinx-0.5.3/jupyter_sphinx/css/ 0000775 0000000 0000000 00000000000 14543263320 0020415 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/jupyter_sphinx/css/jupyter-sphinx.css 0000664 0000000 0000000 00000005713 14543263320 0024146 0 ustar 00root root 0000000 0000000 /* Stylesheet for jupyter-sphinx
These styles mimic the Jupyter HTML styles.
The default CSS (Cascading Style Sheet) class structure of jupyter-sphinx
is the following:
jupyter_container
code_cell (optional)
stderr (optional)
output (optional)
If the code_cell is not displayed, then there is not a jupyter_container, and
the output is provided without CSS.
This stylesheet attempts to override the defaults of all packaged Sphinx themes
to display jupter-sphinx cells in a Jupyter-like style.
If you want to adjust the styles, add additional custom CSS to override these
styles.
After a build, this stylesheet is loaded from ./_static/jupyter-sphinx.css .
*/
div.jupyter_container {
padding: .4em;
margin: 0 0 .4em 0;
background-color: #FFFF;
border: 1px solid #CCC;
-moz-box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2);
-webkit-box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2);
box-shadow: 2px 2px 4px rgba(87, 87, 87, 0.2);
}
.jupyter_container div.code_cell {
border: 1px solid #cfcfcf;
border-radius: 2px;
background-color: #f7f7f7;
margin: 0 0;
overflow: auto;
}
.jupyter_container div.code_cell pre {
padding: 4px;
margin: 0 0;
background-color: #f7f7f7;
border: none;
background: none;
box-shadow: none;
-webkit-box-shadow: none; /* for nature */
-moz-box-shadow: none; /* for nature */
}
.jupyter_container div.code_cell * {
margin: 0 0;
}
div.jupyter_container div.highlight {
background-color: #f7f7f7; /* for haiku */
}
div.jupyter_container {
padding: 0;
margin: 0;
}
/* Prevent alabaster breaking highlight alignment */
div.jupyter_container .hll {
padding: 0;
margin: 0;
}
/* overrides for sphinx_rtd_theme */
.rst-content .jupyter_container div[class^='highlight'],
.document .jupyter_container div[class^='highlight'],
.rst-content .jupyter_container pre.literal-block {
border:none;
margin: 0;
padding: 0;
background: none;
padding: 3px;
background-color: transparent;
}
/* restore Mathjax CSS, as it assumes a vertical margin. */
.jupyter_container .MathJax_Display {
margin: 1em 0em;
text-align: center;
}
.jupyter_container .stderr {
background-color: #FCC;
border: none;
padding: 3px;
}
.jupyter_container .output {
border: none;
}
.jupyter_container div.output pre {
background-color: white;
background: none;
padding: 4px;
border: none;
box-shadow: none;
-webkit-box-shadow: none; /* for nature */
-moz-box-shadow: none; /* for nature */
}
.jupyter_container .code_cell td.linenos {
text-align: right;
padding: 4px 4px 4px 8px;
border-right: 1px solid #cfcfcf;
color: #999;
}
.jupyter_container .output .highlight {
background-color: #ffffff;
}
/* combine sequential jupyter cells,
by moving sequential ones up higher on y-axis */
div.jupyter_container + div.jupyter_container {
margin: -.5em 0 .4em 0;
}
/* Fix for sphinx_rtd_theme spacing after jupyter_container #91 */
.rst-content .jupyter_container {
margin: 0 0 24px 0;
}
jupyter-sphinx-0.5.3/jupyter_sphinx/execute.py 0000664 0000000 0000000 00000027254 14543263320 0021653 0 ustar 00root root 0000000 0000000 """Execution and managing kernels."""
import os
import warnings
from logging import Logger
from pathlib import Path
import nbconvert
from docutils.parsers.rst import Directive, directives
from nbconvert.preprocessors import ExtractOutputPreprocessor
from nbconvert.writers import FilesWriter
from sphinx.errors import ExtensionError
from sphinx.transforms import SphinxTransform
if nbconvert.version_info < (6,):
from nbconvert.preprocessors.execute import executenb
else:
from nbclient.client import execute as executenb
import traitlets
# Workaround of https://github.com/ipython/traitlets/issues/606
if traitlets.version_info < (5, 1):
class LoggerAdapterWrapper(Logger):
"""Wrap a logger adapter, while pretending to be a logger."""
def __init__(self, wrapped):
self._wrapped = wrapped
def __getattribute__(self, attr):
if attr == "_wrapped":
return object.__getattribute__(self, attr)
return self._wrapped.__getattribute__(attr)
else:
def LoggerAdapterWrapper(logger_adapter):
return logger_adapter
import nbformat
import jupyter_sphinx as js
from .ast import (
CellOutputNode,
JupyterCellNode,
JupyterKernelNode,
JupyterWidgetStateNode,
apply_styling,
cell_output_to_nodes,
get_widgets,
)
from .thebelab import ThebeButtonNode, add_thebelab_library
from .utils import (
blank_nb,
default_notebook_names,
output_directory,
sphinx_abs_dir,
split_on,
)
class JupyterKernel(Directive):
"""Specify a new Jupyter Kernel.
Arguments
---------
kernel_name : str (optional)
The name of the kernel in which to execute future Jupyter cells, as
reported by executing 'jupyter kernelspec list' on the command line.
Options
-------
id : str
An identifier for *this kernel instance*. Used to name any output
files generated when executing the Jupyter cells (e.g. images
produced by cells, or a script containing the cell inputs).
Content
-------
None
"""
optional_arguments = 1
final_argument_whitespace = False
has_content = False
option_spec = {"id": directives.unchanged}
def run(self):
return [
JupyterKernelNode(
"",
kernel_name=self.arguments[0].strip() if self.arguments else "",
kernel_id=self.options.get("id", "").strip(),
)
]
# Doctree transformations
class ExecuteJupyterCells(SphinxTransform):
"""Execute code cells in Jupyter kernels.
Traverses the doctree to find JupyterKernel and JupyterCell nodes,
then executes the code in the JupyterCell nodes in sequence, starting
a new kernel every time a JupyterKernel node is encountered. The output
from each code cell is inserted into the doctree.
"""
# Beginning of main transforms. Not 100% sure it's the correct time.
default_priority = 400
def apply(self):
doctree = self.document
docname_path = Path(self.env.docname)
doc_dir_relpath = docname_path.parent # relative to src dir
docname = docname_path.name
default_kernel = self.config.jupyter_execute_default_kernel
default_names = default_notebook_names(docname)
thebe_config = self.config.jupyter_sphinx_thebelab_config
linenos_config = self.config.jupyter_sphinx_linenos
continue_linenos = self.config.jupyter_sphinx_continue_linenos
# Check if we have anything to execute.
if not next(doctree.findall(JupyterCellNode), False):
return
if thebe_config:
# Add the button at the bottom if it is not present
if not next(doctree.findall(ThebeButtonNode), False):
doctree.append(ThebeButtonNode())
add_thebelab_library(doctree, self.env)
js.logger.info(f"executing {docname}")
output_dir = Path(output_directory(self.env)) / doc_dir_relpath
# Start new notebook whenever a JupyterKernelNode is encountered
jupyter_nodes = (JupyterCellNode, JupyterKernelNode)
nodes_by_notebook = split_on(
lambda n: isinstance(n, JupyterKernelNode),
list(doctree.findall(lambda n: isinstance(n, jupyter_nodes))),
)
for first, *nodes in nodes_by_notebook:
if isinstance(first, JupyterKernelNode):
kernel_name = first["kernel_name"] or default_kernel
file_name = first["kernel_id"] or next(default_names)
else:
nodes = (first, *nodes)
kernel_name = default_kernel
file_name = next(default_names)
# Add empty placeholder cells for non-executed nodes so nodes
# and cells can be zipped and the provided input/output
# can be inserted later
notebook = execute_cells(
kernel_name,
[
nbformat.v4.new_code_cell(node.astext() if node["execute"] else "")
for node in nodes
],
self.config.jupyter_execute_kwargs,
)
# Raise error if cells raised exceptions and were not marked as doing so
for node, cell in zip(nodes, notebook.cells):
errors = [
output
for output in cell.outputs
if output["output_type"] == "error"
]
allowed_errors = node.attributes.get("raises") or []
raises_provided = node.attributes["raises"] is not None
if (
raises_provided and not allowed_errors
): # empty 'raises': suppress all errors
pass
elif errors and not any(e["ename"] in allowed_errors for e in errors):
raise ExtensionError(
"Cell raised uncaught exception:\n{}".format(
"\n".join(errors[0]["traceback"])
)
)
# Raise error if cells print to stderr
for node, cell in zip(nodes, notebook.cells):
stderr = [
output
for output in cell.outputs
if output["output_type"] == "stream" and output["name"] == "stderr"
]
if stderr and not node.attributes["stderr"]:
js.logger.warning(
"Cell printed to stderr:\n{}".format(stderr[0]["text"])
)
# Insert input/output into placeholders for non-executed cells
for node, cell in zip(nodes, notebook.cells):
if not node["execute"]:
cell.source = node.children[0].astext()
if len(node.children) == 2:
output = nbformat.v4.new_output("stream")
output.text = node.children[1].astext()
cell.outputs = [output]
node.children.pop()
try:
lexer = notebook.metadata.language_info.pygments_lexer
except AttributeError:
lexer = notebook.metadata.kernelspec.language
# Highlight the code cells now that we know what language they are
for node in nodes:
source = node.children[0].children[0]
source.attributes["language"] = lexer
# Add line numbering
linenostart = 1
for node in nodes:
# The literal_block node with the source
source = node.children[0].children[0]
nlines = source.rawsource.count("\n") + 1
show_numbering = (
linenos_config or source["linenos"] or source["linenostart"]
)
if show_numbering:
source["linenos"] = True
if source["linenostart"]:
linenostart = source["linenostart"]
if source["linenostart"] or continue_linenos:
source["highlight_args"] = {"linenostart": linenostart}
else:
linenostart = 1
linenostart += nlines
hl_lines = node["emphasize_lines"]
if hl_lines:
highlight_args = source.setdefault("highlight_args", {})
highlight_args["hl_lines"] = hl_lines
# Add code cell CSS class
for node in nodes:
source = node.children[0]
source.attributes["classes"].append("code_cell")
# Write certain cell outputs (e.g. images) to separate files, and
# modify the metadata of the associated cells in 'notebook' to
# include the path to the output file.
write_notebook_output(
notebook, str(output_dir), file_name, self.env.docname
)
try:
cm_language = notebook.metadata.language_info.codemirror_mode.name
except AttributeError:
cm_language = notebook.metadata.kernelspec.language
for node in nodes:
node.attributes["cm_language"] = cm_language
# Add doctree nodes for cell outputs.
for node, cell in zip(nodes, notebook.cells):
# Add the outputs as children
output = CellOutputNode(classes=["cell_output"])
output.children = cell_output_to_nodes(
cell.outputs,
bool(node.attributes["stderr"]),
sphinx_abs_dir(self.env),
thebe_config,
)
node += output
apply_styling(node, thebe_config)
if contains_widgets(notebook):
doctree.append(JupyterWidgetStateNode(state=get_widgets(notebook)))
# Roles
def execute_cells(kernel_name, cells, execute_kwargs):
"""Execute Jupyter cells in the specified kernel and return the notebook."""
notebook = blank_nb(kernel_name)
notebook.cells = cells
# Modifies 'notebook' in-place
try:
executenb(notebook, **execute_kwargs)
except Exception as e:
raise ExtensionError("Notebook execution failed", orig_exc=e)
return notebook
def write_notebook_output(notebook, output_dir, notebook_name, location=None):
"""Extract output from notebook cells and write to files in output_dir.
This also modifies 'notebook' in-place, adding metadata to each cell that
maps output mime-types to the filenames the output was saved under.
"""
resources = dict(unique_key=os.path.join(output_dir, notebook_name), outputs={})
# Modifies 'resources' in-place
ExtractOutputPreprocessor().preprocess(notebook, resources)
# Write the cell outputs to files where we can (images and PDFs),
# as well as the notebook file.
FilesWriter(build_directory=output_dir).write(
nbformat.writes(notebook),
resources,
os.path.join(output_dir, notebook_name + ".ipynb"),
)
exporter = nbconvert.exporters.ScriptExporter(log=LoggerAdapterWrapper(js.logger))
with warnings.catch_warnings():
# See https://github.com/jupyter/nbconvert/issues/1388
warnings.simplefilter("ignore", DeprecationWarning)
contents, resources = exporter.from_notebook_node(notebook)
notebook_file = notebook_name + resources["output_extension"]
output_dir = Path(output_dir)
# utf-8 is the de-facto standard encoding for notebooks.
(output_dir / notebook_file).write_text(contents, encoding="utf8")
def contains_widgets(notebook):
widgets = get_widgets(notebook)
return widgets and widgets["state"]
jupyter-sphinx-0.5.3/jupyter_sphinx/thebelab.py 0000664 0000000 0000000 00000007667 14543263320 0021765 0 ustar 00root root 0000000 0000000 """Inserting interactive links with Thebelab."""
import json
from pathlib import Path
import docutils
from docutils.parsers.rst import Directive
import jupyter_sphinx as js
class ThebeSourceNode(docutils.nodes.container):
"""Container that holds the cell source when thebelab is enabled"""
def __init__(self, rawsource="", *children, **attributes):
super().__init__("", **attributes)
def visit_html(self):
code_class = "thebelab-code"
if self["hide_code"]:
code_class += " thebelab-hidden"
if self["code_below"]:
code_class += " thebelab-below"
language = self["language"]
return '
'.format(
code_class, language
)
def depart_html(self):
return "
"
class ThebeOutputNode(docutils.nodes.container):
"""Container that holds all the output nodes when thebelab is enabled"""
def visit_html(self):
return '
'
def depart_html(self):
return "
"
class ThebeButtonNode(docutils.nodes.Element):
"""Appended to the doctree by the ThebeButton directive
Renders as a button to enable thebelab on the page.
If no ThebeButton directive is found in the document but thebelab
is enabled, the node is added at the bottom of the document.
"""
def __init__(self, rawsource="", *children, text="Make live", **attributes):
super().__init__("", text=text)
def html(self):
text = self["text"]
return (
''.format(text=text)
)
class ThebeButton(Directive):
"""Specify a button to activate thebelab on the page
Arguments
---------
text : str (optional)
If provided, the button text to display
Content
-------
None
"""
optional_arguments = 1
final_argument_whitespace = True
has_content = False
def run(self):
kwargs = {"text": self.arguments[0]} if self.arguments else {}
return [ThebeButtonNode(**kwargs)]
def add_thebelab_library(doctree, env):
"""Adds the thebelab configuration and library to the doctree"""
thebe_config = env.config.jupyter_sphinx_thebelab_config
if isinstance(thebe_config, dict):
pass
elif isinstance(thebe_config, str):
thebe_config = Path(thebe_config)
if thebe_config.is_absolute():
filename = thebe_config
else:
filename = Path(env.app.srcdir).resolve() / thebe_config
if not filename.exists():
js.logger.warning("The supplied thebelab configuration file does not exist")
return
try:
thebe_config = json.loads(filename.read_text())
except ValueError:
js.logger.warning(
"The supplied thebelab configuration file is not in JSON format."
)
return
else:
js.logger.warning(
"The supplied thebelab configuration should be either"
" a filename or a dictionary."
)
return
# Force config values to make thebelab work correctly
thebe_config["predefinedOutput"] = True
thebe_config["requestKernel"] = True
# Specify the thebelab config inline, a separate file is not supported
doctree.append(
docutils.nodes.raw(
text='\n'.format(
json.dumps(thebe_config)
),
format="html",
)
)
# Add thebelab library after the config is specified
doctree.append(
docutils.nodes.raw(
text='\n'.format(
env.config.jupyter_sphinx_thebelab_url
),
format="html",
)
)
jupyter-sphinx-0.5.3/jupyter_sphinx/thebelab/ 0000775 0000000 0000000 00000000000 14543263320 0021373 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/jupyter_sphinx/thebelab/thebelab-helper.js 0000664 0000000 0000000 00000001540 14543263320 0024754 0 ustar 00root root 0000000 0000000 function initThebelab() {
let activateButton = document.getElementById("thebelab-activate-button");
if (activateButton.classList.contains('thebelab-active')) {
return;
}
// Place all outputs below the source where this was not the case
// to make them recognizable by thebelab
let codeBelows = document.getElementsByClassName('thebelab-below');
for(var i = 0; i < codeBelows.length; i++) {
let prev = codeBelows[i]
// Find previous sibling element, compatible with IE8
do prev = prev.previousSibling; while(prev && prev.nodeType !== 1);
swapSibling(prev, codeBelows[i])
}
thebelab.bootstrap();
activateButton.classList.add('thebelab-active')
}
function swapSibling(node1, node2) {
node1.parentNode.replaceChild(node1, node2);
node1.parentNode.insertBefore(node2, node1);
}
jupyter-sphinx-0.5.3/jupyter_sphinx/thebelab/thebelab.css 0000664 0000000 0000000 00000001355 14543263320 0023657 0 ustar 00root root 0000000 0000000 .thebelab-cell .thebelab-input pre {
z-index: 0;
}
.thebelab-hidden {
display: none;
}
.thebelab-button {
position: relative;
display: inline-block;
box-sizing: border-box;
border: none;
border-radius: .1rem;
padding: 0 2rem;
margin: .5rem .1rem;
min-width: 64px;
height: 1.6rem;
vertical-align: middle;
text-align: center;
font-size: 0.8rem;
color: rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.07);
overflow: hidden;
outline: none;
cursor: pointer;
transition: background-color 0.2s;
}
.thebelab-button:hover {
background-color: rgba(0, 0, 0, 0.12);
}
.thebelab-button:active {
background-color: rgba(0, 0, 0, 0.15);
color: rgba(0, 0, 0, 1)
}
jupyter-sphinx-0.5.3/jupyter_sphinx/utils.py 0000664 0000000 0000000 00000006303 14543263320 0021341 0 ustar 00root root 0000000 0000000 """Utility functions and helpers."""
import os
from itertools import count, groupby
from pathlib import Path
import nbformat
from jupyter_client.kernelspec import NoSuchKernel, get_kernel_spec
from sphinx.errors import ExtensionError
def blank_nb(kernel_name):
try:
spec = get_kernel_spec(kernel_name)
except NoSuchKernel as e:
raise ExtensionError("Unable to find kernel", orig_exc=e)
return nbformat.v4.new_notebook(
metadata={
"kernelspec": {
"display_name": spec.display_name,
"language": spec.language,
"name": kernel_name,
}
}
)
def split_on(pred, it):
"""Split an iterator wherever a predicate is True."""
counter = 0
def count(x):
nonlocal counter
if pred(x):
counter += 1
return counter
# Return iterable of lists to ensure that we don't lose our
# place in the iterator
return (list(x) for _, x in groupby(it, count))
def strip_latex_delimiters(source):
r"""Remove LaTeX math delimiters that would be rendered by the math block.
These are: ``\(…\)``, ``\[…\]``, ``$…$``, and ``$$…$$``.
This is necessary because sphinx does not have a dedicated role for
generic LaTeX, while Jupyter only defines generic LaTeX output, see
https://github.com/jupyter/jupyter-sphinx/issues/90 for discussion.
"""
source = source.strip()
delimiter_pairs = (pair.split() for pair in r"\( \),\[ \],$$ $$,$ $".split(","))
for start, end in delimiter_pairs:
if source.startswith(start) and source.endswith(end):
return source[len(start) : -len(end)]
return source
def default_notebook_names(basename):
"""Return an iterator yielding notebook names based off 'basename'"""
yield basename
for i in count(1):
yield "_".join((basename, str(i)))
def language_info(executor):
# Can only run this function inside 'setup_preprocessor'
assert hasattr(executor, "kc")
info_msg = executor._wait_for_reply(executor.kc.kernel_info())
return info_msg["content"]["language_info"]
def sphinx_abs_dir(env, *paths):
# We write the output files into
# output_directory / jupyter_execute / path relative to source directory
# Sphinx expects download links relative to source file or relative to
# source dir and prepended with '/'. We use the latter option.
out_path = (
output_directory(env) / Path(env.docname).parent / Path(*paths)
).resolve()
if os.name == "nt":
# Can't get relative path between drives on Windows
return out_path.as_posix()
# Path().relative_to() doesn't work when not a direct subpath
return "/" + os.path.relpath(out_path, env.app.srcdir)
def output_directory(env):
# Put output images inside the sphinx build directory to avoid
# polluting the current working directory. We don't use a
# temporary directory, as sphinx may cache the doctree with
# references to the images that we write
# Note: we are using an implicit fact that sphinx output directories are
# direct subfolders of the build directory.
return (Path(env.app.outdir) / os.path.pardir / "jupyter_execute").resolve()
jupyter-sphinx-0.5.3/pyproject.toml 0000664 0000000 0000000 00000003576 14543263320 0017461 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["hatchling>=1.5"]
build-backend = "hatchling.build"
[project]
name = "jupyter-sphinx"
dynamic = ["version"]
description = "Jupyter Sphinx Extensions"
readme = "README.md"
license = {file="LICENSE"}
requires-python = ">=3.8"
authors = [
{ name = "Jupyter Development Team", email = "jupyter@googlegroups.com" },
]
dependencies = [
"ipykernel>=4.5.1",
"IPython",
"ipywidgets>=7.0.0",
"nbconvert>=5.5",
"nbformat",
"Sphinx>=7",
]
[project.urls]
"Bug Tracker" = "https://github.com/jupyter/jupyter-sphinx/issues/"
Documentation = "https://jupyter-sphinx.readthedocs.io"
Homepage = "https://jupyter.org"
"Source Code" = "https://github.com/jupyter/jupyter-sphinx/"
[project.optional-dependencies]
test = [
"pytest",
"bash_kernel"
]
doc = [
"matplotlib"
]
[tool.hatch.version]
path = "jupyter_sphinx/_version.py"
[tool.hatch.build.targets.sdist]
include = [
"/jupyter_sphinx",
]
[tool.hatch.envs.lint]
detached = true
dependencies = ["pre-commit"]
[tool.hatch.envs.lint.scripts]
build = [
"pre-commit run --all-files ruff",
"pre-commit run --all-files ruff-format",
]
[tool.hatch.envs.doc]
features = ["doc"]
[tool.hatch.envs.doc.scripts]
build = "cd doc; make html-strict"
[tool.hatch.envs.test]
features = ["test"]
[tool.hatch.envs.test.env-vars]
JUPYTER_PLATFORM_DIRS = "1"
[tool.hatch.envs.test.scripts]
test = ["python -m bash_kernel.install", "python -m pytest -vv {args}"]
nowarn = "test -W default {args}"
[tool.pytest.ini_options]
minversion = "7.0"
xfail_strict = true
log_cli_level = "info"
addopts = [
"-ra", "--durations=10", "--color=yes", "--strict-config", "--strict-markers"
]
testpaths = ["tests/"]
filterwarnings = [
"error",
# https://github.com/dateutil/dateutil/issues/1314
"module:datetime.datetime.utc:DeprecationWarning"
]
[tool.repo-review]
ignore = ["GH102", "MY100", "RF001", "PY007", "GH103", "PC140"]
jupyter-sphinx-0.5.3/tests/ 0000775 0000000 0000000 00000000000 14543263320 0015674 5 ustar 00root root 0000000 0000000 jupyter-sphinx-0.5.3/tests/test_execute.py 0000664 0000000 0000000 00000051504 14543263320 0020754 0 ustar 00root root 0000000 0000000 import asyncio
import os
import shutil
import sys
import tempfile
import warnings
from io import StringIO
from pathlib import Path
from unittest.mock import Mock
import pytest
from docutils.nodes import container, image, literal, literal_block, math_block, raw
from nbformat import from_dict
from sphinx.addnodes import download_reference
from sphinx.errors import ExtensionError
from sphinx.testing.util import SphinxTestApp, assert_node
try:
from sphinx.testing.util import path
except ImportError:
path = None
from jupyter_sphinx.ast import (
JupyterCellNode,
JupyterDownloadRole,
JupyterWidgetStateNode,
JupyterWidgetViewNode,
cell_output_to_nodes,
)
from jupyter_sphinx.thebelab import ThebeButtonNode, ThebeOutputNode, ThebeSourceNode
if os.name == "nt":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@pytest.fixture()
def doctree():
source_trees = []
apps = []
syspath = sys.path[:]
def doctree(
source,
config=None,
return_all=False,
entrypoint="jupyter_sphinx",
buildername="html",
):
src_dir = Path(tempfile.mkdtemp())
source_trees.append(src_dir)
conf_contents = "extensions = ['%s']" % entrypoint
if config is not None:
conf_contents += "\n" + config
(src_dir / "conf.py").write_text(conf_contents, encoding="utf8")
(src_dir / "index.rst").write_text(source, encoding="utf8")
warnings = StringIO()
if path is not None:
src_dir = path(src_dir.as_posix())
app = SphinxTestApp(
srcdir=src_dir,
status=StringIO(),
warning=warnings,
buildername=buildername,
)
apps.append(app)
app.build()
doctree = app.env.get_and_resolve_doctree("index", app.builder)
if return_all:
return doctree, app, warnings.getvalue()
else:
return doctree
yield doctree
sys.path[:] = syspath
for app in reversed(apps):
app.cleanup()
for tree in source_trees:
shutil.rmtree(tree)
@pytest.mark.parametrize("buildername", ["html", "singlehtml"])
def test_basic(doctree, buildername):
source = """
.. jupyter-execute::
2 + 2
"""
tree = doctree(source, buildername=buildername)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert not cell.attributes["code_below"]
assert not cell.attributes["hide_code"]
assert not cell.attributes["hide_output"]
assert not cellinput.children[0]["linenos"]
assert cellinput.children[0].astext().strip() == "2 + 2"
assert celloutput.children[0].astext().strip() == "4"
def test_hide_output(doctree):
source = """
.. jupyter-execute::
:hide-output:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cell.attributes["hide_output"]
assert len(celloutput.children) == 0
assert cellinput.children[0].astext().strip() == "2 + 2"
def test_hide_code(doctree):
source = """
.. jupyter-execute::
:hide-code:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(celloutput,) = cell.children
assert cell.attributes["hide_code"]
assert len(cell.children) == 1
assert celloutput.children[0].astext().strip() == "4"
def test_code_below(doctree):
source = """
.. jupyter-execute::
:code-below:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(celloutput, cellinput) = cell.children
assert cell.attributes["code_below"]
assert cellinput.children[0].astext().strip() == "2 + 2"
assert celloutput.children[0].astext().strip() == "4"
def test_linenos(doctree):
source = """
.. jupyter-execute::
:linenos:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cellinput.children[0]["linenos"]
assert len(cell.children) == 2
assert cellinput.children[0].astext().strip() == "2 + 2"
assert celloutput.children[0].astext().strip() == "4"
source = """
.. jupyter-execute::
:linenos:
:code-below:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(celloutput, cellinput) = cell.children
assert cellinput.children[0]["linenos"]
def test_linenos_conf_option(doctree):
source = """
.. jupyter-execute::
2 + 2
"""
tree = doctree(source, config="jupyter_sphinx_linenos = True")
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cellinput.children[0].attributes["linenos"]
assert "highlight_args" not in cellinput.children[0].attributes
assert cellinput.children[0].astext().strip() == "2 + 2"
assert celloutput.children[0].astext().strip() == "4"
def test_continue_linenos_conf_option(doctree):
# Test no linenumbering without linenos config or lineno-start directive
source = """
.. jupyter-execute::
2 + 2
"""
tree = doctree(source, config="jupyter_sphinx_continue_linenos = True")
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert not cellinput.children[0].attributes["linenos"]
assert cellinput.children[0].astext().strip() == "2 + 2"
assert celloutput.children[0].astext().strip() == "4"
# Test continuous line numbering
source = """
.. jupyter-execute::
2 + 2
.. jupyter-execute::
3 + 3
"""
tree = doctree(
source,
config="jupyter_sphinx_linenos = True\n"
"jupyter_sphinx_continue_linenos = True",
)
cell0, cell1 = tree.findall(JupyterCellNode)
(cellinput0, celloutput0) = cell0.children
(cellinput1, celloutput1) = cell1.children
assert cellinput0.children[0].attributes["linenos"]
assert cellinput0.children[0].astext().strip() == "2 + 2"
assert celloutput0.children[0].astext().strip() == "4"
assert cellinput1.children[0].attributes["linenos"]
assert cellinput1.children[0].attributes["highlight_args"]["linenostart"] == 2
assert cellinput1.children[0].astext().strip() == "3 + 3"
assert celloutput1.children[0].astext().strip() == "6"
# Line number should continue after lineno-start option
source = """
.. jupyter-execute::
:lineno-start: 7
2 + 2
.. jupyter-execute::
3 + 3
"""
tree = doctree(
source,
config="jupyter_sphinx_linenos = True\n"
"jupyter_sphinx_continue_linenos = True",
)
cell0, cell1 = tree.findall(JupyterCellNode)
(cellinput0, celloutput0) = cell0.children
(cellinput1, celloutput1) = cell1.children
assert cellinput0.children[0].attributes["highlight_args"]["linenostart"] == 7
assert cellinput0.children[0].astext().strip() == "2 + 2"
assert celloutput0.children[0].astext().strip() == "4"
assert cellinput1.children[0].attributes["linenos"]
assert cellinput1.children[0].attributes["highlight_args"]["linenostart"] == 8
assert cellinput1.children[0].astext().strip() == "3 + 3"
assert celloutput1.children[0].astext().strip() == "6"
def test_emphasize_lines(doctree):
source = """
.. jupyter-execute::
:emphasize-lines: 1,3-5
1 + 1
2 + 2
3 + 3
4 + 4
5 + 5
.. jupyter-execute::
:emphasize-lines: 2, 4
1 + 1
2 + 2
3 + 3
4 + 4
5 + 5
"""
tree = doctree(source)
cell0, cell1 = tree.findall(JupyterCellNode)
assert cell0.attributes["emphasize_lines"] == [1, 3, 4, 5]
assert cell1.attributes["emphasize_lines"] == [2, 4]
def test_execution_environment_carries_over(doctree):
source = """
.. jupyter-execute::
a = 1
.. jupyter-execute::
a += 1
a
"""
tree = doctree(source)
_, cell1 = tree.findall(JupyterCellNode)
(_, celloutput1) = cell1.children
assert celloutput1.children[0].astext().strip() == "2"
def test_kernel_restart(doctree):
source = """
.. jupyter-execute::
a = 1
.. jupyter-kernel::
:id: new-kernel
.. jupyter-execute::
:raises:
a += 1
a
"""
tree = doctree(source)
_, cell1 = tree.findall(JupyterCellNode)
(_, celloutput1) = cell1.children
assert "NameError" in celloutput1.children[0].astext()
def test_raises(doctree):
source = """
.. jupyter-execute::
raise ValueError()
"""
with pytest.raises(ExtensionError):
doctree(source)
source = """
.. jupyter-execute::
:raises:
raise ValueError()
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert "ValueError" in celloutput.children[0].astext()
source = """
.. jupyter-execute::
:raises: KeyError, ValueError
raise ValueError()
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert "ValueError" in celloutput.children[0].astext()
def test_widgets(doctree):
source = """
.. jupyter-execute::
import ipywidgets
ipywidgets.Button()
"""
tree = doctree(source)
assert len(list(tree.findall(JupyterWidgetViewNode))) == 1
assert len(list(tree.findall(JupyterWidgetStateNode))) == 1
def test_javascript(doctree):
source = """
.. jupyter-execute::
from IPython.display import display_javascript, Javascript
Javascript('window.alert("Hello world!")')
"""
tree = doctree(source)
(node,) = list(tree.findall(raw))
(text,) = node.children
assert "world" in text
def test_stdout(doctree):
source = """
.. jupyter-execute::
print('hello world')
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert len(cell.children) == 2
assert celloutput.children[0].astext().strip() == "hello world"
def test_stderr(doctree):
source = """
.. jupyter-execute::
import sys
print('hello world', file=sys.stderr)
"""
tree, _, warnings = doctree(source, return_all=True)
assert "hello world" in warnings
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert len(celloutput) == 0 # no output
source = """
.. jupyter-execute::
:stderr:
import sys
print('hello world', file=sys.stderr)
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert len(cell.children) == 2
assert "stderr" in celloutput.children[0].attributes["classes"]
assert celloutput.children[0].astext().strip() == "hello world"
thebe_config = 'jupyter_sphinx_thebelab_config = {"dummy": True}'
def test_thebe_hide_output(doctree):
source = """
.. jupyter-execute::
:hide-output:
2 + 2
"""
tree = doctree(source, thebe_config)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cell.attributes["hide_output"]
assert len(celloutput.children) == 0
source = cellinput.children[0]
assert type(source) == ThebeSourceNode
assert len(source.children) == 1
assert source.children[0].astext().strip() == "2 + 2"
def test_thebe_hide_code(doctree):
source = """
.. jupyter-execute::
:hide-code:
2 + 2
"""
tree = doctree(source, thebe_config)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cell.attributes["hide_code"]
assert len(cell.children) == 2
source = cellinput.children[0]
assert type(source) == ThebeSourceNode
assert source.attributes["hide_code"]
assert len(source.children) == 1
assert source.children[0].astext().strip() == "2 + 2"
output = celloutput.children[0]
assert type(output) == ThebeOutputNode
assert len(output.children) == 1
assert output.children[0].astext().strip() == "4"
def test_thebe_code_below(doctree):
source = """
.. jupyter-execute::
:code-below:
2 + 2
"""
tree = doctree(source, thebe_config)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, celloutput) = cell.children
assert cell.attributes["code_below"]
output = cellinput.children[0]
assert type(output) is ThebeOutputNode
assert len(output.children) == 1
assert output.children[0].astext().strip() == "4"
source = celloutput.children[0]
assert type(source) is ThebeSourceNode
assert len(source.children) == 1
assert source.children[0].astext().strip() == "2 + 2"
assert source.attributes["code_below"]
def test_thebe_button_auto(doctree):
config = 'jupyter_sphinx_thebelab_config = {"dummy": True}'
source = """
.. jupyter-execute::
1 + 1
"""
tree = doctree(source, config=config)
assert len(list(tree.findall(ThebeButtonNode))) == 1
def test_thebe_button_manual(doctree):
config = 'jupyter_sphinx_thebelab_config = {"dummy": True}'
source = """
.. jupyter-execute::
1 + 1
.. thebe-button::
"""
tree = doctree(source, config)
assert len(list(tree.findall(ThebeButtonNode))) == 1
def test_thebe_button_none(doctree):
config = 'jupyter_sphinx_thebelab_config = {"dummy": True}'
source = "No Jupyter cells"
tree = doctree(source, config)
assert len(list(tree.findall(ThebeButtonNode))) == 0
def test_latex(doctree):
source = r"""
.. jupyter-execute::
from IPython.display import Latex
Latex(r"{}\int{}")
"""
delimiter_pairs = (pair.split() for pair in r"\( \),\[ \],$$ $$,$ $".split(","))
for start, end in delimiter_pairs:
tree = doctree(source.format(start, end))
(cell,) = tree.findall(JupyterCellNode)
(_, celloutput) = cell.children
assert next(celloutput.findall(math_block)).astext() == r"\int"
def test_cell_output_to_nodes(doctree):
# tests the image uri paths on conversion to docutils image nodes
output_dir = "/_build/jupyter_execute"
img_locs = [
"/_build/jupyter_execute/docs/image_1.png",
"/_build/jupyter_execute/image_2.png",
]
cells = [
{
"outputs": [
{
"data": {
"image/png": "Vxb6L1wAAAABJRU5ErkJggg==\n",
"text/plain": "",
},
"metadata": {"filenames": {"image/png": img_locs[0]}},
"output_type": "display_data",
}
]
},
{
"outputs": [
{
"data": {
"image/png": "iVBOJggg==\n",
"text/plain": "",
},
"metadata": {"filenames": {"image/png": img_locs[1]}},
"output_type": "display_data",
}
]
},
]
for index, cell in enumerate(cells):
cell = from_dict(cell)
(output_node,) = cell_output_to_nodes(cell["outputs"], True, output_dir, None)
(image_node,) = output_node.findall(image)
assert image_node.attributes["uri"] == img_locs[index]
# Testing inline functionality
outputs = [
{"name": "stdout", "output_type": "stream", "text": ["hi\n"]},
{"name": "stderr", "output_type": "stream", "text": ["hi\n"]},
]
output_nodes = cell_output_to_nodes(outputs, True, output_dir, None)
for output, kind in zip(output_nodes, [literal_block, container]):
assert isinstance(output, kind)
output_nodes = cell_output_to_nodes(outputs, True, output_dir, None, inline=True)
for output, kind in zip(output_nodes, [literal, literal]):
assert isinstance(output, kind)
@pytest.mark.parametrize(
"text,reftarget,caption",
(
("nb_name", "/../jupyter_execute/path/to/nb_name.ipynb", "nb_name.ipynb"),
("../nb_name", "/../jupyter_execute/path/nb_name.ipynb", "../nb_name.ipynb"),
("text ", "/../jupyter_execute/path/to/nb_name.ipynb", "text"),
),
)
def test_download_role(text, reftarget, caption, tmp_path):
role = JupyterDownloadRole()
mock_inliner = Mock()
config = {
"document.settings.env.app.outdir": str(tmp_path),
"document.settings.env.docname": "path/to/docname",
"document.settings.env.srcdir": str(tmp_path),
"document.settings.env.app.srcdir": str(tmp_path),
"reporter.get_source_and_line": lambda line: ("source", line),
}
mock_inliner.configure_mock(**config)
ret, msg = role("jupyter-download-notebook", text, text, 0, mock_inliner)
if os.name == "nt":
# Get equivalent abs path for Windows
reftarget = (Path(tmp_path) / reftarget[1:]).resolve().as_posix()
assert_node(ret[0], [download_reference], reftarget=reftarget)
assert_node(ret[0][0], [literal, caption])
assert msg == []
def test_save_script(doctree):
source = """
.. jupyter-kernel:: python3
:id: test
.. jupyter-execute::
a = 1
print(a)
"""
_, app, _ = doctree(source, return_all=True)
outdir = Path(app.outdir)
saved_text = (outdir / "../jupyter_execute/test.py").read_text()
assert saved_text.startswith("#!/usr/bin/env python")
assert "print(a)" in saved_text
def test_bash_kernel(doctree):
pytest.importorskip("bash_kernel")
if sys.platform == "win32":
pytest.skip("Not trying bash on windows.")
# we set enable-bracketed-paste off
# to avoid bash_kernel accidentally raising errors
# (related to https://github.com/takluyver/bash_kernel/issues/107)
source = """
.. jupyter-kernel:: bash
:id: test
.. jupyter-execute::
bind 'set enable-bracketed-paste off'
echo "foo"
"""
with warnings.catch_warnings():
# See https://github.com/takluyver/bash_kernel/issues/105
warnings.simplefilter("ignore", DeprecationWarning)
_, app, _ = doctree(source, return_all=True)
outdir = Path(app.outdir)
saved_text = (outdir / "../jupyter_execute/test.sh").read_text()
assert 'echo "foo"' in saved_text
def test_input_cell(doctree):
source = """
.. jupyter-input::
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, empty) = cell.children
assert cell.attributes["hide_output"] is True
assert cellinput.children[0].attributes["linenos"] is False
assert cellinput.children[0].astext().strip() == "2 + 2"
assert len(empty.children) == 0
def test_input_cell_linenos(doctree):
source = """
.. jupyter-input::
:linenos:
2 + 2
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(cellinput, empty) = cell.children
assert cell.attributes["hide_output"] is True
assert cellinput.children[0].attributes["linenos"] is True
assert cellinput.children[0].astext().strip() == "2 + 2"
assert len(empty.children) == 0
def test_output_cell(doctree):
source = """
.. jupyter-input::
3 + 2
.. jupyter-output::
4
"""
tree = doctree(source)
(cell,) = tree.findall(JupyterCellNode)
(
cellinput,
celloutput,
) = cell.children
assert cellinput.children[0].astext().strip() == "3 + 2"
assert celloutput.children[0].astext().strip() == "4"
def test_output_only_error(doctree):
source = """
.. jupyter-output::
4
"""
with pytest.raises(ExtensionError):
doctree(source)
def test_multiple_directives(doctree):
source = """
.. jupyter-execute::
2 + 2
.. jupyter-input::
3 + 3
.. jupyter-output::
5
"""
tree = doctree(source)
(ex, jin) = tree.findall(JupyterCellNode)
(ex_in, ex_out) = ex.children
(jin_in, jin_out) = jin.children
assert ex_in.children[0].astext().strip() == "2 + 2"
assert ex_out.children[0].astext().strip() == "4"
assert jin_in.children[0].astext().strip() == "3 + 3"
assert jin_out.children[0].astext().strip() == "5"
def test_builder_priority(doctree):
source = """
.. jupyter-execute::
display({"text/plain": "I am html output", "text/latex": "I am latex"})
"""
config = (
"render_priority_html = ['text/plain', 'text/latex']\n"
"render_priority_latex = ['text/latex', 'text/plain']"
)
_, app, _ = doctree(source, config=config, return_all=True, buildername="html")
html = (Path(app.outdir) / "index.html").read_text()
assert "I am html output" in html
_, app, _ = doctree(source, config=config, return_all=True, buildername="latex")
latex = (Path(app.outdir) / "python.tex").read_text()
assert "I am latex" in latex