import os
import logging
from typing import Union # noqa: F401
from docutils import nodes # noqa E501
from docutils.parsers import rst
from ipypublish.utils import handle_error
from ipypublish.sphinx.utils import import_sphinx
from ipypublish.convert.main import IpyPubMain
# TODO should inherit from sphinx.parsers.RSTParser
# https://www.sphinx-doc.org/en/master/extdev/parserapi.html
# however, sphinx is an optional dependency
[docs]class NBParser(rst.Parser):
"""Sphinx source parser for Jupyter notebooks.
adapted from nbsphinx
"""
supported = ("jupyter_notebook",)
def __init__(self, *args, **kwargs):
self.app = None
self.config = None
self.env = None
try:
sphinx = import_sphinx()
class NotebookError(sphinx.errors.SphinxError):
"""Error during notebook parsing."""
category = "Notebook error"
self.error_nb = NotebookError
self.error_config = sphinx.errors.ConfigError
self.logger = sphinx.util.logging.getLogger("nbparser")
except (ImportError, AttributeError):
self.error_nb = IOError
self.error_config = TypeError
self.logger = logging.getLogger("nbparser")
super(NBParser, self).__init__(*args, **kwargs)
[docs] def set_application(self, app):
"""set_application will be called from Sphinx to set app
and other instance variables
Parameters
----------
app: sphinx.application.Sphinx
Sphinx application object
"""
self.app = app
self.config = app.config
self.env = app.env
[docs] def parse(self, inputstring, document):
# type: (Union[str, list[str]], nodes.document) -> None
"""Parse text and generate a document tree."""
# fix for when calling on readthedocs
self.env = self.env or document.settings.env
self.config = self.config or document.settings.env.config
# get file for conversion
filepath = self.env.doc2path(self.env.docname)
filedir = os.path.dirname(filepath)
self.logger.info("ipypublish: converting {}".format(filepath))
config = {
"IpyPubMain": {
"conversion": self.config.ipysphinx_export_config,
"plugin_folder_paths": self.config.ipysphinx_config_folders,
"outpath": filedir,
"folder_suffix": self.config.ipysphinx_folder_suffix,
"log_to_stdout": False,
"log_to_file": False,
"default_pporder_kwargs": dict(clear_existing=False, dump_files=True),
}
}
if self.config.ipysphinx_preconverters:
# NB: jupytext is already a default for .Rmd
config["IpyPubMain"][
"pre_conversion_funcs"
] = self.config.ipysphinx_preconverters
publish = IpyPubMain(config=config)
outdata = publish(filepath)
self.logger.info("ipypublish: successful conversion")
# check we got back restructuredtext
exporter = outdata["exporter"]
if not exporter.output_mimetype == "text/restructuredtext":
handle_error(
"ipypublish: the output content is not of type "
"text/restructuredtext: {}".format(exporter.output_mimetype),
TypeError,
self.logger,
)
# TODO document use of orphan
if outdata["resources"].get("ipub", {}).get("orphan", False):
rst.Parser.parse(self, ":orphan:", document)
# parse a prolog
if self.env.config.ipysphinx_prolog:
prolog = exporter.environment.from_string(
self.env.config.ipysphinx_prolog
).render(env=self.env)
rst.Parser.parse(self, prolog, document)
# parse the main body of the file
rst.Parser.parse(self, outdata["stream"], document)
# parse an epilog
if self.env.config.ipysphinx_epilog:
prolog = exporter.environment.from_string(
self.env.config.ipysphinx_epilog
).render(env=self.env)
rst.Parser.parse(self, prolog, document)
# TODO is there a better way to parse data back from the parser?
# record if the notebook contains ipywidgets
if outdata["resources"].get("contains_ipywidgets", False):
if not hasattr(self.env, "ipysphinx_widgets"):
self.env.ipysphinx_widgets = set()
self.env.ipysphinx_widgets.add(self.env.docname)
# record that the document was created from a notebook
if not hasattr(self.env, "ipysphinx_created_from_nb"):
self.env.ipysphinx_created_from_nb = set()
self.env.ipysphinx_created_from_nb.add(self.env.docname)