Repository: sphinx-gallery/sphinx-gallery

Issue:
Relate warnings and errors on generated rst file back to source Python file / prevent accidental writing of generated files
When there is a problem in the generated RST file from a Gallery Python file, Sphinx reports the line error in the generated rst file. There's a few problems with this:

1. If you're not paying close attention, you might try editing the RST file to fix the error. But this is the wrong thing to do: you should have edited the source Python file. Some ways to make it harder to make this mistake include (1) removing the write bit from generated files, and (2) inserting a "@generated" comment which is respected by many editors and will warn people when editing
2. It would have been much better to report the error in the source Python file in the first place.

Compilers with the C preprocessor have solved this problem by generating comment lines that let you correspond lines in the generated source back to the original source, and then having the eventual frontend report errors by first translating them back to the original source. sphinx-gallery should do this too. It might be difficult to fix this without also getting Sphinx to play ball (e.g., a logical separation would be for Sphinx to add support for file/line number comments, and then teaching sphinx-gallery to respect them).

Is a feature like this something that would be accepted by sphinx-gallery?

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=h1) Report
> Merging [#750](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=desc) into [master](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/commit/ecd399e2e60557875d9312a6f5f8dbe4d0dd7a0e?el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750/graphs/tree.svg?width=650&height=150&src=pr&token=K4eytvhwU5)](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #750   +/-   ##
=======================================
  Coverage   97.51%   97.52%           
=======================================
  Files          32       32           
  Lines        3741     3751   +10     
=======================================
+ Hits         3648     3658   +10     
  Misses         93       93           
```


| [Impacted Files](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sphinx\_gallery/gen\_rst.py](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750/diff?src=pr&el=tree#diff-c3BoaW54X2dhbGxlcnkvZ2VuX3JzdC5weQ==) | `97.71% <100.00%> (+0.03%)` | :arrow_up: |
| [sphinx\_gallery/tests/test\_gen\_rst.py](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750/diff?src=pr&el=tree#diff-c3BoaW54X2dhbGxlcnkvdGVzdHMvdGVzdF9nZW5fcnN0LnB5) | `99.71% <100.00%> (+<0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=footer). Last update [ecd399e...f561687](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/750?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

lucyleeow: Thanks, feel free to merge!

Patch:
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index 2e95ca1..dd27222 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -10,8 +10,7 @@ example files.
 
 Files that generate images should start with 'plot'.
 """
-# Don't use unicode_literals here (be explicit with u"..." instead) otherwise
-# tricky errors come up with exec(code_blocks, ...) calls
+
 from __future__ import division, print_function, absolute_import
 from time import time
 import copy
@@ -25,6 +24,7 @@ import importlib
 from io import StringIO
 import os
 import re
+import stat
 from textwrap import indent
 import warnings
 from shutil import copyfile
@@ -138,8 +138,7 @@ SINGLE_IMAGE = """
     :class: sphx-glr-single-img
 """
 
-# This one could contain unicode
-CODE_OUTPUT = u""".. rst-class:: sphx-glr-script-out
+CODE_OUTPUT = """.. rst-class:: sphx-glr-script-out
 
  Out:
 
@@ -176,7 +175,7 @@ def codestr2rst(codestr, lang='python', lineno=None):
         lineno = '   :lineno-start: {0}\n'.format(lineno + blank_lines)
     else:
         lineno = ''
-    code_directive = "\n.. code-block:: {0}\n{1}\n".format(lang, lineno)
+    code_directive = ".. code-block:: {0}\n{1}\n".format(lang, lineno)
     indented_block = indent(codestr, ' ' * 4)
     return code_directive + indented_block
 
@@ -875,8 +874,34 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,
     return intro, title, (time_elapsed, memory_used)
 
 
+EXAMPLE_HEADER = """
+.. DO NOT EDIT.
+.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
+.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
+.. "{0}"
+.. LINE NUMBERS ARE GIVEN BELOW.
+
+.. only:: html
+
+    .. note::
+        :class: sphx-glr-download-link-note
+
+        Click :ref:`here <sphx_glr_download_{1}>`
+        to download the full example code{2}
+
+.. rst-class:: sphx-glr-example-title
+
+.. _sphx_glr_{1}:
+
+"""
+RST_BLOCK_HEADER = """\
+.. GENERATED FROM PYTHON SOURCE LINES {0}-{1}
+
+"""
+
+
 def rst_blocks(script_blocks, output_blocks, file_conf, gallery_conf):
-    """Generates the rst string containing the script prose, code and output
+    """Generate the rst string containing the script prose, code and output.
 
     Parameters
     ----------
@@ -902,9 +927,14 @@ def rst_blocks(script_blocks, output_blocks, file_conf, gallery_conf):
     # A simple example has two blocks: one for the
     # example introduction/explanation and one for the code
     is_example_notebook_like = len(script_blocks) > 2
-    example_rst = u""  # there can be unicode content
-    for (blabel, bcontent, lineno), code_output in \
-            zip(script_blocks, output_blocks):
+    example_rst = ""
+    for bi, ((blabel, bcontent, lineno), code_output) in \
+            enumerate(zip(script_blocks, output_blocks)):
+        # do not add comment to the title block, otherwise the linking does
+        # not work properly
+        if bi > 0:
+            example_rst += RST_BLOCK_HEADER.format(
+                lineno, lineno + bcontent.count('\n'))
         if blabel == 'code':
 
             if not file_conf.get('line_numbers',
@@ -946,21 +976,15 @@ def save_rst_example(example_rst, example_file, time_elapsed,
         Sphinx-Gallery configuration dictionary
     """
 
-    ref_fname = os.path.relpath(example_file, gallery_conf['src_dir'])
-    ref_fname = ref_fname.replace(os.path.sep, "_")
+    example_fname = os.path.relpath(example_file, gallery_conf['src_dir'])
+    ref_fname = example_fname.replace(os.path.sep, "_")
 
     binder_conf = check_binder_conf(gallery_conf.get('binder'))
 
     binder_text = (" or to run this example in your browser via Binder"
                    if len(binder_conf) else "")
-    example_rst = (".. only:: html\n\n"
-                   "    .. note::\n"
-                   "        :class: sphx-glr-download-link-note\n\n"
-                   "        Click :ref:`here <sphx_glr_download_{0}>` "
-                   "    to download the full example code{1}\n"
-                   "    .. rst-class:: sphx-glr-example-title\n\n"
-                   "    .. _sphx_glr_{0}:\n\n"
-                   ).format(ref_fname, binder_text) + example_rst
+    example_rst = EXAMPLE_HEADER.format(
+        example_fname, ref_fname, binder_text) + example_rst
 
     if time_elapsed >= gallery_conf["min_reported_time"]:
         time_m, time_s = divmod(time_elapsed, 60)
@@ -985,6 +1009,10 @@ def save_rst_example(example_rst, example_file, time_elapsed,
     write_file_new = re.sub(r'\.py$', '.rst.new', example_file)
     with codecs.open(write_file_new, 'w', encoding="utf-8") as f:
         f.write(example_rst)
+    # make it read-only so that people don't try to edit it
+    mode = os.stat(write_file_new).st_mode
+    ro_mask = 0x777 ^ (stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH)
+    os.chmod(write_file_new, mode & ro_mask)
     # in case it wasn't in our pattern, only replace the file if it's
     # still stale.
     _replace_md5(write_file_new, mode='t')


Test patch:
diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py
index 447b068..c037a8e 100644
--- a/sphinx_gallery/tests/test_gen_rst.py
+++ b/sphinx_gallery/tests/test_gen_rst.py
@@ -130,6 +130,7 @@ def test_rst_block_after_docstring(gallery_conf, tmpdir):
         f.write('\n'.join(['"Docstring"',
                            '####################',
                            '# Paragraph 1',
+                           '# is long.',
                            '',
                            '#%%',
                            '# Paragraph 2',
@@ -152,16 +153,24 @@ def test_rst_block_after_docstring(gallery_conf, tmpdir):
         blocks, script_vars, gallery_conf)
 
     example_rst = sg.rst_blocks(blocks, output_blocks, file_conf, gallery_conf)
-    assert example_rst == '\n'.join([
-        'Docstring',
-        '',
-        'Paragraph 1',
-        '',
-        'Paragraph 2',
-        '',
-        'Paragraph 3',
-        '',
-        ''])
+    want_rst = """\
+Docstring
+
+.. GENERATED FROM PYTHON SOURCE LINES 3-5
+
+Paragraph 1
+is long.
+
+.. GENERATED FROM PYTHON SOURCE LINES 7-8
+
+Paragraph 2
+
+.. GENERATED FROM PYTHON SOURCE LINES 10-11
+
+Paragraph 3
+
+"""
+    assert example_rst == want_rst
 
 
 def test_rst_empty_code_block(gallery_conf, tmpdir):
@@ -191,15 +200,20 @@ def test_rst_empty_code_block(gallery_conf, tmpdir):
         blocks, script_vars, gallery_conf)
 
     example_rst = sg.rst_blocks(blocks, output_blocks, file_conf, gallery_conf)
-    assert example_rst.rstrip('\n') == """Docstring
+    want_rst = """\
+Docstring
+
+.. GENERATED FROM PYTHON SOURCE LINES 3-4
 
 Paragraph 1
 
+.. GENERATED FROM PYTHON SOURCE LINES 4-5
 
 .. code-block:: python
 
 
     # just a comment"""
+    assert example_rst.rstrip('\n') == want_rst
 
 
 def test_script_vars_globals(gallery_conf, tmpdir):
@@ -235,7 +249,7 @@ b = 'foo'
 def test_codestr2rst():
     """Test the correct translation of a code block into rst."""
     output = sg.codestr2rst('print("hello world")')
-    reference = """
+    reference = """\
 .. code-block:: python
 
     print("hello world")"""