Skip to content

Commit

Permalink
✨ NEW: nb_merge_streams configuration
Browse files Browse the repository at this point in the history
If `True`, ensure all stdout / stderr output streams are merged into single outputs.
This ensures deterministic outputs.
  • Loading branch information
chrisjsewell committed Oct 3, 2021
1 parent c54becd commit a097602
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/use/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,7 @@ Then for parsing and output rendering:
* - `nb_output_stderr`
- `show`
- One of 'show', 'remove', 'warn', 'error' or 'severe', [see here](use/format/stderr) for details.
* - `nb_merge_streams`
- `False`
- If `True`, ensure all stdout / stderr output streams are merged into single outputs. This ensures deterministic outputs.
`````
1 change: 1 addition & 0 deletions myst_nb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def visit_element_html(self, node):
app.add_config_value("nb_render_plugin", "default", "env")
app.add_config_value("nb_render_text_lexer", "myst-ansi", "env")
app.add_config_value("nb_output_stderr", "show", "env")
app.add_config_value("nb_merge_streams", False, "env")

# Register our post-transform which will convert output bundles to nodes
app.add_post_transform(PasteNodesToDocutils)
Expand Down
55 changes: 55 additions & 0 deletions myst_nb/render_outputs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""A Sphinx post-transform, to convert notebook outpus to AST nodes."""
import os
import re
from abc import ABC, abstractmethod
from typing import List, Optional
from unittest import mock
Expand Down Expand Up @@ -91,6 +92,58 @@ def load_renderer(name: str) -> "CellOutputRendererBase":
raise MystNbEntryPointError(f"No Entry Point found for myst_nb.mime_render:{name}")


RGX_CARRIAGERETURN = re.compile(r".*\r(?=[^\n])")
RGX_BACKSPACE = re.compile(r"[^\n]\b")


def coalesce_streams(outputs: List[NotebookNode]) -> List[NotebookNode]:
"""Merge all stream outputs with shared names into single streams.
This ensure deterministic outputs.
Adapted from:
https://github.com/computationalmodelling/nbval/blob/master/nbval/plugin.py.
"""
if not outputs:
return []

new_outputs = []
streams = {}
for output in outputs:
if output["output_type"] == "stream":
if output["name"] in streams:
streams[output["name"]]["text"] += output["text"]
else:
new_outputs.append(output)
streams[output["name"]] = output
else:
new_outputs.append(output)

# process \r and \b characters
for output in streams.values():
old = output["text"]
while len(output["text"]) < len(old):
old = output["text"]
# Cancel out anything-but-newline followed by backspace
output["text"] = RGX_BACKSPACE.sub("", output["text"])
# Replace all carriage returns not followed by newline
output["text"] = RGX_CARRIAGERETURN.sub("", output["text"])

# We also want to ensure stdout and stderr are always in the same consecutive order,
# because they are asynchronous, so order isn't guaranteed.
for i, output in enumerate(new_outputs):
if output["output_type"] == "stream" and output["name"] == "stderr":
if (
len(new_outputs) >= i + 2
and new_outputs[i + 1]["output_type"] == "stream"
and new_outputs[i + 1]["name"] == "stdout"
):
stdout = new_outputs.pop(i + 1)
new_outputs.insert(i, stdout)

return new_outputs


class CellOutputsToNodes(SphinxPostTransform):
"""Use the builder context to transform a CellOutputNode into Sphinx nodes."""

Expand All @@ -108,6 +161,8 @@ def run(self):
renderer_cls = load_renderer(node.renderer)
renderers[node.renderer] = renderer_cls
renderer = renderer_cls(self.document, node, abs_dir)
if self.config.nb_merge_streams:
node._outputs = coalesce_streams(node.outputs)
output_nodes = renderer.cell_output_to_nodes(self.env.nb_render_priority)
node.replace_self(output_nodes)

Expand Down
82 changes: 82 additions & 0 deletions tests/notebooks/merge_streams.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"source": [
"import sys\n",
"print('stdout1', file=sys.stdout)\n",
"print('stdout2', file=sys.stdout)\n",
"print('stderr1', file=sys.stderr)\n",
"print('stderr2', file=sys.stderr)\n",
"print('stdout3', file=sys.stdout)\n",
"print('stderr3', file=sys.stderr)\n",
"1"
],
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"stdout1\n",
"stdout2\n"
]
},
{
"output_type": "stream",
"name": "stderr",
"text": [
"stderr1\n",
"stderr2\n"
]
},
{
"output_type": "stream",
"name": "stdout",
"text": [
"stdout3\n"
]
},
{
"output_type": "stream",
"name": "stderr",
"text": [
"stderr3\n"
]
},
{
"output_type": "execute_result",
"data": {
"text/plain": [
"1"
]
},
"metadata": {},
"execution_count": 1
}
],
"metadata": {}
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
11 changes: 11 additions & 0 deletions tests/test_render_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ def test_stderr_remove(sphinx_run, file_regression):
file_regression.check(doctree.pformat(), extension=".xml", encoding="utf8")


@pytest.mark.sphinx_params(
"merge_streams.ipynb",
conf={"jupyter_execute_notebooks": "off", "nb_merge_streams": True},
)
def test_merge_streams(sphinx_run, file_regression):
sphinx_run.build()
assert sphinx_run.warnings() == ""
doctree = sphinx_run.get_resolved_doctree("merge_streams")
file_regression.check(doctree.pformat(), extension=".xml", encoding="utf8")


@pytest.mark.sphinx_params(
"metadata_image.ipynb",
conf={"jupyter_execute_notebooks": "off", "nb_render_key": "myst"},
Expand Down
23 changes: 23 additions & 0 deletions tests/test_render_outputs/test_merge_streams.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<document source="merge_streams">
<CellNode cell_type="code" classes="cell">
<CellInputNode classes="cell_input">
<literal_block language="ipython3" linenos="False" xml:space="preserve">
import sys
print('stdout1', file=sys.stdout)
print('stdout2', file=sys.stdout)
print('stderr1', file=sys.stderr)
print('stderr2', file=sys.stderr)
print('stdout3', file=sys.stdout)
print('stderr3', file=sys.stderr)
1
<CellOutputNode classes="cell_output">
<literal_block classes="output stream" language="myst-ansi" linenos="False" xml:space="preserve">
stdout1
stdout2
stdout3
<literal_block classes="output stderr" language="myst-ansi" linenos="False" xml:space="preserve">
stderr1
stderr2
stderr3
<literal_block classes="output text_plain" language="myst-ansi" linenos="False" xml:space="preserve">
1
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# then then deleting compiled files has been found to fix it: `find . -name \*.pyc -delete`

[tox]
envlist = py37-sphinx3
envlist = py37-sphinx4

[testenv:py{36,37,38,39}-sphinx{3,4}]
[testenv:py{37,38,39}-sphinx{3,4}]
extras = testing
deps =
sphinx3: sphinx>=3,<4
Expand Down

0 comments on commit a097602

Please sign in to comment.