Repository: sphinx-gallery/sphinx-gallery

Issue:
ENH: Check for invalid sphinx_gallery_conf keys
I made silly error of misspelling `examples_dirs` as `example_dir` in the `sphinx_gallery_conf` dict in the `conf.py` for a project.

The path to my gallery examples was not the default value of `../examples`, so I was confused by the following error:

```
Extension error:
Example directory proj/doc/source/../examples does not have a README file with one of the expected file extensions ['.txt', '.rst']. Please write one to introduce your gallery.
```

Of course, the *real* error was my misspelling. Everything behaves exactly as it should, but I probably would've been able to chase down my error faster if there had been a warning/exception drawing attention to the fact that I had passed an invalid key to the `sphinx_gallery_conf` dict.

Discussion:
larsoner: @rossbar feel free to look and try

Patch:
diff --git a/sphinx_gallery/__init__.py b/sphinx_gallery/__init__.py
index 0eccbfc..925099b 100644
--- a/sphinx_gallery/__init__.py
+++ b/sphinx_gallery/__init__.py
@@ -6,7 +6,7 @@ Sphinx Gallery
 import os
 # dev versions should have "dev" in them, stable should not.
 # doc/conf.py makes use of this to set the version drop-down.
-__version__ = '0.8.2'
+__version__ = '0.9.0.dev0'
 
 
 def glr_path_static():
diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py
index 5292dbe..d6dd19b 100644
--- a/sphinx_gallery/gen_gallery.py
+++ b/sphinx_gallery/gen_gallery.py
@@ -14,6 +14,7 @@ from __future__ import division, print_function, absolute_import
 import codecs
 import copy
 from datetime import timedelta, datetime
+from difflib import get_close_matches
 from importlib import import_module
 import re
 import os
@@ -91,6 +92,8 @@ DEFAULT_GALLERY_CONF = {
     'inspect_global_variables': True,
     'css': _KNOWN_CSS,
     'matplotlib_animations': False,
+    'default_thumb_file': None,
+    'line_numbers': False,
 }
 
 logger = sphinx_compatibility.getLogger('sphinx-gallery')
@@ -105,7 +108,7 @@ def _bool_eval(x):
     return bool(x)
 
 
-def parse_config(app):
+def parse_config(app, check_keys=True):
     """Process the Sphinx Gallery configuration."""
     plot_gallery = _bool_eval(app.builder.config.plot_gallery)
     src_dir = app.builder.srcdir
@@ -114,7 +117,8 @@ def parse_config(app):
     lang = app.builder.config.highlight_language
     gallery_conf = _complete_gallery_conf(
         app.config.sphinx_gallery_conf, src_dir, plot_gallery,
-        abort_on_example_error, lang, app.builder.name, app)
+        abort_on_example_error, lang, app.builder.name, app,
+        check_keys)
 
     # this assures I can call the config in other places
     app.config.sphinx_gallery_conf = gallery_conf
@@ -124,8 +128,21 @@ def parse_config(app):
 
 def _complete_gallery_conf(sphinx_gallery_conf, src_dir, plot_gallery,
                            abort_on_example_error, lang='python',
-                           builder_name='html', app=None):
+                           builder_name='html', app=None, check_keys=True):
     gallery_conf = copy.deepcopy(DEFAULT_GALLERY_CONF)
+    options = sorted(gallery_conf)
+    extra_keys = sorted(set(sphinx_gallery_conf) - set(options))
+    if extra_keys and check_keys:
+        msg = 'Unknown key(s) in sphinx_gallery_conf:\n'
+        for key in extra_keys:
+            options = get_close_matches(key, options, cutoff=0.66)
+            msg += repr(key)
+            if len(options) == 1:
+                msg += ', did you mean %r?' % (options[0],)
+            elif len(options) > 1:
+                msg += ', did you mean one of %r?' % (options,)
+            msg += '\n'
+        raise ConfigError(msg.strip())
     gallery_conf.update(sphinx_gallery_conf)
     if sphinx_gallery_conf.get('find_mayavi_figures', False):
         logger.warning(
@@ -143,20 +160,6 @@ def _complete_gallery_conf(sphinx_gallery_conf, src_dir, plot_gallery,
     gallery_conf['src_dir'] = src_dir
     gallery_conf['app'] = app
 
-    if gallery_conf.get("mod_example_dir", False):
-        backreferences_warning = """\n========
-        Sphinx-Gallery found the configuration key 'mod_example_dir'. This
-        is deprecated, and you should now use the key 'backreferences_dir'
-        instead. Support for 'mod_example_dir' will be removed in a subsequent
-        version of Sphinx-Gallery. For more details, see the backreferences
-        documentation:
-
-        https://sphinx-gallery.github.io/configuration.html#references-to-examples"""  # noqa: E501
-        gallery_conf['backreferences_dir'] = gallery_conf['mod_example_dir']
-        logger.warning(
-            backreferences_warning,
-            type=DeprecationWarning)
-
     # Check capture_repr
     capture_repr = gallery_conf['capture_repr']
     supported_reprs = ['__repr__', '__str__', '_repr_html_']
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index c9a8292..0ed8582 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -301,8 +301,9 @@ def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
         img = thumbnail_image_path
     elif not os.path.exists(thumb_file):
         # create something to replace the thumbnail
-        img = os.path.join(glr_path_static(), 'no_image.png')
-        img = gallery_conf.get("default_thumb_file", img)
+        img = gallery_conf.get("default_thumb_file")
+        if img is None:
+            img = os.path.join(glr_path_static(), 'no_image.png')
     else:
         return
     if ext in ('svg', 'gif'):


Test patch:
diff --git a/sphinx_gallery/tests/test_gen_gallery.py b/sphinx_gallery/tests/test_gen_gallery.py
index 56b607f..b8576bf 100644
--- a/sphinx_gallery/tests/test_gen_gallery.py
+++ b/sphinx_gallery/tests/test_gen_gallery.py
@@ -14,10 +14,19 @@ import re
 import pytest
 
 from sphinx.errors import ConfigError, ExtensionError
-from sphinx_gallery.gen_gallery import (check_duplicate_filenames,
-                                        check_spaces_in_filenames,
-                                        collect_gallery_files,
-                                        write_computation_times)
+from sphinx_gallery.gen_gallery import (
+    check_duplicate_filenames, check_spaces_in_filenames,
+    collect_gallery_files, write_computation_times, _complete_gallery_conf)
+
+
+def test_bad_config():
+    """Test that bad config values are caught."""
+    sphinx_gallery_conf = dict(example_dir='')
+    with pytest.raises(ConfigError, match="example_dir.*did you mean 'examples_dirs'?.*"):  # noqa: E501
+        _complete_gallery_conf(sphinx_gallery_conf, '', True, False)
+    sphinx_gallery_conf = dict(n_subsection_order='')
+    with pytest.raises(ConfigError, match=r"did you mean one of \['subsection_order', 'within_.*"):  # noqa: E501
+        _complete_gallery_conf(sphinx_gallery_conf, '', True, False)
 
 
 def test_default_config(sphinx_app_wrapper):
@@ -81,30 +90,6 @@ def test_bad_css(sphinx_app_wrapper, err_class, err_match):
         sphinx_app_wrapper.create_sphinx_app()
 
 
-@pytest.mark.conf_file(content="""
-sphinx_gallery_conf = {
-    'mod_example_dir' : os.path.join('modules', 'gen'),
-    'examples_dirs': 'src',
-    'gallery_dirs': 'ex',
-}""")
-def test_config_old_backreferences_conf(sphinx_app_wrapper):
-    """Testing Deprecation warning message against old backreference config
-
-    In this case the user is required to update the mod_example_dir config
-    variable Sphinx-Gallery should notify the user and also silently update
-    the old config to the new one. """
-    sphinx_app = sphinx_app_wrapper.create_sphinx_app()
-    cfg = sphinx_app.config
-    assert cfg.project == "Sphinx-Gallery <Tests>"
-    assert cfg.sphinx_gallery_conf['backreferences_dir'] == os.path.join(
-        'modules', 'gen')
-    build_warn = sphinx_app._warning.getvalue()
-
-    assert "WARNING:" in build_warn
-    assert "deprecated" in build_warn
-    assert "Support for 'mod_example_dir' will be removed" in build_warn
-
-
 @pytest.mark.conf_file(content="""
 sphinx_gallery_conf = {
     'backreferences_dir': os.path.join('gen_modules', 'backreferences'),
@@ -367,7 +352,7 @@ def test_first_notebook_cell_config(sphinx_app_wrapper):
     from sphinx_gallery.gen_gallery import parse_config
     # First cell must be str
     with pytest.raises(ConfigError):
-        parse_config(sphinx_app_wrapper.create_sphinx_app())
+        parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
 
 
 @pytest.mark.conf_file(content="""
@@ -378,7 +363,7 @@ def test_last_notebook_cell_config(sphinx_app_wrapper):
     from sphinx_gallery.gen_gallery import parse_config
     # First cell must be str
     with pytest.raises(ConfigError):
-        parse_config(sphinx_app_wrapper.create_sphinx_app())
+        parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
 
 
 @pytest.mark.conf_file(content="""
@@ -390,7 +375,7 @@ def test_backreferences_dir_config(sphinx_app_wrapper):
     from sphinx_gallery.gen_gallery import parse_config
     with pytest.raises(ConfigError,
                        match="The 'backreferences_dir' parameter must be of"):
-        parse_config(sphinx_app_wrapper.create_sphinx_app())
+        parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
 
 
 @pytest.mark.conf_file(content="""
@@ -402,7 +387,7 @@ sphinx_gallery_conf = {
 def test_backreferences_dir_pathlib_config(sphinx_app_wrapper):
     """Tests pathlib.Path does not raise exception."""
     from sphinx_gallery.gen_gallery import parse_config
-    parse_config(sphinx_app_wrapper.create_sphinx_app())
+    parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
 
 
 def test_write_computation_times_noop():
@@ -419,7 +404,7 @@ def test_pypandoc_config_list(sphinx_app_wrapper):
     with pytest.raises(ConfigError,
                        match="'pypandoc' parameter must be of type bool or "
                              "dict"):
-        parse_config(sphinx_app_wrapper.create_sphinx_app())
+        parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
 
 
 @pytest.mark.conf_file(content="""
@@ -432,4 +417,4 @@ def test_pypandoc_config_keys(sphinx_app_wrapper):
     with pytest.raises(ConfigError,
                        match="'pypandoc' only accepts the following key "
                              "values:"):
-        parse_config(sphinx_app_wrapper.create_sphinx_app())
+        parse_config(sphinx_app_wrapper.create_sphinx_app(), False)
diff --git a/sphinx_gallery/tests/test_scrapers.py b/sphinx_gallery/tests/test_scrapers.py
index 4c73f0c..b0a07cb 100644
--- a/sphinx_gallery/tests/test_scrapers.py
+++ b/sphinx_gallery/tests/test_scrapers.py
@@ -131,19 +131,20 @@ def test_custom_scraper(gallery_conf, monkeypatch):
                   lambda: _custom_func, raising=False)
         for cust in (_custom_func, 'sphinx_gallery'):
             gallery_conf.update(image_scrapers=[cust])
-            _complete_gallery_conf(*complete_args)  # smoke test that it works
+            # smoke test that it works
+            _complete_gallery_conf(*complete_args, check_keys=False)
     # degenerate
     # without the monkey patch to add sphinx_gallery._get_sg_image_scraper,
     # we should get an error
     gallery_conf.update(image_scrapers=['sphinx_gallery'])
     with pytest.raises(ConfigError,
                        match="has no attribute '_get_sg_image_scraper'"):
-        _complete_gallery_conf(*complete_args)
+        _complete_gallery_conf(*complete_args, check_keys=False)
 
     # other degenerate conditions
     gallery_conf.update(image_scrapers=['foo'])
     with pytest.raises(ConfigError, match='Unknown image scraper'):
-        _complete_gallery_conf(*complete_args)
+        _complete_gallery_conf(*complete_args, check_keys=False)
     gallery_conf.update(image_scrapers=[_custom_func])
     fname_template = os.path.join(gallery_conf['gallery_dir'],
                                   'image{0}.png')
@@ -161,12 +162,12 @@ def test_custom_scraper(gallery_conf, monkeypatch):
         m.setattr(sphinx_gallery, '_get_sg_image_scraper', 'foo',
                   raising=False)
         with pytest.raises(ConfigError, match='^Unknown image.*\n.*callable'):
-            _complete_gallery_conf(*complete_args)
+            _complete_gallery_conf(*complete_args, check_keys=False)
     with monkeypatch.context() as m:
         m.setattr(sphinx_gallery, '_get_sg_image_scraper', lambda: 'foo',
                   raising=False)
         with pytest.raises(ConfigError, match='^Scraper.*was not callable'):
-            _complete_gallery_conf(*complete_args)
+            _complete_gallery_conf(*complete_args, check_keys=False)
 
 
 @pytest.mark.parametrize('ext', _KNOWN_IMG_EXTS)
diff --git a/sphinx_gallery/tests/tinybuild/conf.py b/sphinx_gallery/tests/tinybuild/conf.py
index 6f7b921..3399810 100644
--- a/sphinx_gallery/tests/tinybuild/conf.py
+++ b/sphinx_gallery/tests/tinybuild/conf.py
@@ -82,7 +82,7 @@ sphinx_gallery_conf = {
     'reset_modules': (MockScrapeProblem(), 'matplotlib'),
     'gallery_dirs': ['auto_examples'],
     'backreferences_dir': 'gen_modules/backreferences',
-    'within_section_order': FileNameSortKey,
+    'within_subsection_order': FileNameSortKey,
     'image_scrapers': (matplotlib_format_scraper(),),
     'expected_failing_examples': [
         'examples/future/plot_future_imports_broken.py',