Source code for sphinx_toolbox.code

#!/usr/bin/env python3
#
#  code.py
"""
Customised ``.. code-block::`` directive with an adjustable indent size.

.. extensions:: sphinx_toolbox.code


Usage
------

.. rst:directive:: .. code-block:: [language]
                   .. sourcecode:: [language]

	Customised ``.. code-block::`` directive with an adjustable indent size.

	.. rst:directive:option:: tab-width: width
		:type: integer

		Sets the size of the indentation in spaces.


	All other options from :rst:dir:`sphinx:code-block` are available,
	see the `Sphinx documentation`_ for details.

	.. _Sphinx documentation: https://www.sphinx-doc.org/en/3.x/usage/restructuredtext/directives.html#directive-code-block

	**Examples**

	.. rest-example::

		.. code-block:: python

			def print(text):
				sys.stdout.write(text)


	.. rest-example::

		.. code-block:: python
			:tab-width: 8

			def print(text):
				sys.stdout.write(text)

.. clearpage::

.. rst:directive:: .. code-cell:: [language]
				   .. output-cell:: [language]

	Customised ``.. code-block::`` directives which display an execution count to
	the left of the code block, similar to a Jupyter Notebook cell.

	.. versionadded:: 2.6.0

	.. rst:directive:option:: execution-count: count
		:type: positive integer

		The execution count of the cell.

	All other options from the :rst:dir:`code-block` directive above are available.

	**Examples**

	.. rest-example::

		.. code-cell:: python
			:execution-count: 1

			def print(text):
				sys.stdout.write(text)

			print("hello world")

		.. output-cell::
			:execution-count: 1

			hello world


	.. rest-example::

		.. code-cell:: python
			:execution-count: 2
			:tab-width: 8

			def print(text):
				sys.stdout.write(text)


	.. seealso::

		`nbsphinx <https://nbsphinx.readthedocs.io/en/0.8.3/>`_,
		which inspired these directives and provides additional functionality
		for integrating Jupyter Notebooks with Sphinx.

API Reference
----------------

"""
#
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#
#  Based on Sphinx
#  Copyright (c) 2007-2020 by the Sphinx team.
#  |  All rights reserved.
#  |
#  |  Redistribution and use in source and binary forms, with or without
#  |  modification, are permitted provided that the following conditions are
#  |  met:
#  |
#  |  * Redistributions of source code must retain the above copyright
#  |    notice, this list of conditions and the following disclaimer.
#  |
#  |  * 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.
#  |
#  |  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.
#

# stdlib
from typing import List, MutableMapping, Optional

# 3rd party
import dict2css
import docutils.nodes
import docutils.statemachine
import sphinx.directives.code
from docutils.nodes import Node
from docutils.parsers.rst import directives
from domdf_python_tools.paths import PathPlus
from domdf_python_tools.stringlist import StringList
from domdf_python_tools.utils import convert_indents
from sphinx.application import Sphinx
from sphinx.writers.html import HTMLTranslator
from sphinx.writers.latex import LaTeXTranslator

# this package
from sphinx_toolbox.utils import Config, OptionSpec, SphinxExtMetadata, metadata_add_version

__all__ = (
		"CodeBlock",
		"CodeCell",
		"OutputCell",
		"Prompt",
		"visit_prompt_html",
		"visit_prompt_latex",
		"copy_asset_files",
		"configure",
		"setup",
		)


[docs]class CodeBlock(sphinx.directives.code.CodeBlock): """ Directive for a code block with special highlighting or line numbering settings. The indent_size can be adjusted with the ``:tab-width: <int>`` option. .. autoclasssumm:: CodeBlock :autosummary-sections: ;; """ option_spec: OptionSpec = { # type: ignore[assignment] "force": directives.flag, "linenos": directives.flag, "tab-width": int, "dedent": int, "lineno-start": int, "emphasize-lines": directives.unchanged_required, "caption": directives.unchanged_required, "class": directives.class_option, "name": directives.unchanged, }
[docs] def run(self) -> List[Node]: """ Process the content of the code block. """ code = '\n'.join(self.content) if "tab-width" in self.options: tab_width = self.options["tab-width"] else: tab_width = 4 code = convert_indents(code, tab_width=tab_width, from_=' ' * self.config.docutils_tab_width) self.content = docutils.statemachine.StringList(code.split('\n')) return super().run()
[docs]class Prompt(docutils.nodes.General, docutils.nodes.FixedTextElement): """ Represents a cell prompt for a :class:`CodeCell` and :class:`OutputCell`. .. versionadded:: 2.6.0 """
[docs]class CodeCell(CodeBlock): """ Customised code block which displays an execution count to the left of the code block, similar to a Jupyter Notebook cell. The indent_size can be adjusted with the ``:tab-width: <int>`` option. The execution count can be set using the ``:execution-count: <int>`` option. .. autoclasssumm:: CodeCell :autosummary-sections: ;; .. versionadded:: 2.6.0 """ # noqa: D400 option_spec: OptionSpec = { **CodeBlock.option_spec, "execution-count": directives.positive_int, } _prompt: str = "In [%s]:" _class: str = "code-cell"
[docs] def run(self) -> List[Node]: """ Process the content of the code block. """ self.options.setdefault("class", []) self.options["class"].append(f"{self._class}-code") prompt = self._prompt % self.options.get("execution-count", ' ') outer_node = docutils.nodes.container(classes=[self._class]) outer_node += Prompt( prompt, prompt, language="none", classes=["prompt", f"{self._class}-prompt"], ) outer_node += super().run()[0] return [outer_node]
[docs]class OutputCell(CodeCell): """ Variant of :class:`~.CodeCell` for displaying the output of a cell in a Jupyter Notebook. The indent_size can be adjusted with the ``:tab-width: <int>`` option. The execution count can be set using the ``:execution-count: <int>`` option. .. versionadded:: 2.6.0 .. autoclasssumm:: OutputCell :autosummary-sections: ;; """ _prompt: str = "[%s]:" _class: str = "output-cell"
[docs]def visit_prompt_html(translator: HTMLTranslator, node: Prompt) -> None: """ Visit a :class:`~.Prompt` node with the HTML translator. .. versionadded:: 2.6.0 :param translator: :param node: """ starttag = translator.starttag(node, "div", suffix='', CLASS="notranslate") translator.body.append(starttag + node.rawsource + '</div>\n') raise docutils.nodes.SkipNode
[docs]def visit_prompt_latex(translator: LaTeXTranslator, node: Prompt) -> None: """ Visit a :class:`~.Prompt` node with the LaTeX translator. .. versionadded:: 2.6.0 :param translator: :param node: """ translator.body.append("\n\n") translator.body.append(r"\vspace{4mm}") if f"code-cell-prompt" in node["classes"]: colour = "nbsphinxin" elif f"output-cell-prompt" in node["classes"]: colour = "nbsphinxout" else: # pragma: no cover colour = "black" translator.body.append( rf"\llap{{\color{{{colour}}}\texttt{{{node.rawsource}}}" r"\,\hspace{\fboxrule}\hspace{\fboxrule}\hspace{\fboxsep}}" ) translator.body.append(r"\vspace{-7mm}") raise docutils.nodes.SkipNode
[docs]def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. .. versionadded:: 2.6.0 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return prompt_style: dict2css.Style = { "user-select": None, "font-size": "13px", "font-family": '"SFMono-Regular", Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace', "border": None, "padding": "11px 0 0", "margin": "0 5px 0 0", "box-shadow": None, "wrap-option": None, "white-space": "nowrap", } container_style: dict2css.Style = { "padding-top": "5px", "display": "flex", "align-items": "stretch", "margin": 0, } code_style_string = "div.code-cell.container div.code-cell-code, div.output-cell.container div.output-cell-code" code_style: dict2css.Style = { "width": "100%", "padding-top": 0, "margin-top": 0, } style: MutableMapping[str, dict2css.Style] = { "div.code-cell.container div.prompt": {"color": "#307FC1"}, "div.output-cell.container div.prompt": {"color": "#BF5B3D"}, "div.code-cell.container div.prompt, div.output-cell.container div.prompt": prompt_style, "div.code-cell.container, div.output-cell.container": container_style, code_style_string: code_style, } static_dir = PathPlus(app.outdir) / "_static" static_dir.maybe_make(parents=True) dict2css.dump(style, static_dir / "sphinx-toolbox-code.css")
[docs]def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.code`. .. versionadded:: 2.9.0 :param app: The Sphinx application. :param config: """ latex_elements = getattr(config, "latex_elements", {}) latex_preamble = StringList(latex_elements.get("preamble", '')) latex_preamble.blankline() latex_preamble.append(r"\definecolor{nbsphinxin}{HTML}{307FC1}") latex_preamble.append(r"\definecolor{nbsphinxout}{HTML}{BF5B3D}") latex_elements["preamble"] = str(latex_preamble) config.latex_elements = latex_elements
[docs]@metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.code`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ # Code block with customisable indent size. app.add_directive("code-block", CodeBlock, override=True) app.add_directive("sourcecode", CodeBlock, override=True) app.add_directive("code-cell", CodeCell) app.add_directive("output-cell", OutputCell) # Hack to get the docutils tab size, as there doesn't appear to be any other way app.setup_extension("sphinx_toolbox.tweaks.tabsize") app.add_node( Prompt, html=(visit_prompt_html, lambda *args, **kwargs: None), latex=(visit_prompt_latex, lambda *args, **kwargs: None) ) app.connect("config-inited", configure) app.add_css_file("sphinx-toolbox-code.css") app.connect("build-finished", copy_asset_files) return {"parallel_read_safe": True}