Repository: sphinx-gallery/sphinx-gallery

Issue:
Confusing line numbers in traceback
When building e.g. the s-g docs, one gets the traceback
```
/home/antony/src/extern/sphinx-gallery/examples/plot_raise.py failed leaving traceback:
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/sphinx_gallery/gen_rst.py", line 472, in execute_code_block
    exec(code_block, example_globals)
  File "<string>", line 2, in <module>
NameError: name 'iae' is not defined
```
but the error actually occurs at line 23 of the .py file (I realize this is probably the line number *within the cell*, but this can make troubleshooting a bit trickier).

Discussion:
QuLogic: Rebased to fix conflicts.
QuLogic: Rebased due to conflicts.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 18a18f6..9d34b65 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,9 @@ Change Log
 git master
 ----------
 
+v0.1.12
+-------
+
 New features
 ''''''''''''
 
@@ -12,6 +15,10 @@ New features
   `#37 <https://github.com/sphinx-gallery/sphinx-gallery/issues/37>`_,
   `#233 <https://github.com/sphinx-gallery/sphinx-gallery/pull/233>`_ and
   `#234 <https://github.com/sphinx-gallery/sphinx-gallery/pull/234>`_
+* Cleanup console output during build
+  `#250 <https://github.com/sphinx-gallery/sphinx-gallery/pull/250>`_
+* New  configuration Test
+  `#225 <https://github.com/sphinx-gallery/sphinx-gallery/pull/225>`_
 
 Bug Fixes
 '''''''''
@@ -19,6 +26,20 @@ Bug Fixes
 * Reset ``sys.argv`` before running each example. See
   `#252 <https://github.com/sphinx-gallery/sphinx-gallery/pull/252>`_
   for more details.
+* Correctly re-raise errors in doc resolver. See
+  `#264 <https://github.com/sphinx-gallery/sphinx-gallery/pull/264>`_.
+* Allow and use https links where possible
+  `#258 <https://github.com/sphinx-gallery/sphinx-gallery/pull/258>`_.
+* Escape tooltips for any HTML special characters.
+  `#249 <https://github.com/sphinx-gallery/sphinx-gallery/pull/249>`_
+
+Documentation
+'''''''''''''''
+
+* Update link to numpy to point to latest
+  `#271 <https://github.com/sphinx-gallery/sphinx-gallery/pull/271>`_
+* Added documentation dependencies.
+  `#267 <https://github.com/sphinx-gallery/sphinx-gallery/pull/267>`_
 
 v0.1.11
 -------
diff --git a/circle.yml b/circle.yml
index 54d32b7..bd469ae 100644
--- a/circle.yml
+++ b/circle.yml
@@ -31,3 +31,10 @@ general:
     - "doc/_build/html"
     - "rtd_html"
     - "alabaster_html"
+
+deployment:
+  staging:
+    branch:
+      - master
+    commands:
+        - ./docs_deploy.sh rtd_html
diff --git a/doc/advanced_configuration.rst b/doc/advanced_configuration.rst
index 184a8aa..4a71add 100644
--- a/doc/advanced_configuration.rst
+++ b/doc/advanced_configuration.rst
@@ -318,6 +318,29 @@ configuration file to have it as a default. The highest precedence is
 always given to the `-D` flag of the ``sphinx-build`` command.
 
 
+Adding line numbers to example listings
+=======================================
+
+Line numbers can be displayed in listings by adding the global ``line_numbers``
+setting:
+
+.. code-block:: python
+
+    sphinx_gallery_conf = {
+        'line_numbers': True
+    }
+
+or by adding a comment to the example script, which overrides any global
+setting:
+
+.. code-block:: python
+
+    # sphinx_gallery_line_numbers = True
+
+Note that for Sphinx < 1.3, the line numbers will not be consistent with the
+original file.
+
+
 Dealing with failing Gallery example scripts
 ============================================
 
diff --git a/docs_deploy.sh b/docs_deploy.sh
new file mode 100755
index 0000000..cfee758
--- /dev/null
+++ b/docs_deploy.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+# ideas used from https://gist.github.com/motemen/8595451
+
+# abort the script if there is a non-zero error
+set -e
+
+# show where we are on the machine
+pwd
+
+siteSource="$1"
+
+if [ ! -d "$siteSource" ]
+then
+    echo "Usage: $0 <site source dir>"
+    exit 1
+fi
+
+# now lets setup the docs repo so we can update them with the current build
+git config --global user.email "Circle CI" > /dev/null 2>&1
+git config --global user.name "bot@try.out" > /dev/null 2>&1
+git clone git@github.com:sphinx-gallery/sphinx-gallery.github.io.git
+cd sphinx-gallery.github.io/
+
+# copy over or recompile the new site
+git rm -rf .
+cp -a "../${siteSource}/." .
+# github nojekyll
+touch .nojekyll
+
+# stage any changes and new files
+git add -A
+# now commit
+git commit --allow-empty -m "Update the docs from master"
+# and push, but send any output to /dev/null to hide anything sensitive
+git push --force --quiet origin master > /dev/null 2>&1
+
+# go back to where we started
+cd ..
+
+echo "Finished Deployment!"
diff --git a/examples/no_output/plot_raise.py b/examples/no_output/plot_raise.py
index a9b8f09..94f995b 100644
--- a/examples/no_output/plot_raise.py
+++ b/examples/no_output/plot_raise.py
@@ -11,6 +11,7 @@ You also get the python traceback of the failed code block
 
 # Code source: Óscar Nájera
 # License: BSD 3 clause
+# sphinx_gallery_line_numbers = True
 
 import numpy as np
 import matplotlib.pyplot as plt
diff --git a/sphinx_gallery/__init__.py b/sphinx_gallery/__init__.py
index e113f97..811241f 100644
--- a/sphinx_gallery/__init__.py
+++ b/sphinx_gallery/__init__.py
@@ -4,7 +4,7 @@ Sphinx Gallery
 
 """
 import os
-__version__ = '0.1.11'
+__version__ = '0.1.12'
 
 
 def glr_path_static():
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index 8d6e581..aa6d06f 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -15,6 +15,7 @@ Files that generate images should start with 'plot'
 # tricky errors come up with exec(code_blocks, ...) calls
 from __future__ import division, print_function, absolute_import
 from time import time
+import ast
 import codecs
 import hashlib
 import os
@@ -23,6 +24,7 @@ import shutil
 import subprocess
 import sys
 import traceback
+from distutils.version import LooseVersion
 
 
 # Try Python 2 first, otherwise load from Python 3
@@ -69,13 +71,13 @@ if matplotlib_backend != 'agg':
     raise ValueError(mpl_backend_msg.format(matplotlib_backend))
 
 import matplotlib.pyplot as plt
+import sphinx
 
 from . import glr_path_static
 from . import sphinx_compatibility
 from .backreferences import write_backreferences, _thumbnail_div
 from .downloads import CODE_DOWNLOAD
-from .py_source_parser import (get_docstring_and_rest,
-                               split_code_and_text_blocks)
+from .py_source_parser import split_code_and_text_blocks
 
 from .notebook import jupyter_notebook, save_notebook
 
@@ -135,36 +137,25 @@ SPHX_GLR_SIG = """\n.. rst-class:: sphx-glr-signature
     `Generated by Sphinx-Gallery <https://sphinx-gallery.readthedocs.io>`_\n"""
 
 
-def codestr2rst(codestr, lang='python'):
+def codestr2rst(codestr, lang='python', lineno=None):
     """Return reStructuredText code block from code string"""
-    code_directive = "\n.. code-block:: {0}\n\n".format(lang)
+    if lineno is not None:
+        if LooseVersion(sphinx.__version__) >= '1.3':
+            # Sphinx only starts numbering from the first non-empty line.
+            blank_lines = codestr.count('\n', 0, -len(codestr.lstrip()))
+            lineno = '   :lineno-start: {0}\n'.format(lineno + blank_lines)
+        else:
+            lineno = '   :linenos:\n'
+    else:
+        lineno = ''
+    code_directive = "\n.. code-block:: {0}\n{1}\n".format(lang, lineno)
     indented_block = indent(codestr, ' ' * 4)
     return code_directive + indented_block
 
 
-def extract_thumbnail_number(text):
-    """ Pull out the thumbnail image number specified in the docstring. """
-
-    # check whether the user has specified a specific thumbnail image
-    pattr = re.compile(
-        r"^\s*#\s*sphinx_gallery_thumbnail_number\s*=\s*([0-9]+)\s*$",
-        flags=re.MULTILINE)
-    match = pattr.search(text)
-
-    if match is None:
-        # by default, use the first figure created
-        thumbnail_number = 1
-    else:
-        thumbnail_number = int(match.groups()[0])
-
-    return thumbnail_number
-
-
-def extract_intro(filename):
+def extract_intro(filename, docstring):
     """ Extract the first paragraph of module-level docstring. max:95 char"""
 
-    docstring, _ = get_docstring_and_rest(filename)
-
     # lstrip is just in case docstring has a '\n\n' at the beginning
     paragraphs = docstring.lstrip().split('\n\n')
     if len(paragraphs) > 1:
@@ -341,11 +332,13 @@ def scale_image(in_fname, out_fname, max_width, max_height):
                 'Install optipng to reduce the size of the generated images')
 
 
-def save_thumbnail(image_path_template, src_file, gallery_conf):
+def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
     """Save the thumbnail image"""
     # read specification of the figure to display as thumbnail from main text
-    _, content = get_docstring_and_rest(src_file)
-    thumbnail_number = extract_thumbnail_number(content)
+    thumbnail_number = file_conf.get('thumbnail_number', 1)
+    if not isinstance(thumbnail_number, int):
+        raise TypeError(
+            'sphinx_gallery_thumbnail_number setting is not a number.')
     thumbnail_image_path = image_path_template.format(thumbnail_number)
 
     thumb_dir = os.path.join(os.path.dirname(thumbnail_image_path), 'thumb')
@@ -392,11 +385,13 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs):
         'Generating gallery for %s ' % build_target_dir,
         length=len(sorted_listdir))
     for fname in iterator:
-        amount_of_code, time_elapsed = generate_file_rst(fname, target_dir,
-                                                         src_dir, gallery_conf)
+        intro, amount_of_code, time_elapsed = generate_file_rst(
+            fname,
+            target_dir,
+            src_dir,
+            gallery_conf)
         computation_times.append((time_elapsed, fname))
         new_fname = os.path.join(src_dir, fname)
-        intro = extract_intro(new_fname)
         this_entry = _thumbnail_div(build_target_dir, fname, intro) + """
 
 .. toctree::
@@ -422,15 +417,42 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs):
     return fhindex, computation_times
 
 
-def execute_code_block(src_file, code_block, example_globals,
+def handle_exception(exc_info, src_file, block_vars, gallery_conf):
+    etype, exc, tb = exc_info
+    stack = traceback.extract_tb(tb)
+    # Remove our code from traceback:
+    if isinstance(exc, SyntaxError):
+        # Remove one extra level through ast.parse.
+        stack = stack[2:]
+    else:
+        stack = stack[1:]
+    formatted_exception = 'Traceback (most recent call last):\n' + ''.join(
+        traceback.format_list(stack) +
+        traceback.format_exception_only(etype, exc))
+
+    logger.warning('%s failed to execute correctly: %s', src_file,
+                   formatted_exception)
+
+    except_rst = codestr2rst(formatted_exception, lang='pytb')
+
+    # Breaks build on first example error
+    if gallery_conf['abort_on_example_error']:
+        raise
+    # Stores failing file
+    gallery_conf['failing_examples'][src_file] = formatted_exception
+    block_vars['execute_script'] = False
+
+    return except_rst
+
+
+def execute_code_block(src_file, code_block, lineno, example_globals,
                        block_vars, gallery_conf):
     """Executes the code block of the example file"""
     time_elapsed = 0
-    stdout = ''
 
     # If example is not suitable to run, skip executing its blocks
     if not block_vars['execute_script']:
-        return stdout, time_elapsed
+        return '', time_elapsed
 
     plt.close('all')
     cwd = os.getcwd()
@@ -438,54 +460,49 @@ def execute_code_block(src_file, code_block, example_globals,
     orig_stdout = sys.stdout
     src_file = block_vars['src_file']
 
-    try:
-        # First cd in the original example dir, so that any file
-        # created by the example get created in this directory
-        os.chdir(os.path.dirname(src_file))
-        sys.stdout = my_stdout = MixedEncodingStringIO()
+    # First cd in the original example dir, so that any file
+    # created by the example get created in this directory
+    my_stdout = MixedEncodingStringIO()
+    os.chdir(os.path.dirname(src_file))
+    sys.stdout = my_stdout
 
+    try:
+        code_ast = ast.parse(code_block, src_file)
+        ast.increment_lineno(code_ast, lineno - 1)
         t_start = time()
         # don't use unicode_literals at the top of this file or you get
         # nasty errors here on Py2.7
-        exec(code_block, example_globals)
+        exec(compile(code_ast, src_file, 'exec'), example_globals)
         time_elapsed = time() - t_start
 
+    except Exception:
+        sys.stdout = orig_stdout
+        except_rst = handle_exception(sys.exc_info(), src_file, block_vars,
+                                      gallery_conf)
+        code_output = u"\n{0}\n\n\n\n".format(except_rst)
+
+    else:
         sys.stdout = orig_stdout
+        os.chdir(cwd)
 
         my_stdout = my_stdout.getvalue().strip().expandtabs()
         if my_stdout:
             stdout = CODE_OUTPUT.format(indent(my_stdout, u' ' * 4))
             logger.verbose('Output from %s', src_file, color='brown')
             logger.verbose(my_stdout)
-        os.chdir(cwd)
+        else:
+            stdout = ''
         images_rst, fig_num = save_figures(block_vars['image_path'],
-                                           block_vars['fig_count'], gallery_conf)
-
-    except Exception:
-        formatted_exception = traceback.format_exc()
-
-        logger.warning('%s failed to execute correctly:%s', src_file,
-                       formatted_exception)
+                                           block_vars['fig_count'],
+                                           gallery_conf)
 
-        fig_num = 0
-        images_rst = codestr2rst(formatted_exception, lang='pytb')
-
-        # Breaks build on first example error
-        # XXX This check can break during testing e.g. if you uncomment the
-        # `raise RuntimeError` by the `my_stdout` call, maybe use `.get()`?
-        if gallery_conf['abort_on_example_error']:
-            raise
-        # Stores failing file
-        gallery_conf['failing_examples'][src_file] = formatted_exception
-        block_vars['execute_script'] = False
+        block_vars['fig_count'] += fig_num
+        code_output = u"\n{0}\n\n{1}\n\n".format(images_rst, stdout)
 
     finally:
         os.chdir(cwd)
         sys.stdout = orig_stdout
 
-    code_output = u"\n{0}\n\n{1}\n\n".format(images_rst, stdout)
-    block_vars['fig_count'] += fig_num
-
     return code_output, time_elapsed
 
 
@@ -512,6 +529,8 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf):
 
     Returns
     -------
+    intro: str
+        The introduction of the example
     amount_of_code : int
         character count of the corresponding python script in file
     time_elapsed : float
@@ -521,13 +540,14 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf):
     src_file = os.path.normpath(os.path.join(src_dir, fname))
     example_file = os.path.join(target_dir, fname)
     shutil.copyfile(src_file, example_file)
-    script_blocks = split_code_and_text_blocks(src_file)
+    file_conf, script_blocks = split_code_and_text_blocks(src_file)
     amount_of_code = sum([len(bcontent)
-                          for blabel, bcontent in script_blocks
+                          for blabel, bcontent, lineno in script_blocks
                           if blabel == 'code'])
+    intro = extract_intro(fname, script_blocks[0][1])
 
     if md5sum_is_current(example_file):
-        return amount_of_code, 0
+        return intro, amount_of_code, 0
 
     image_dir = os.path.join(target_dir, 'images')
     if not os.path.exists(image_dir):
@@ -574,23 +594,27 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf):
         sys.argv[0] = src_file
         sys.argv[1:] = []
 
-    for blabel, bcontent in script_blocks:
+    for blabel, bcontent, lineno in script_blocks:
         if blabel == 'code':
-            code_output, rtime = execute_code_block(src_file, bcontent,
+            code_output, rtime = execute_code_block(src_file, bcontent, lineno,
                                                     example_globals,
                                                     block_vars, gallery_conf)
 
             time_elapsed += rtime
 
+            if not file_conf.get('line_numbers',
+                                 gallery_conf.get('line_numbers', False)):
+                lineno = None
+
             if is_example_notebook_like:
-                example_rst += codestr2rst(bcontent) + '\n'
+                example_rst += codestr2rst(bcontent, lineno=lineno) + '\n'
                 example_rst += code_output
             else:
                 example_rst += code_output
                 if 'sphx-glr-script-out' in code_output:
                     # Add some vertical space after output
                     example_rst += "\n\n|\n\n"
-                example_rst += codestr2rst(bcontent) + '\n'
+                example_rst += codestr2rst(bcontent, lineno=lineno) + '\n'
 
         else:
             example_rst += bcontent + '\n\n'
@@ -604,7 +628,7 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf):
         with open(example_file + '.md5', 'w') as file_checksum:
             file_checksum.write(get_md5sum(example_file))
 
-    save_thumbnail(image_path_template, src_file, gallery_conf)
+    save_thumbnail(image_path_template, src_file, file_conf, gallery_conf)
 
     time_m, time_s = divmod(time_elapsed, 60)
     example_nb = jupyter_notebook(script_blocks)
@@ -622,4 +646,4 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf):
     if block_vars['execute_script']:
         logger.debug("%s ran in : %.2g seconds", src_file, time_elapsed)
 
-    return amount_of_code, time_elapsed
+    return intro, amount_of_code, time_elapsed
diff --git a/sphinx_gallery/notebook.py b/sphinx_gallery/notebook.py
index a0cfdbd..4ffee59 100644
--- a/sphinx_gallery/notebook.py
+++ b/sphinx_gallery/notebook.py
@@ -157,7 +157,7 @@ def fill_notebook(work_notebook, script_blocks):
     script_blocks : list of tuples
     """
 
-    for blabel, bcontent in script_blocks:
+    for blabel, bcontent, lineno in script_blocks:
         if blabel == 'code':
             add_code_cell(work_notebook, bcontent)
         else:
@@ -187,7 +187,7 @@ def python_to_jupyter_cli(args=None, namespace=None):
     args = parser.parse_args(args, namespace)
 
     for src_file in args.python_src_file:
-        blocks = split_code_and_text_blocks(src_file)
+        file_conf, blocks = split_code_and_text_blocks(src_file)
         print('Converting {0}'.format(src_file))
         example_nb = jupyter_notebook(blocks)
         save_notebook(example_nb, src_file.replace('.py', '.ipynb'))
diff --git a/sphinx_gallery/py_source_parser.py b/sphinx_gallery/py_source_parser.py
index d397087..f19d7dc 100644
--- a/sphinx_gallery/py_source_parser.py
+++ b/sphinx_gallery/py_source_parser.py
@@ -42,7 +42,7 @@ def get_docstring_and_rest(filename):
     try:
         node = ast.parse(content)
     except SyntaxError:
-        return SYNTAX_ERROR_DOCSTRING, content.decode('utf-8')
+        return SYNTAX_ERROR_DOCSTRING, content.decode('utf-8'), 1
 
     if not isinstance(node, ast.Module):
         raise TypeError("This function only supports modules. "
@@ -53,47 +53,80 @@ def get_docstring_and_rest(filename):
         docstring = docstring_node.value.s
         if hasattr(docstring, 'decode'):  # python2.7
             docstring = docstring.decode('utf-8')
+        lineno = docstring_node.lineno  # The last line of the string.
         # This get the content of the file after the docstring last line
         # Note: 'maxsplit' argument is not a keyword argument in python2
-        rest = content.decode('utf-8').split('\n', docstring_node.lineno)[-1]
-        return docstring, rest
+        rest = content.decode('utf-8').split('\n', lineno)[-1]
+        return docstring, rest, lineno + 1
     else:
         raise ValueError(('Could not find docstring in file "{0}". '
                           'A docstring is required by sphinx-gallery')
                          .format(filename))
 
 
+def extract_file_config(content):
+    """
+    Pull out the file-specific config specified in the docstring.
+    """
+
+    prop_pat = re.compile(
+        r"^\s*#\s*sphinx_gallery_([A-Za-z0-9_]+)\s*=\s*(.+)\s*$",
+        re.MULTILINE)
+    file_conf = {}
+    for match in re.finditer(prop_pat, content):
+        name = match.group(1)
+        value = match.group(2)
+        try:
+            value = ast.literal_eval(value)
+        except (SyntaxError, ValueError):
+            logger.warning(
+                'Sphinx-gallery option %s was passed invalid value %s',
+                name, value)
+        else:
+            file_conf[name] = value
+    return file_conf
+
+
 def split_code_and_text_blocks(source_file):
     """Return list with source file separated into code and text blocks.
 
     Returns
     -------
+    file_conf : dict
+        File-specific settings given in comments as:
+        # sphinx_gallery_<name> = <value>
     blocks : list of (label, content)
         List where each element is a tuple with the label ('text' or 'code'),
         and content string of block.
     """
-    docstring, rest_of_content = get_docstring_and_rest(source_file)
-    blocks = [('text', docstring)]
+    docstring, rest_of_content, lineno = get_docstring_and_rest(source_file)
+    blocks = [('text', docstring, 1)]
+
+    file_conf = extract_file_config(rest_of_content)
 
     pattern = re.compile(
         r'(?P<header_line>^#{20,}.*)\s(?P<text_content>(?:^#.*\s)*)',
         flags=re.M)
+    sub_pat = re.compile('^#', flags=re.M)
 
     pos_so_far = 0
     for match in re.finditer(pattern, rest_of_content):
-        match_start_pos, match_end_pos = match.span()
-        code_block_content = rest_of_content[pos_so_far:match_start_pos]
+        code_block_content = rest_of_content[pos_so_far:match.start()]
+        if code_block_content.strip():
+            blocks.append(('code', code_block_content, lineno))
+        lineno += code_block_content.count('\n')
+
+        lineno += 1  # Ignored header line of hashes.
         text_content = match.group('text_content')
-        sub_pat = re.compile('^#', flags=re.M)
         text_block_content = dedent(re.sub(sub_pat, '', text_content)).lstrip()
-        if code_block_content.strip():
-            blocks.append(('code', code_block_content))
         if text_block_content.strip():
-            blocks.append(('text', text_block_content))
-        pos_so_far = match_end_pos
+            blocks.append(('text', text_block_content, lineno))
+        lineno += text_content.count('\n')
+
+        pos_so_far = match.end()
 
     remaining_content = rest_of_content[pos_so_far:]
     if remaining_content.strip():
-        blocks.append(('code', remaining_content))
+        blocks.append(('code', remaining_content, lineno))
 
-    return blocks
+    return file_conf, blocks


Test patch:
diff --git a/sphinx_gallery/tests/reference_parse.txt b/sphinx_gallery/tests/reference_parse.txt
index a5ae343..31ff943 100644
--- a/sphinx_gallery/tests/reference_parse.txt
+++ b/sphinx_gallery/tests/reference_parse.txt
@@ -1,24 +1,33 @@
 [('text',
-  '\nThe Header Docstring\n====================\n\nWhen writting latex in a Python string keep in mind to escape the backslashes\nor use a raw docstring\n\n.. math:: \\sin (x)\n\nClosing this string quotes on same line'),
- ('text', 'Direct first comment\nwith second line\n'),
- ('code', '\nimport numpy as np\n\n'),
- ('code', 'A = 1\n\nimport matplotlib.pyplot as plt\n\n'),
+  '\nThe Header Docstring\n====================\n\nWhen writting latex in a Python string keep in mind to escape the backslashes\nor use a raw docstring\n\n.. math:: \\sin (x)\n\nClosing this string quotes on same line',
+  1),
+ ('text', 'Direct first comment\nwith second line\n', 15),
+ ('code', '\nimport numpy as np\n\n', 17),
+ ('code', 'A = 1\n\nimport matplotlib.pyplot as plt\n\n', 21),
  ('text',
-  'There is no need to always alternate between code and comment blocks\nNow there is free repetition of both\n'),
- ('text', 'And a single line of hashes can split your blocks\n'),
- ('text', 'Latex in the comments does not need to be escaped\n\n.. math::\n   \\sin\n'),
+  'There is no need to always alternate between code and comment blocks\nNow there is free repetition of both\n',
+  26),
+ ('text', 'And a single line of hashes can split your blocks\n', 30),
+ ('text', 'Latex in the comments does not need to be escaped\n\n.. math::\n   \\sin\n', 34),
  ('code',
-  '\ndef dummy():\n    """This should not be part of a \'text\' block\'"""\n\n    ######################################\n    # Comment inside code to remain here\n    pass\n\n# this should not be part of a \'text\' block\n\n'),
+  '\ndef dummy():\n    """This should not be part of a \'text\' block\'"""\n\n    ######################################\n    # Comment inside code to remain here\n    pass\n\n# this should not be part of a \'text\' block\n\n',
+  38),
  ('text',
-  '####################################################################\n\nMaking a line cut in sphinx\n'),
+  '####################################################################\n\nMaking a line cut in sphinx\n',
+  49),
  ('text',
-  '.. warning::\n    The next kind of comments are not supported and become to hard to escape\n    so just don\'t code like this.\n\n.. code-block:: python\n\n    def dummy2():\n        """Function docstring"""\n    ####################################\n    # This comment inside python indentation\n    # breaks the block structure and is not\n    # supported\n        dummy2\n\n'),
- ('code', '\n"""Free strings are not supported they remain part of the code"""\n\n'),
+  '.. warning::\n    The next kind of comments are not supported and become to hard to escape\n    so just don\'t code like this.\n\n.. code-block:: python\n\n    def dummy2():\n        """Function docstring"""\n    ####################################\n    # This comment inside python indentation\n    # breaks the block structure and is not\n    # supported\n        dummy2\n\n',
+  55),
+ ('code', '\n"""Free strings are not supported they remain part of the code"""\n\n', 69),
  ('text',
-  'New lines can be included in you block comments and the parser\nis capable of retaining this significant whitespace to work with sphinx\n\nSo the reStructuredText headers survive\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'),
- ('code', "\n\nprint('one')\n\n"),
- ('text', 'Code block separators\n##############################################################################\n Surrounding a comment line with lines of # like a block spliter also\n works and creates a new header for that comment block\n too. Nevertheless to get rich text formatting we advise to use\n RestructuredText syntax in the comment blocks.\n'),
- ('code', "\nprint('two')\n"),
- ('code', 'B = 1\n\n'),
+  'New lines can be included in you block comments and the parser\nis capable of retaining this significant whitespace to work with sphinx\n\nSo the reStructuredText headers survive\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
+  73),
+ ('code', "\n\nprint('one')\n\n", 78),
  ('text',
-  "End comments\n\nThat's all folks !\n\n.. literalinclude:: plot_parse.py\n\n\n")]
+  'Code block separators\n##############################################################################\n Surrounding a comment line with lines of # like a block spliter also\n works and creates a new header for that comment block\n too. Nevertheless to get rich text formatting we advise to use\n RestructuredText syntax in the comment blocks.\n',
+  83),
+ ('code', "\nprint('two')\n", 89),
+ ('code', 'B = 1\n\n', 93),
+ ('text',
+  "End comments\n\nThat's all folks !\n\n.. literalinclude:: plot_parse.py\n\n\n",
+  96)]
diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py
index 185bcdd..1047f37 100644
--- a/sphinx_gallery/tests/test_gen_rst.py
+++ b/sphinx_gallery/tests/test_gen_rst.py
@@ -54,8 +54,10 @@ CONTENT = [
 def test_split_code_and_text_blocks():
     """Test if a known example file gets properly split"""
 
-    blocks = sg.split_code_and_text_blocks('examples/no_output/just_code.py')
+    file_conf, blocks = sg.split_code_and_text_blocks(
+        'examples/no_output/just_code.py')
 
+    assert file_conf == {}
     assert blocks[0][0] == 'text'
     assert blocks[1][0] == 'code'
 
@@ -66,8 +68,10 @@ def test_bug_cases_of_notebook_syntax():
 
     with open('sphinx_gallery/tests/reference_parse.txt') as reference:
         ref_blocks = ast.literal_eval(reference.read())
-        blocks = sg.split_code_and_text_blocks('tutorials/plot_parse.py')
+        file_conf, blocks = sg.split_code_and_text_blocks(
+            'tutorials/plot_parse.py')
 
+        assert file_conf == {}
         assert blocks == ref_blocks
 
 
@@ -81,16 +85,17 @@ def test_direct_comment_after_docstring():
                            'x, y = 1, 2',
                            '']))
     try:
-        result = sg.split_code_and_text_blocks(f.name)
+        file_conf, result = sg.split_code_and_text_blocks(f.name)
     finally:
         os.remove(f.name)
 
+    assert file_conf == {}
     expected_result = [
-        ('text', 'Docstring'),
+        ('text', 'Docstring', 1),
         ('code', '\n'.join(['# and now comes the module code',
                             '# with a second line of comment',
                             'x, y = 1, 2',
-                            '']))]
+                            '']), 2)]
     assert result == expected_result
 
 
@@ -105,12 +110,7 @@ def test_codestr2rst():
 
 
 def test_extract_intro():
-    with tempfile.NamedTemporaryFile('wb', delete=False) as f:
-        f.write('\n'.join(CONTENT).encode('utf-8'))
-    try:
-        result = sg.extract_intro(f.name)
-    finally:
-        os.remove(f.name)
+    result = sg.extract_intro('<string>', '\n'.join(CONTENT[1:9]))
     assert 'Docstring' not in result
     assert result == 'This is the description of the example which goes on and on, Óscar'  # noqa
     assert 'second paragraph' not in result
@@ -222,21 +222,21 @@ def test_pattern_matching(gallery_conf, log_collector):
             assert code_output not in rst
 
 
-def test_thumbnail_number():
+@pytest.mark.parametrize('test_str', [
+    '# sphinx_gallery_thumbnail_number= 2',
+    '# sphinx_gallery_thumbnail_number=2',
+    '#sphinx_gallery_thumbnail_number = 2',
+    '    # sphinx_gallery_thumbnail_number=2'])
+def test_thumbnail_number(test_str):
     # which plot to show as the thumbnail image
-    for test_str in ['# sphinx_gallery_thumbnail_number= 2',
-                     '# sphinx_gallery_thumbnail_number=2',
-                     '#sphinx_gallery_thumbnail_number = 2',
-                     '    # sphinx_gallery_thumbnail_number=2']:
-        with tempfile.NamedTemporaryFile('w', delete=False) as f:
-            f.write('\n'.join(['"Docstring"',
-                               test_str]))
-        try:
-            _, content = sg.get_docstring_and_rest(f.name)
-        finally:
-            os.remove(f.name)
-        thumbnail_number = sg.extract_thumbnail_number(content)
-        assert thumbnail_number == 2
+    with tempfile.NamedTemporaryFile('w', delete=False) as f:
+        f.write('\n'.join(['"Docstring"',
+                           test_str]))
+    try:
+        file_conf, blocks = sg.split_code_and_text_blocks(f.name)
+    finally:
+        os.remove(f.name)
+    assert file_conf == {'thumbnail_number': 2}
 
 
 def test_save_figures(gallery_conf):
diff --git a/sphinx_gallery/tests/test_notebook.py b/sphinx_gallery/tests/test_notebook.py
index aa0d938..5f7760e 100644
--- a/sphinx_gallery/tests/test_notebook.py
+++ b/sphinx_gallery/tests/test_notebook.py
@@ -80,7 +80,7 @@ For more details on interpolation see the page `channel_interpolation`.
 
 def test_jupyter_notebook():
     """Test that written ipython notebook file corresponds to python object"""
-    blocks = sg.split_code_and_text_blocks('tutorials/plot_parse.py')
+    file_conf, blocks = sg.split_code_and_text_blocks('tutorials/plot_parse.py')
     example_nb = jupyter_notebook(blocks)
 
     with tempfile.NamedTemporaryFile('w', delete=False) as f: