Repository: sphinx-gallery/sphinx-gallery

Issue:
[QUESTION] Is it possible to ignore the output of a specific cell?
## Question

I tried looking in the docs for this but I could not find any options on how to hide the output of a specific notebook cells. Did I miss something?

## Motivation

The reason I'm asking is because I'm [currently working on](https://github.com/tpvasconcelos/ridgeplot/pull/267/files#diff-ca4940391ff4423ef59f4c827f3ccc447a492b08698c9860d3efb4c134ca4f17) an examples gallery with Plotly and I sometimes want to have cells with something like:

```python
fig.update_layout(
    foo="Bar",
    bar="Foo",
)
```

but since `update_layout` return a plotly figure, this is automatically captured by Sphinx-Gallery and include in the output.

A workaround would be to assign it back to a variable:

```python
fig = fig.update_layout(
    foo="Bar",
    bar="Foo",
)
```

but I want to be consistent and follow the same standards used throughout the Plotly official docs (they always threat the `Figure` object as mutable and never reassign back to a `fig` variable). See [this](https://plotly.com/python/creating-and-updating-figures/) for instance.



Discussion:
tpvasconcelos: Hey again @larsoner! It was pretty straightforward to implement what we discussed in #1397 👍 

I opened this PR for you to take a look at and get some feedback first. Happy to work on documenting this in https://sphinx-gallery.github.io/stable/configuration.html once you're happy with the implementation
tpvasconcelos: @larsoner I did't find a clean nice way of introducing the `.. warning::` or `.. admonition::` admonitions. To my eyes the docs look clear to me as they are now. If you think something could be improved maybe it would be simpler for you to push some changes to my branch (I believe you already have access to it).

Let me know what you think
tpvasconcelos: I ended up adding a note admonition but to connect the suggestions for matplotlib figures to Plotly figures:

![image](https://github.com/user-attachments/assets/ec023f3f-0310-41e3-b65d-d6c3e65a7a76)

tpvasconcelos: Thanks @larsoner and @lucyleeow for the prompt feedback and fast turnaround on this issue! 🚀🚀
larsoner: Pushed nitpick fix and marking for merge-when-green, thanks in advance @tpvasconcelos !

Patch:
diff --git a/doc/configuration.rst b/doc/configuration.rst
index 01989e5..7ee3769 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -47,6 +47,13 @@ file, inside a ``sphinx_gallery_conf`` dictionary.
 - ``reset_modules`` and ``reset_modules_order`` (:ref:`reset_modules`)
 - ``parallel`` (:ref:`parallel`)
 
+.. admonition:: Diverging from Jupyter
+    :class: danger
+
+    Sphinx-gallery attempts to render examples to HTML in a manner largely consistent with what a user will experience when they download the corresponding ``.ipynb`` notebook file and run it locally.
+    Some options, such as ``'capture_repr': (),``, will make these behaviors less consistent.
+    Consider using these options sparingly as it could lead to confusion or sub-optimal experiences for users!
+
 **Cross-referencing**
 
 - ``reference_url``, ``prefer_full_module`` (:ref:`link_to_documentation`)
@@ -107,6 +114,7 @@ Some options can also be set or overridden on a file-by-file basis:
 
 Some options can be set on a per-code-block basis in a file:
 
+- ``# sphinx_gallery_capture_repr_block`` (:ref:`capture_repr`)
 - ``# sphinx_gallery_defer_figures`` (:ref:`defer_figures`)
 - ``# sphinx_gallery_multi_image_block`` (:ref:`multi_image`)
 
@@ -2411,11 +2419,15 @@ are:
   present in some objects, for example, pandas dataframes.
 
 Output capture can be controlled globally by the ``capture_repr`` configuration
-setting or file-by-file by adding a comment to the example file, which overrides
+setting, file-by-file by adding a comment to the example file, which overrides
 any global setting::
 
     # sphinx_gallery_capture_repr = ()
 
+, or block-by-block by adding a comment to the code block, which overrides any
+global or file setting::
+
+    # sphinx_gallery_capture_repr_block = ()
 
 The default setting is::
 
@@ -2445,8 +2457,7 @@ From another perspective, take for example the following code block::
 ``'Hello world'`` would be captured for every ``capture_repr`` setting as this
 is directed to standard output. Further,
 
-* if ``capture_repr`` is an empty tuple, nothing else would be
-  captured.
+* if ``capture_repr`` is an empty tuple, nothing else would be captured.
 * if ``capture_repr`` is ``('__repr__')``, ``2`` would also be captured.
 * if ``capture_repr`` is ``('_repr_html_', '__repr__')`` (the default)
   Sphinx-Gallery would first attempt to capture ``_repr_html_``. Since this
@@ -2477,8 +2488,27 @@ method which would thus be captured. You can prevent this by:
     plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
     plt.show()
 
-The unwanted string output will not occur if ``'capture_repr'`` is an empty
-tuple or does not contain ``__repr__`` or ``__str__``.
+* add a ``# sphinx_gallery_capture_repr_block = ()`` comment to the code block.
+  The unwanted string output will not occur if ``'capture_repr'`` is an empty
+  tuple or, at least, does not contain ``__repr__`` or ``__str__``.
+  For example::
+
+    # sphinx_gallery_capture_repr_block = ()
+    import matplotlib.pyplot as plt
+
+    plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
+
+.. admonition:: Note for Plotly users
+   :class: note
+
+   The suggestions above also apply to Plotly users. Plotly figures have
+   several `update methods
+   <https://plotly.com/python/creating-and-updating-figures/>`_ that
+   implicitly return the updated figure object. You can set a block-level
+   ``# sphinx_gallery_capture_repr_block = ()`` comment to prevent these
+   from being captured, or assign the return values to a variable (*e.g.,*
+   ``fig = fig.update_layout(...)``).
+
 
 .. _regular expressions: https://docs.python.org/3/library/re.html
 
diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py
index 9568447..a493532 100644
--- a/sphinx_gallery/gen_gallery.py
+++ b/sphinx_gallery/gen_gallery.py
@@ -1138,7 +1138,7 @@ def _make_graph(fname, entries, gallery_conf):
                 dg.node(node_to, **node_kwargs)
                 dg.edge(node_from, node_to, color=API_COLORS["edge"])
         # add modules with all API entries
-        for module in gallery_conf["_sg_api_entries"]["module"]:
+        for module in gallery_conf["_sg_api_entries"].get("module", []):
             struct = module.split(".")
             for i in range(len(struct) - 1):
                 if struct[i + 1] not in lut:
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index 2faa858..bb54d5e 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -888,7 +888,7 @@ def _get_last_repr(capture_repr, ___):
 
 
 def _get_code_output(
-    is_last_expr, example_globals, gallery_conf, logging_tee, images_rst, file_conf
+    is_last_expr, example_globals, gallery_conf, logging_tee, images_rst, capture_repr
 ):
     """Obtain standard output and html output in reST.
 
@@ -921,7 +921,6 @@ def _get_code_output(
         ignore_repr = False
         if gallery_conf["ignore_repr_types"]:
             ignore_repr = re.search(gallery_conf["ignore_repr_types"], str(type(___)))
-        capture_repr = file_conf.get("capture_repr", gallery_conf["capture_repr"])
         if capture_repr != () and not ignore_repr:
             last_repr, repr_meth = _get_last_repr(capture_repr, ___)
 
@@ -1007,11 +1006,20 @@ def execute_code_block(
     )
     need_save_figures = defer_figs_match is None
 
+    block_conf = py_source_parser.extract_file_config(block.content)
+
     # Add `sphinx_gallery_multi_image_block` setting to block variables
     # (extract config rather than just regex search since the option's value is needed)
-    script_vars["multi_image"] = py_source_parser.extract_file_config(
-        block.content
-    ).get("multi_image_block")
+    script_vars["multi_image"] = block_conf.get("multi_image_block")
+
+    # Determine the block's `capture_repr` value by prioritising the block-level
+    # `sphinx_gallery_capture_repr_block` setting, then the file-level
+    # `sphinx_gallery_capture_repr` setting, and finally the
+    # global `capture_repr` gallery setting.
+    capture_repr = block_conf.get(
+        "capture_repr_block",
+        file_conf.get("capture_repr", gallery_conf["capture_repr"]),
+    )
 
     # Add file_conf to script_vars to be read by image scrapers
     script_vars["file_conf"] = file_conf
@@ -1059,7 +1067,7 @@ def execute_code_block(
             gallery_conf,
             logging_tee,
             images_rst,
-            file_conf,
+            capture_repr,
         )
     finally:
         _reset_cwd_syspath(cwd, sys_path)


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d6f888e..3dfd0fd 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,7 +15,6 @@ jobs:
   pytest:
     name: '${{ matrix.os }} / ${{ matrix.python }} / ${{ matrix.distrib }} / ${{ matrix.sphinx_version }}'
     timeout-minutes: 30
-    continue-on-error: true
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -25,6 +24,7 @@ jobs:
       SPHINX_VERSION: '${{ matrix.sphinx_version }}'
       DISTRIB: '${{ matrix.distrib }}'
     strategy:
+      fail-fast: false
       matrix:
         include:
           - os: ubuntu-latest  # newest possible
diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py
index 260dc2a..b8d26ac 100644
--- a/sphinx_gallery/tests/test_gen_rst.py
+++ b/sphinx_gallery/tests/test_gen_rst.py
@@ -1043,24 +1043,33 @@ def test_capture_repr(
 
 
 @pytest.mark.parametrize(
-    "caprepr_gallery, caprepr_file, expected_out",
+    "caprepr_gallery, caprepr_file, caprepr_block, expected_out",
     [
-        pytest.param(tuple(), ("__repr__",), "2", id="() --> repr"),
-        pytest.param(("__repr__",), "()", "", id="repr --> ()"),
+        pytest.param(tuple(), "()", '("__repr__",)', "2", id="() --> () --> repr"),
+        pytest.param(tuple(), ("__repr__",), None, "2", id="() --> repr --> None"),
+        pytest.param(("__repr__",), "()", None, "", id="repr --> () --> None"),
+        pytest.param(tuple(), ("__repr__",), "()", "", id="() --> repr --> ()"),
+        pytest.param(("__repr__",), "()", "()", "", id="repr --> () --> ()"),
     ],
 )
-def test_per_file_capture_repr(
+def test_capture_repr_per_file_and_per_block(
     gallery_conf,
     caprepr_gallery,
     caprepr_file,
+    caprepr_block,
     expected_out,
     req_mpl,
     req_pil,
     script_vars,
 ):
-    """Tests that per file capture_repr overrides gallery_conf."""
+    """Tests that per file/block capture_repr overrides gallery_conf."""
     compiler = codeop.Compile()
-    code_block = Block("code", "a=2\n2", 1)
+    caprepr_block = (
+        f"# sphinx_gallery_capture_repr_block={caprepr_block}\n"
+        if caprepr_block
+        else ""
+    )
+    code_block = Block("code", f"{caprepr_block}a=2\n2", 1)
     gallery_conf["capture_repr"] = caprepr_gallery
     file_conf = {"capture_repr": caprepr_file}
     output = sg.execute_code_block(
@@ -1070,7 +1079,7 @@ def test_per_file_capture_repr(
 
 
 def test_ignore_repr_types(gallery_conf, req_mpl, req_pil, script_vars):
-    """Tests output capturing with various capture_repr settings."""
+    """Tests that `ignore_repr_types` correctly ignores the output of a specific type."""
     compiler = codeop.Compile()
     code_block = Block("code", "a=2\na", 1)
     gallery_conf["ignore_repr_types"] = r"int"