11"""A Sphinx post-transform, to convert notebook outpus to AST nodes."""
22import os
3+ import re
34from abc import ABC , abstractmethod
45from typing import List , Optional
56from unittest import mock
@@ -91,6 +92,58 @@ def load_renderer(name: str) -> "CellOutputRendererBase":
9192 raise MystNbEntryPointError (f"No Entry Point found for myst_nb.mime_render:{ name } " )
9293
9394
95+ RGX_CARRIAGERETURN = re .compile (r".*\r(?=[^\n])" )
96+ RGX_BACKSPACE = re .compile (r"[^\n]\b" )
97+
98+
99+ def coalesce_streams (outputs : List [NotebookNode ]) -> List [NotebookNode ]:
100+ """Merge all stream outputs with shared names into single streams.
101+
102+ This ensure deterministic outputs.
103+
104+ Adapted from:
105+ https://github.com/computationalmodelling/nbval/blob/master/nbval/plugin.py.
106+ """
107+ if not outputs :
108+ return []
109+
110+ new_outputs = []
111+ streams = {}
112+ for output in outputs :
113+ if output ["output_type" ] == "stream" :
114+ if output ["name" ] in streams :
115+ streams [output ["name" ]]["text" ] += output ["text" ]
116+ else :
117+ new_outputs .append (output )
118+ streams [output ["name" ]] = output
119+ else :
120+ new_outputs .append (output )
121+
122+ # process \r and \b characters
123+ for output in streams .values ():
124+ old = output ["text" ]
125+ while len (output ["text" ]) < len (old ):
126+ old = output ["text" ]
127+ # Cancel out anything-but-newline followed by backspace
128+ output ["text" ] = RGX_BACKSPACE .sub ("" , output ["text" ])
129+ # Replace all carriage returns not followed by newline
130+ output ["text" ] = RGX_CARRIAGERETURN .sub ("" , output ["text" ])
131+
132+ # We also want to ensure stdout and stderr are always in the same consecutive order,
133+ # because they are asynchronous, so order isn't guaranteed.
134+ for i , output in enumerate (new_outputs ):
135+ if output ["output_type" ] == "stream" and output ["name" ] == "stderr" :
136+ if (
137+ len (new_outputs ) >= i + 2
138+ and new_outputs [i + 1 ]["output_type" ] == "stream"
139+ and new_outputs [i + 1 ]["name" ] == "stdout"
140+ ):
141+ stdout = new_outputs .pop (i + 1 )
142+ new_outputs .insert (i , stdout )
143+
144+ return new_outputs
145+
146+
94147class CellOutputsToNodes (SphinxPostTransform ):
95148 """Use the builder context to transform a CellOutputNode into Sphinx nodes."""
96149
@@ -108,6 +161,8 @@ def run(self):
108161 renderer_cls = load_renderer (node .renderer )
109162 renderers [node .renderer ] = renderer_cls
110163 renderer = renderer_cls (self .document , node , abs_dir )
164+ if self .config .nb_merge_streams :
165+ node ._outputs = coalesce_streams (node .outputs )
111166 output_nodes = renderer .cell_output_to_nodes (self .env .nb_render_priority )
112167 node .replace_self (output_nodes )
113168
0 commit comments