Skip to content

Commit 4558fb8

Browse files
authored
Merge branch 'master' into inline_nodes
2 parents f5e8e2c + 6eb309d commit 4558fb8

File tree

14 files changed

+152
-83
lines changed

14 files changed

+152
-83
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
name: Build documentation
1919
command: |
2020
cd doc
21-
make html
21+
make html-strict
2222
2323
- store_artifacts:
2424
path: doc/build/html/

.github/workflows/tests.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ jobs:
66

77
tests:
88

9-
runs-on: ubuntu-latest
109
strategy:
1110
matrix:
12-
python-version: [3.5, 3.6, 3.7, 3.8]
11+
python-version: [3.6, 3.7, 3.8]
12+
os: [ubuntu-latest]
13+
include:
14+
- os: windows-latest
15+
python-version: 3.7
16+
17+
runs-on: ${{ matrix.os }}
1318

1419
steps:
1520
- uses: actions/checkout@v2

doc/Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@ help:
1616
# Catch-all target: route all unknown targets to Sphinx using the new
1717
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
1818
%: Makefile
19-
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
19+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
20+
21+
# raise warnings to errors
22+
html-strict:
23+
@$(SPHINXBUILD) -b html -nW --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
24+
25+
clean:
26+
rm -r $(BUILDDIR)

doc/source/index.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,14 +317,18 @@ Pygments.
317317

318318
Downloading the code as a script
319319
--------------------------------
320+
320321
Jupyter Sphinx includes 2 roles that can be used to download the code embedded in a document:
321-
``:jupyter-download:script:`` (for a raw script file) and ``:jupyter-download:notebook:`` (for
322-
a Jupyter notebook). For example, to download all the code from this document as a script we
322+
``:jupyter-download:script:`` (for a raw script file) and ``:jupyter-download:notebook:`` or ``:jupyter-download:nb:`` (for
323+
a Jupyter notebook).
324+
325+
These roles are equivalent to the standard sphinx `download role <https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-download>`__, **except** the extension of the file should not be given.
326+
For example, to download all the code from this document as a script we
323327
would use::
324328

325-
:jupyter-download:script:`index`
329+
:jupyter-download:script:`click to download <index>`
326330

327-
Which produces a link like this: :jupyter-download:script:`index`. The name that the role is
331+
Which produces a link like this: :jupyter-download:script:`click to download <index>`. The target that the role is
328332
applied to (``index`` in this case) is the name of the document for which you wish to download
329333
the code. If a document contains ``jupyter-kernel`` directives with ``:id:`` specified, then
330334
the name provided to ``:id:`` can be used to get the code for the cells belonging to the

jupyter_sphinx/__init__.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sphinx.util.fileutil import copy_asset
99
from sphinx.errors import ExtensionError
1010
from IPython.lib.lexers import IPythonTracebackLexer, IPython3Lexer
11+
from pathlib import Path
1112

1213
from .ast import (
1314
JupyterCell,
@@ -19,7 +20,7 @@
1920
JupyterWidgetViewNode,
2021
JupyterWidgetStateNode,
2122
WIDGET_VIEW_MIMETYPE,
22-
jupyter_download_role,
23+
JupyterDownloadRole,
2324
CellOutputsToNodes,
2425
)
2526
from .execute import JupyterKernel, ExecuteJupyterCells
@@ -121,18 +122,20 @@ def build_finished(app, env):
121122
if app.builder.format != "html":
122123
return
123124

125+
module_dir = Path(__file__).parent
126+
outdir = Path(app.outdir)
127+
124128
# Copy stylesheet
125-
src = os.path.join(os.path.dirname(__file__), "css")
126-
dst = os.path.join(app.outdir, "_static")
129+
src = module_dir / "css"
130+
dst = outdir / "_static"
127131
copy_asset(src, dst)
128132

129133
thebe_config = app.config.jupyter_sphinx_thebelab_config
130134
if not thebe_config:
131135
return
132136

133137
# Copy all thebelab related assets
134-
src = os.path.join(os.path.dirname(__file__), "thebelab")
135-
dst = os.path.join(app.outdir, "_static")
138+
src = module_dir / "thebelab"
136139
copy_asset(src, dst)
137140

138141

@@ -272,14 +275,15 @@ def setup(app):
272275
app.add_directive("jupyter-execute", JupyterCell)
273276
app.add_directive("jupyter-kernel", JupyterKernel)
274277
app.add_directive("thebe-button", ThebeButton)
275-
app.add_role("jupyter-download:notebook", jupyter_download_role)
276-
app.add_role("jupyter-download:script", jupyter_download_role)
278+
app.add_role("jupyter-download:notebook", JupyterDownloadRole())
279+
app.add_role("jupyter-download:nb", JupyterDownloadRole())
280+
app.add_role("jupyter-download:script", JupyterDownloadRole())
277281
app.add_transform(ExecuteJupyterCells)
278282
app.add_post_transform(CellOutputsToNodes)
279283

280284
# For syntax highlighting
281-
app.add_lexer("ipythontb", IPythonTracebackLexer())
282-
app.add_lexer("ipython", IPython3Lexer())
285+
app.add_lexer("ipythontb", IPythonTracebackLexer)
286+
app.add_lexer("ipython", IPython3Lexer)
283287

284288
app.connect("builder-inited", builder_inited)
285289
app.connect("build-finished", build_finished)

jupyter_sphinx/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version_info = (0, 2, 5, "alpha", 1)
1+
version_info = (0, 3, 0, "alpha", 1)
22

33
_specifier_ = {"alpha": "a", "beta": "b", "candidate": "rc", "final": ""}
44

jupyter_sphinx/ast.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import os
44
import json
5+
from pathlib import Path
56

67
import docutils
78
from docutils.parsers.rst import Directive, directives
8-
from docutils.nodes import math_block, image
9+
from docutils.nodes import math_block, image, literal
910
from sphinx.util import parselinenos
11+
from sphinx.util.docutils import ReferenceRole
1012
from sphinx.addnodes import download_reference
1113
from sphinx.transforms import SphinxTransform
1214
from sphinx.environment.collectors.asset import ImageCollector
@@ -100,7 +102,7 @@ def run(self):
100102
location=location,
101103
)
102104
try:
103-
with open(filename) as f:
105+
with Path(filename).open() as f:
104106
content = [line.rstrip() for line in f.readlines()]
105107
except (IOError, OSError):
106108
raise IOError("File {} not found or reading it failed".format(filename))
@@ -227,7 +229,7 @@ def html(self):
227229
)
228230

229231

230-
def cell_output_to_nodes(outputs, data_priority, write_stderr, dir,
232+
def cell_output_to_nodes(outputs, data_priority, write_stderr, out_dir,
231233
thebe_config, inline=False):
232234
"""Convert a jupyter cell with outputs and filenames to doctree nodes.
233235
@@ -238,7 +240,7 @@ def cell_output_to_nodes(outputs, data_priority, write_stderr, dir,
238240
Which media types to prioritize.
239241
write_stderr : bool
240242
If True include stderr in cell output
241-
dir : string
243+
out_dir : string
242244
Sphinx "absolute path" to the output folder, so it is a relative path
243245
to the source folder prefixed with ``/``.
244246
thebe_config: dict
@@ -317,19 +319,17 @@ def cell_output_to_nodes(outputs, data_priority, write_stderr, dir,
317319
continue
318320
data = output["data"][mime_type]
319321
if mime_type.startswith("image"):
322+
file_path = Path(output.metadata["filenames"][mime_type])
323+
out_dir = Path(out_dir)
320324
# Sphinx treats absolute paths as being rooted at the source
321325
# directory, so make a relative path, which Sphinx treats
322326
# as being relative to the current working directory.
323-
filename = os.path.basename(output.metadata["filenames"][mime_type])
327+
filename = file_path.name
324328

325-
# checks if file dir path is inside a subdir of dir
326-
filedir = os.path.dirname(output.metadata["filenames"][mime_type])
327-
subpaths = filedir.split(dir)
328-
if subpaths and len(subpaths) > 1:
329-
subpath = subpaths[1]
330-
dir += subpath
329+
if out_dir in file_path.parents:
330+
out_dir = file_path.parent
331331

332-
uri = os.path.join(dir, filename)
332+
uri = (out_dir / filename).as_posix()
333333
to_add.append(docutils.nodes.image(uri=uri))
334334
elif mime_type == "text/html":
335335
to_add.append(
@@ -407,16 +407,19 @@ def attach_outputs(output_nodes, node, thebe_config, cm_language):
407407
node.children = node.children[::-1]
408408

409409

410-
def jupyter_download_role(name, rawtext, text, lineno, inliner):
411-
_, filetype = name.split(":")
412-
assert filetype in ("notebook", "script")
413-
ext = ".ipynb" if filetype == "notebook" else ".py"
414-
output_dir = sphinx_abs_dir(inliner.document.settings.env)
415-
download_file = text + ext
416-
node = download_reference(
417-
download_file, download_file, reftarget=os.path.join(output_dir, download_file)
418-
)
419-
return [node], []
410+
class JupyterDownloadRole(ReferenceRole):
411+
def run(self):
412+
_, filetype = self.name.split(":")
413+
414+
assert filetype in ("notebook", "nb", "script")
415+
ext = ".ipynb" if filetype in ("notebook", "nb") else ".py"
416+
download_file = self.target + ext
417+
reftarget = sphinx_abs_dir(self.env, download_file)
418+
node = download_reference(self.rawtext, reftarget=reftarget)
419+
self.set_source_info(node)
420+
title = self.title if self.has_explicit_title else download_file
421+
node += literal(self.rawtext, title, classes=["xref", "download"])
422+
return [node], []
420423

421424

422425
def get_widgets(notebook):

jupyter_sphinx/execute.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Execution and managing kernels."""
22

33
import os
4+
from pathlib import Path
45

56
from sphinx.transforms import SphinxTransform
67
from sphinx.errors import ExtensionError
@@ -22,7 +23,6 @@
2223
default_notebook_names,
2324
output_directory,
2425
split_on,
25-
sphinx_abs_dir,
2626
blank_nb,
2727
)
2828
from .ast import (
@@ -87,8 +87,9 @@ class ExecuteJupyterCells(SphinxTransform):
8787

8888
def apply(self):
8989
doctree = self.document
90-
doc_relpath = os.path.dirname(self.env.docname) # relative to src dir
91-
docname = os.path.basename(self.env.docname)
90+
docname_path = Path(self.env.docname)
91+
doc_dir_relpath = docname_path.parent # relative to src dir
92+
docname = docname_path.name
9293
default_kernel = self.config.jupyter_execute_default_kernel
9394
default_names = default_notebook_names(docname)
9495
thebe_config = self.config.jupyter_sphinx_thebelab_config
@@ -106,7 +107,7 @@ def apply(self):
106107
add_thebelab_library(doctree, self.env)
107108

108109
js.logger.info("executing {}".format(docname))
109-
output_dir = os.path.join(output_directory(self.env), doc_relpath)
110+
output_dir = Path(output_directory(self.env)) / doc_dir_relpath
110111

111112
# Start new notebook whenever a JupyterKernelNode is encountered
112113
jupyter_nodes = (JupyterCellNode, JupyterKernelNode)
@@ -206,7 +207,7 @@ def apply(self):
206207
# Write certain cell outputs (e.g. images) to separate files, and
207208
# modify the metadata of the associated cells in 'notebook' to
208209
# include the path to the output file.
209-
write_notebook_output(notebook, output_dir, file_name)
210+
write_notebook_output(notebook, str(output_dir), file_name, self.env.docname)
210211

211212
try:
212213
cm_language = notebook.metadata.language_info.codemirror_mode.name
@@ -239,7 +240,7 @@ def execute_cells(kernel_name, cells, execute_kwargs):
239240
return notebook
240241

241242

242-
def write_notebook_output(notebook, output_dir, notebook_name):
243+
def write_notebook_output(notebook, output_dir, notebook_name, location=None):
243244
"""Extract output from notebook cells and write to files in output_dir.
244245
245246
This also modifies 'notebook' in-place, adding metadata to each cell that
@@ -256,11 +257,20 @@ def write_notebook_output(notebook, output_dir, notebook_name):
256257
resources,
257258
os.path.join(output_dir, notebook_name + ".ipynb"),
258259
)
259-
# Write a script too.
260-
ext = notebook.metadata.language_info.file_extension
260+
# Write a script too. Note that utf-8 is the de facto
261+
# standard encoding for notebooks.
262+
ext = notebook.metadata.get("language_info", {}).get("file_extension", None)
263+
if ext is None:
264+
ext = ".txt"
265+
js.logger.warning(
266+
"Notebook code has no file extension metadata, " "defaulting to `.txt`",
267+
location=location,
268+
)
261269
contents = "\n\n".join(cell.source for cell in notebook.cells)
262-
with open(os.path.join(output_dir, notebook_name + ext), "w") as f:
263-
f.write(contents)
270+
271+
notebook_file = notebook_name + ext
272+
output_dir = Path(output_dir)
273+
(output_dir / notebook_file).write_text(contents, encoding = "utf8")
264274

265275

266276
def contains_widgets(notebook):

jupyter_sphinx/thebelab.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import docutils
55
from docutils.parsers.rst import Directive
6+
from pathlib import Path
67

78
import jupyter_sphinx as js
89

@@ -86,23 +87,23 @@ def add_thebelab_library(doctree, env):
8687
if isinstance(thebe_config, dict):
8788
pass
8889
elif isinstance(thebe_config, str):
89-
if os.path.isabs(thebe_config):
90+
thebe_config = Path(thebe_config)
91+
if thebe_config.is_absolute():
9092
filename = thebe_config
9193
else:
92-
filename = os.path.join(os.path.abspath(env.app.srcdir), thebe_config)
94+
filename = Path(env.app.srcdir).resolve() / thebe_config
9395

94-
if not os.path.exists(filename):
96+
if not filename.exists():
9597
js.logger.warning("The supplied thebelab configuration file does not exist")
9698
return
9799

98-
with open(filename, "r") as config_file:
99-
try:
100-
thebe_config = json.load(config_file)
101-
except ValueError:
102-
js.logger.warning(
103-
"The supplied thebelab configuration file is not in JSON format."
104-
)
105-
return
100+
try:
101+
thebe_config = json.loads(filename.read_text())
102+
except ValueError:
103+
js.logger.warning(
104+
"The supplied thebelab configuration file is not in JSON format."
105+
)
106+
return
106107
else:
107108
js.logger.warning(
108109
"The supplied thebelab configuration should be either a filename or a dictionary."

jupyter_sphinx/utils.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sphinx.errors import ExtensionError
55
import nbformat
66
from jupyter_client.kernelspec import get_kernel_spec, NoSuchKernel
7+
from pathlib import Path
78

89

910
def blank_nb(kernel_name):
@@ -69,17 +70,19 @@ def language_info(executor):
6970
return info_msg["content"]["language_info"]
7071

7172

72-
def sphinx_abs_dir(env):
73+
def sphinx_abs_dir(env, *paths):
7374
# We write the output files into
7475
# output_directory / jupyter_execute / path relative to source directory
7576
# Sphinx expects download links relative to source file or relative to
7677
# source dir and prepended with '/'. We use the latter option.
77-
return "/" + os.path.relpath(
78-
os.path.abspath(
79-
os.path.join(output_directory(env), os.path.dirname(env.docname))
80-
),
81-
os.path.abspath(env.app.srcdir),
82-
)
78+
out_path = (output_directory(env) / Path(env.docname).parent / Path(*paths)).resolve()
79+
80+
if os.name == "nt":
81+
# Can't get relative path between drives on Windows
82+
return out_path.as_posix()
83+
84+
# Path().relative_to() doesn't work when not a direct subpath
85+
return "/" + os.path.relpath(out_path, env.app.srcdir)
8386

8487

8588
def output_directory(env):
@@ -90,6 +93,4 @@ def output_directory(env):
9093

9194
# Note: we are using an implicit fact that sphinx output directories are
9295
# direct subfolders of the build directory.
93-
return os.path.abspath(
94-
os.path.join(env.app.outdir, os.path.pardir, "jupyter_execute")
95-
)
96+
return (Path(env.app.outdir) / os.path.pardir / "jupyter_execute").resolve()

0 commit comments

Comments
 (0)