Repository: mkdocs/mkdocs

Issue:
Improve error message for missing Markdown extensions
I just had to install that manually.


Discussion:

Patch:
diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md
index 9ae74fb1..6f0f3518 100644
--- a/docs/about/release-notes.md
+++ b/docs/about/release-notes.md
@@ -66,6 +66,7 @@ authors should review how [search and themes] interact.
 
 ### Other Changes and Additions to Development Version
 
+* Improve Markdown extension error messages. (#782).
 * Drop official support for Python 3.3 and set `tornado>=5.0` (#1427).
 * Add support for GitLab edit links (#1435).
 * Link to GitHub issues from release notes (#644).
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 30da2a2a..a67dd05e 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 from collections import Sequence
 import os
 from collections import namedtuple
+import markdown
 
 from mkdocs import utils, theme, plugins
 from mkdocs.config.base import Config, ValidationError
@@ -629,7 +630,16 @@ class MarkdownExtensions(OptionallyRequired):
                 extensions.append(item)
             else:
                 raise ValidationError('Invalid Markdown Extensions configuration')
-        return utils.reduce_list(self.builtins + extensions)
+
+        extensions = utils.reduce_list(self.builtins + extensions)
+
+        # Confirm that Markdown considers extensions to be valid
+        try:
+            markdown.Markdown(extensions=extensions, extension_configs=self.configdata)
+        except Exception as e:
+            raise ValidationError(e.args[0])
+
+        return extensions
 
     def post_validation(self, config, key_name):
         config[self.configkey] = self.configdata
diff --git a/requirements/project-min.txt b/requirements/project-min.txt
index 6b0262e0..a60bec24 100644
--- a/requirements/project-min.txt
+++ b/requirements/project-min.txt
@@ -4,3 +4,4 @@ livereload==2.5.1
 Markdown==2.5
 PyYAML==3.10
 tornado==4.1
+mdx_gh_links>=0.2


Test patch:
diff --git a/mkdocs/tests/cli_tests.py b/mkdocs/tests/cli_tests.py
index 85ab87de..57f12845 100644
--- a/mkdocs/tests/cli_tests.py
+++ b/mkdocs/tests/cli_tests.py
@@ -186,8 +186,9 @@ class CLITests(unittest.TestCase):
         logger = logging.getLogger('mkdocs')
         self.assertEqual(logger.level, logging.INFO)
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
-    def test_build_clean(self, mock_build):
+    def test_build_clean(self, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['build', '--clean'], catch_exceptions=False)
@@ -198,8 +199,9 @@ class CLITests(unittest.TestCase):
         self.assertTrue('dirty' in kwargs)
         self.assertFalse(kwargs['dirty'])
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
-    def test_build_dirty(self, mock_build):
+    def test_build_dirty(self, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['build', '--dirty'], catch_exceptions=False)
@@ -296,8 +298,9 @@ class CLITests(unittest.TestCase):
             site_dir='custom'
         )
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
-    def test_build_verbose(self, mock_build):
+    def test_build_verbose(self, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['build', '--verbose'], catch_exceptions=False)
@@ -307,8 +310,9 @@ class CLITests(unittest.TestCase):
         logger = logging.getLogger('mkdocs')
         self.assertEqual(logger.level, logging.DEBUG)
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
-    def test_build_quiet(self, mock_build):
+    def test_build_quiet(self, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['build', '--quiet'], catch_exceptions=False)
@@ -352,9 +356,10 @@ class CLITests(unittest.TestCase):
             remote_name=None
         )
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
     @mock.patch('mkdocs.commands.gh_deploy.gh_deploy', autospec=True)
-    def test_gh_deploy_clean(self, mock_gh_deploy, mock_build):
+    def test_gh_deploy_clean(self, mock_gh_deploy, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['gh-deploy', '--clean'], catch_exceptions=False)
@@ -366,9 +371,10 @@ class CLITests(unittest.TestCase):
         self.assertTrue('dirty' in kwargs)
         self.assertFalse(kwargs['dirty'])
 
+    @mock.patch('mkdocs.config.load_config', autospec=True)
     @mock.patch('mkdocs.commands.build.build', autospec=True)
     @mock.patch('mkdocs.commands.gh_deploy.gh_deploy', autospec=True)
-    def test_gh_deploy_dirty(self, mock_gh_deploy, mock_build):
+    def test_gh_deploy_dirty(self, mock_gh_deploy, mock_build, mock_load_config):
 
         result = self.runner.invoke(
             cli.cli, ['gh-deploy', '--dirty'], catch_exceptions=False)
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index 56b7a724..1d573ba2 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 import os
 import unittest
+from mock import patch
 
 import mkdocs
 from mkdocs import utils
@@ -491,7 +492,8 @@ class PrivateTest(unittest.TestCase):
 
 class MarkdownExtensionsTest(unittest.TestCase):
 
-    def test_simple_list(self):
+    @patch('markdown.Markdown')
+    def test_simple_list(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': ['foo', 'bar']
@@ -503,7 +505,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             'mdx_configs': {}
         }, config)
 
-    def test_list_dicts(self):
+    @patch('markdown.Markdown')
+    def test_list_dicts(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': [
@@ -522,7 +525,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             }
         }, config)
 
-    def test_mixed_list(self):
+    @patch('markdown.Markdown')
+    def test_mixed_list(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': [
@@ -539,7 +543,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             }
         }, config)
 
-    def test_builtins(self):
+    @patch('markdown.Markdown')
+    def test_builtins(self, mockMd):
         option = config_options.MarkdownExtensions(builtins=['meta', 'toc'])
         config = {
             'markdown_extensions': ['foo', 'bar']
@@ -577,7 +582,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             'mdx_configs': {'toc': {'permalink': True}}
         }, config)
 
-    def test_configkey(self):
+    @patch('markdown.Markdown')
+    def test_configkey(self, mockMd):
         option = config_options.MarkdownExtensions(configkey='bar')
         config = {
             'markdown_extensions': [
@@ -605,12 +611,14 @@ class MarkdownExtensionsTest(unittest.TestCase):
             'mdx_configs': {}
         }, config)
 
-    def test_not_list(self):
+    @patch('markdown.Markdown')
+    def test_not_list(self, mockMd):
         option = config_options.MarkdownExtensions()
         self.assertRaises(config_options.ValidationError,
                           option.validate, 'not a list')
 
-    def test_invalid_config_option(self):
+    @patch('markdown.Markdown')
+    def test_invalid_config_option(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': [
@@ -622,7 +630,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             option.validate, config['markdown_extensions']
         )
 
-    def test_invalid_config_item(self):
+    @patch('markdown.Markdown')
+    def test_invalid_config_item(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': [
@@ -634,7 +643,8 @@ class MarkdownExtensionsTest(unittest.TestCase):
             option.validate, config['markdown_extensions']
         )
 
-    def test_invalid_dict_item(self):
+    @patch('markdown.Markdown')
+    def test_invalid_dict_item(self, mockMd):
         option = config_options.MarkdownExtensions()
         config = {
             'markdown_extensions': [
@@ -645,3 +655,13 @@ class MarkdownExtensionsTest(unittest.TestCase):
             config_options.ValidationError,
             option.validate, config['markdown_extensions']
         )
+
+    def test_unknown_extension(self):
+        option = config_options.MarkdownExtensions()
+        config = {
+            'markdown_extensions': ['unknown']
+        }
+        self.assertRaises(
+            config_options.ValidationError,
+            option.validate, config['markdown_extensions']
+        )
diff --git a/mkdocs/tests/gh_deploy_tests.py b/mkdocs/tests/gh_deploy_tests.py
index b08e28df..d12f98cc 100644
--- a/mkdocs/tests/gh_deploy_tests.py
+++ b/mkdocs/tests/gh_deploy_tests.py
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
 import unittest
 import mock
 
-from mkdocs.config import load_config
+from mkdocs.tests.base import load_config
 from mkdocs.commands import gh_deploy