Skip to content

Commit f81e21f

Browse files
author
Release Manager
committed
gh-37083: HTML documentation: Show preparsed doctests using inline tabs <!-- ^^^^^ Please provide a concise, informative and self-explanatory title. Don't put issue numbers in there, do this in the PR body below. For example, instead of "Fixes #1234" use "Introduce new method to calculate 1+1" --> <!-- Describe your changes here in detail --> Every doctest block is decorated with tabs, by default showing the original doctest in **Sage** syntax, and offering a preparsed, pure **Python** version of it. The tabs are synchronized across the page. If @kwankyu's live documentation is being built, it is offered as another tab **Sage (live)**. When this tab is selected, it automatically starts the Thebe/Binder; it is not necessary to find and push the "Make live" button. ![tabs-anim](https://github.com/sagemath/sage/assets/8345221/4252ae92- eeb7-417a-ba24-c141726f714d) [Preview](https://deploy-preview-37083--sagemath.netlify.app/html/en/ref erence/function_fields/sage/rings/function_field/function_field_rational ) <!-- Why is this change required? What problem does it solve? --> <!-- If this PR resolves an open issue, please link to it here. For example "Fixes #12345". --> Fixes #35791 <!-- If your change requires a documentation PR, please link it appropriately. --> ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> <!-- If your change requires a documentation PR, please link it appropriately --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!-- Feel free to remove irrelevant items. --> - [x] The title is concise, informative, and self-explanatory. - [x] The description explains in detail what this PR is about. - [ ] I have linked a relevant issue or discussion. - [ ] I have created tests covering the changes. - [ ] I have updated the documentation accordingly. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on - #12345: short description why this is a dependency - #34567: ... --> - Depends on #37056 - Depends on #37065 <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> URL: #37083 Reported by: Matthias Köppe Reviewer(s): Kwankyu Lee, Matthias Köppe, Tobias Diez
2 parents 767c7e8 + dea37e7 commit f81e21f

File tree

9 files changed

+128
-42
lines changed

9 files changed

+128
-42
lines changed

Diff for: .ci/merge-fixes.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ for REPO in ${SAGE_CI_FIXES_FROM_REPOSITORIES:-sagemath/sage}; do
4242
# Considered alternative: Use https://github.com/$REPO/pull/$a.diff,
4343
# which squashes everything into one diff without commit metadata.
4444
PULL_URL="https://github.com/$REPO/pull/$a"
45+
PULL_SHORT="$REPO#$a"
4546
PULL_FILE="$REPO_FILE-$a"
4647
PATH=build/bin:$PATH build/bin/sage-download-file --quiet "$PULL_URL.patch" $PULL_FILE.patch
4748
date -u +"%Y-%m-%dT%H:%M:%SZ" > $PULL_FILE.date # Record the date, for future reference
@@ -67,7 +68,7 @@ for REPO in ${SAGE_CI_FIXES_FROM_REPOSITORIES:-sagemath/sage}; do
6768
git am --signoff --show-current-patch=diff
6869
echo "--------------------------------------------------------------------8<-----------------------------"
6970
echo "::endgroup::"
70-
echo "Failure applying $PULL_URL as a patch, resetting"
71+
echo "Failure applying $PULL_SHORT as a patch, resetting"
7172
git am --signoff --abort
7273
fi
7374
done

Diff for: .github/workflows/build.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ jobs:
3535
uses: actions/checkout@v4
3636
- name: Merge CI fixes from sagemath/sage
3737
run: |
38-
.ci/merge-fixes.sh
38+
mkdir -p upstream
39+
.ci/merge-fixes.sh 2>&1 | tee upstream/ci_fixes.log
3940
env:
4041
GH_TOKEN: ${{ github.token }}
4142
SAGE_CI_FIXES_FROM_REPOSITORIES: ${{ vars.SAGE_CI_FIXES_FROM_REPOSITORIES }}
4243
- name: Store CI fixes in upstream artifact
4344
run: |
44-
mkdir -p upstream
4545
if git format-patch --stdout test_base > ci_fixes.patch; then
4646
cp ci_fixes.patch upstream/
4747
fi

Diff for: .github/workflows/doc-build-pdf.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ jobs:
2929
uses: actions/checkout@v4
3030
- name: Merge CI fixes from sagemath/sage
3131
run: |
32-
.ci/merge-fixes.sh
32+
mkdir -p upstream
33+
.ci/merge-fixes.sh 2>&1 | tee upstream/ci_fixes.log
3334
env:
3435
GH_TOKEN: ${{ github.token }}
3536
SAGE_CI_FIXES_FROM_REPOSITORIES: ${{ vars.SAGE_CI_FIXES_FROM_REPOSITORIES }}
3637
- name: Store CI fixes in upstream artifact
3738
run: |
38-
mkdir -p upstream
3939
if git format-patch --stdout test_base > ci_fixes.patch; then
4040
cp ci_fixes.patch upstream/
4141
fi

Diff for: .github/workflows/doc-build.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ jobs:
2424
uses: actions/checkout@v4
2525
- name: Merge CI fixes from sagemath/sage
2626
run: |
27-
.ci/merge-fixes.sh
27+
mkdir -p upstream
28+
.ci/merge-fixes.sh 2>&1 | tee upstream/ci_fixes.log
2829
env:
2930
GH_TOKEN: ${{ github.token }}
3031
SAGE_CI_FIXES_FROM_REPOSITORIES: ${{ vars.SAGE_CI_FIXES_FROM_REPOSITORIES }}
3132
- name: Store CI fixes in upstream artifact
3233
run: |
33-
mkdir -p upstream
3434
if git format-patch --stdout test_base > ci_fixes.patch; then
3535
cp ci_fixes.patch upstream/
3636
fi

Diff for: src/doc/common/static/custom-jupyter-sphinx.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
div.jupyter_container {
2-
margin: .5rem 0;
2+
border: 0;
33
}
44

55
div.jupyter_container + div.jupyter_container {

Diff for: src/doc/common/static/jupyter-sphinx-furo.js

+13
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,16 @@ thebelab.on("status", function (evt, data) {
112112
kernel.requestExecute({code: "%display latex"});
113113
}
114114
});
115+
116+
// Activate Thebe when "Sage (live)" tab is clicked
117+
document.querySelectorAll('input[class="tab-input"]').forEach((elem) => {
118+
elem.addEventListener("click", function(event) {
119+
if (elem.nextElementSibling) {
120+
if (elem.nextElementSibling.nextElementSibling) {
121+
if (elem.nextElementSibling.nextElementSibling.querySelector('div[class="thebelab-code"]')) {
122+
initThebelab();
123+
}
124+
}
125+
}
126+
});
127+
});

Diff for: src/doc/en/installation/source.rst

+13-5
Original file line numberDiff line numberDiff line change
@@ -940,11 +940,19 @@ Environment variables controlling the documentation build
940940

941941
The value of this variable is passed as an
942942
argument to ``sage --docbuild all html`` or ``sage --docbuild all pdf`` when
943-
you run ``make``, ``make doc``, or ``make doc-pdf``. For example, you can
944-
add ``--no-plot`` to this variable to avoid building the graphics coming from
945-
the ``.. PLOT`` directive within the documentation, or you can add
946-
``--include-tests-blocks`` to include all "TESTS" blocks in the reference
947-
manual. Run ``sage --docbuild help`` to see the full list of options.
943+
you run ``make``, ``make doc``, or ``make doc-pdf``. For example:
944+
945+
- add ``--no-plot`` to this variable to avoid building the graphics coming from
946+
the ``.. PLOT`` directive within the documentation,
947+
948+
- add ``--no-preparsed-examples`` to only show the original Sage code of
949+
"EXAMPLES" blocks, suppressing the tab with the preparsed, plain Python
950+
version, or
951+
952+
- add ``--include-tests-blocks`` to include all "TESTS" blocks in the reference
953+
manual.
954+
955+
Run ``sage --docbuild help`` to see the full list of options.
948956

949957
.. envvar:: SAGE_SPKG_INSTALL_DOCS
950958

Diff for: src/sage_docbuild/__main__.py

+5
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ def setup_parser():
289289
standard.add_argument("--no-plot", dest="no_plot",
290290
action="store_true",
291291
help="do not include graphics auto-generated using the '.. plot' markup")
292+
standard.add_argument("--no-preparsed-examples", dest="no_preparsed_examples",
293+
action="store_true",
294+
help="do not show preparsed versions of EXAMPLES blocks")
292295
standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True,
293296
action="store_false",
294297
help="include TESTS blocks in the reference manual")
@@ -478,6 +481,8 @@ def excepthook(*exc_info):
478481
build_options.ALLSPHINXOPTS += "-n "
479482
if args.no_plot:
480483
os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes'
484+
if args.no_preparsed_examples:
485+
os.environ['SAGE_PREPARSED_DOC'] = 'no'
481486
if args.live_doc:
482487
os.environ['SAGE_LIVE_DOC'] = 'yes'
483488
if args.skip_tests:

Diff for: src/sage_docbuild/conf.py

+88-29
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
# General configuration
4040
# ---------------------
4141

42+
SAGE_LIVE_DOC = os.environ.get('SAGE_LIVE_DOC', 'no')
43+
SAGE_PREPARSED_DOC = os.environ.get('SAGE_PREPARSED_DOC', 'yes')
44+
4245
# Add any Sphinx extension module names here, as strings. They can be extensions
4346
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
4447
extensions = [
@@ -57,7 +60,7 @@
5760

5861
jupyter_execute_default_kernel = 'sagemath'
5962

60-
if os.environ.get('SAGE_LIVE_DOC', 'no') == 'yes':
63+
if SAGE_LIVE_DOC == 'yes':
6164
SAGE_JUPYTER_SERVER = os.environ.get('SAGE_JUPYTER_SERVER', 'binder')
6265
if SAGE_JUPYTER_SERVER.startswith('binder'):
6366
# format: "binder" or
@@ -230,7 +233,7 @@ def sphinx_plot(graphics, **kwds):
230233
# console lexers. 'ipycon' is the IPython console, which is what we want
231234
# for most code blocks: anything with "sage:" prompts. For other IPython,
232235
# like blocks which might appear in a notebook cell, use 'ipython'.
233-
highlighting.lexers['ipycon'] = IPythonConsoleLexer(in1_regex=r'sage: ', in2_regex=r'[.][.][.][.]: ')
236+
highlighting.lexers['ipycon'] = IPythonConsoleLexer(in1_regex=r'(sage:|>>>)', in2_regex=r'([.][.][.][.]:|[.][.][.])')
234237
highlighting.lexers['ipython'] = IPyLexer()
235238
highlight_language = 'ipycon'
236239

@@ -305,7 +308,7 @@ def set_intersphinx_mappings(app, config):
305308
multidocs_is_master = True
306309

307310
# https://sphinx-copybutton.readthedocs.io/en/latest/use.html
308-
copybutton_prompt_text = r"sage: |[.][.][.][.]: |\$ "
311+
copybutton_prompt_text = r"sage: |[.][.][.][.]: |>>> |[.][.][.] |\$ "
309312
copybutton_prompt_is_regexp = True
310313
copybutton_exclude = '.linenos, .c1' # exclude single comments (in particular, # optional!)
311314
copybutton_only_copy_prompt_lines = True
@@ -789,8 +792,6 @@ class will be properly documented inside its surrounding class.
789792
return skip
790793

791794

792-
from jupyter_sphinx.ast import JupyterCellNode, CellInputNode
793-
794795
class SagecodeTransform(SphinxTransform):
795796
"""
796797
Transform a code block to a live code block enabled by jupyter-sphinx.
@@ -828,29 +829,87 @@ def apply(self):
828829
if self.app.builder.tags.has('html') or self.app.builder.tags.has('inventory'):
829830
for node in self.document.traverse(nodes.literal_block):
830831
if node.get('language') is None and node.astext().startswith('sage:'):
831-
source = node.rawsource
832-
lines = []
833-
for line in source.splitlines():
834-
newline = line.lstrip()
835-
if newline.startswith('sage: ') or newline.startswith('....: '):
836-
lines.append(newline[6:])
837-
cell_node = JupyterCellNode(
838-
execute=False,
839-
hide_code=True,
840-
hide_output=True,
841-
emphasize_lines=[],
842-
raises=False,
843-
stderr=True,
844-
code_below=False,
845-
classes=["jupyter_cell"])
846-
cell_input = CellInputNode(classes=['cell_input','live-doc'])
847-
cell_input += nodes.literal_block(
848-
text='\n'.join(lines),
849-
linenos=False,
850-
linenostart=1)
851-
cell_node += cell_input
852-
853-
node.parent.insert(node.parent.index(node) + 1, cell_node)
832+
from docutils.nodes import container as Container, label as Label, literal_block as LiteralBlock, Text
833+
from sphinx_inline_tabs._impl import TabContainer
834+
parent = node.parent
835+
index = parent.index(node)
836+
if isinstance(node.previous_sibling(), TabContainer):
837+
# Make sure not to merge inline tabs for adjacent literal blocks
838+
parent.insert(index, Text(''))
839+
index += 1
840+
parent.remove(node)
841+
# Tab for Sage code
842+
container = TabContainer("", type="tab", new_set=False)
843+
textnodes = [Text('Sage')]
844+
label = Label("", "", *textnodes)
845+
container += label
846+
content = Container("", is_div=True, classes=["tab-content"])
847+
content += node
848+
container += content
849+
parent.insert(index, container)
850+
if SAGE_PREPARSED_DOC == 'yes':
851+
# Tab for preparsed version
852+
from sage.repl.preparse import preparse
853+
container = TabContainer("", type="tab", new_set=False)
854+
textnodes = [Text('Python')]
855+
label = Label("", "", *textnodes)
856+
container += label
857+
content = Container("", is_div=True, classes=["tab-content"])
858+
example_lines = []
859+
preparsed_lines = ['>>> from sage.all import *']
860+
for line in node.rawsource.splitlines() + ['']: # one extra to process last example
861+
newline = line.lstrip()
862+
if newline.startswith('....: '):
863+
example_lines.append(newline[6:])
864+
else:
865+
if example_lines:
866+
preparsed_example = preparse('\n'.join(example_lines))
867+
prompt = '>>> '
868+
for preparsed_line in preparsed_example.splitlines():
869+
preparsed_lines.append(prompt + preparsed_line)
870+
prompt = '... '
871+
example_lines = []
872+
if newline.startswith('sage: '):
873+
example_lines.append(newline[6:])
874+
else:
875+
preparsed_lines.append(line)
876+
preparsed = '\n'.join(preparsed_lines)
877+
preparsed_node = LiteralBlock(preparsed, preparsed, language='ipycon')
878+
content += preparsed_node
879+
container += content
880+
parent.insert(index + 1, container)
881+
if SAGE_LIVE_DOC == 'yes':
882+
# Tab for Jupyter-sphinx cell
883+
from jupyter_sphinx.ast import JupyterCellNode, CellInputNode
884+
source = node.rawsource
885+
lines = []
886+
for line in source.splitlines():
887+
newline = line.lstrip()
888+
if newline.startswith('sage: ') or newline.startswith('....: '):
889+
lines.append(newline[6:])
890+
cell_node = JupyterCellNode(
891+
execute=False,
892+
hide_code=False,
893+
hide_output=True,
894+
emphasize_lines=[],
895+
raises=False,
896+
stderr=True,
897+
code_below=False,
898+
classes=["jupyter_cell"])
899+
cell_input = CellInputNode(classes=['cell_input','live-doc'])
900+
cell_input += nodes.literal_block(
901+
text='\n'.join(lines),
902+
linenos=False,
903+
linenostart=1)
904+
cell_node += cell_input
905+
container = TabContainer("", type="tab", new_set=False)
906+
textnodes = [Text('Sage Live')]
907+
label = Label("", "", *textnodes)
908+
container += label
909+
content = Container("", is_div=True, classes=["tab-content"])
910+
content += cell_node
911+
container += content
912+
parent.insert(index + 1, container)
854913

855914

856915
# This replaces the setup() in sage.misc.sagedoc_conf
@@ -864,7 +923,7 @@ def setup(app):
864923
app.connect('autodoc-process-docstring', skip_TESTS_block)
865924
app.connect('autodoc-skip-member', skip_member)
866925
app.add_transform(SagemathTransform)
867-
if os.environ.get('SAGE_LIVE_DOC', 'no') == 'yes':
926+
if SAGE_LIVE_DOC == 'yes' or SAGE_PREPARSED_DOC == 'yes':
868927
app.add_transform(SagecodeTransform)
869928

870929
# When building the standard docs, app.srcdir is set to SAGE_DOC_SRC +

0 commit comments

Comments
 (0)