Repository: mkdocs/mkdocs

Issue:
Static resources (especially CSS/JS!) shouldn't be automatically included
(Pulling this out from #972.)

Currently, MkDocs automatically includes all static resources from the docs dir in a build. This can cause issues if some files aren't meant to be included but are located in the docs dir. For example, this could happen with files that get preprocessed by another tool into `.md` files for MkDocs (which was how I planned on doing auto-generation of docs from code for MkDocs if I ever get around to it).

However, there's a more-significant issue: all CSS and JS files are automatically included in the generated HTML itself (except when hosting on RTD, apparently). This means that any CSS/JS used for static HTML files will get erroneously included in the MkDocs-generated HTML by default. The inconsistency with RTD adds another wrinkle to this. I think the behavior should be the same no matter what, and as Python says, "Explicit is better than implicit."

At minimum, I think CSS and JS shouldn't be automatically added to the generated HTML, but I'd prefer to see MkDocs only include files that are explicitly listed in `mkdocs.yml`. Doing so would make things more flexible when people want to do unusual things with MkDocs, and with globbing or the ability to include whole subdirectories of static resources, the configuration wouldn't be much more complex than it is now.

Discussion:

Patch:
diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md
index b8d9926b..fdb21e46 100644
--- a/docs/about/release-notes.md
+++ b/docs/about/release-notes.md
@@ -92,6 +92,20 @@ to support such customization.
 
 [blocks]: ../user-guide/styling-your-docs/#overriding-template-blocks
 
+#### Auto-Populated `extra_css` and `extra_javascript` Deprecated. (#986)
+
+In previous versions of MkDocs, if the `extra_css` or `extra_javascript` config
+settings were empty, MkDocs would scan the `docs_dir` and auto-populate each
+setting with all of the CSS and JavaScript files found. This behavior is
+deprecated and a warning will be issued. In the next release, the auto-populate
+feature will stop working and any unlisted CSS and JavaScript files will not be
+included in the HTML templates. In other words, they will still be copied to the
+`site-dir`, but they will not have any effect on the theme if they are not
+explicitly listed.
+
+All CSS and javaScript files in the `docs_dir` should be explicitly listed in
+the `extra_css` or `extra_javascript` config settings going forward.
+
 #### Support for dirty builds. (#990)
 
 For large sites the build time required to create the pages can become problematic,
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index 6bc7bf11..1971d23f 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -221,9 +221,9 @@ directory path.
 
 ### extra_css
 
-Set a list of CSS files to be included by the theme. For example, the
-following example will include the the extra.css file within the css
-subdirectory in your [docs_dir](#docs_dir).
+Set a list of CSS files in your `docs_dir` to be included by the theme. For
+example, the following example will include the the extra.css file within the
+css subdirectory in your [docs_dir](#docs_dir).
 
 ```yaml
 extra_css:
@@ -231,27 +231,23 @@ extra_css:
     - css/second_extra.css
 ```
 
-**default**: By default `extra_css` will contain a list of all the CSS files
-found within the `docs_dir`, if none are found it will be `[]` (an empty list).
+**default**: `[]` (an empty list).
 
 ### extra_javascript
 
-Set a list of JavaScript files to be included by the theme. See the example
-in [extra_css](#extra_css) for usage.
+Set a list of JavaScript files in your `docs_dir` to be included by the theme.
+See the example in [extra_css](#extra_css) for usage.
 
-**default**: By default `extra_javascript` will contain a list of all the
-JavaScript files found within the `docs_dir`, if none are found it will be `[]`
-(an empty list).
+**default**: `[]` (an empty list).
 
 ### extra_templates
 
-Set a list of templates to be built by MkDocs. To see more about writing
-templates for MkDocs read the documentation about [custom themes] and
-specifically the section about the [variables that are available] to templates.
-See the example in [extra_css](#extra_css) for usage.
+Set a list of templates in your `docs_dir` to be built by MkDocs. To see more
+about writing templates for MkDocs read the documentation about [custom themes]
+and specifically the section about the [variables that are available] to
+templates. See the example in [extra_css](#extra_css) for usage.
 
-**default**: Unlike extra_css and extra_javascript, by default `extra_templates`
-will be `[]` (an empty list).
+**default**: `[]` (an empty list).
 
 ### extra
 
diff --git a/mkdocs.yml b/mkdocs.yml
index 26463482..1d4c020c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -18,6 +18,9 @@ pages:
     - Contributing: about/contributing.md
     - License: about/license.md
 
+extra_css:
+    - css/extra.css
+
 markdown_extensions:
     - toc:
         permalink: 
diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py
index 9f376a9b..afcec0a9 100644
--- a/mkdocs/config/base.py
+++ b/mkdocs/config/base.py
@@ -64,24 +64,47 @@ class Config(utils.UserDict):
 
     def _pre_validate(self):
 
+        failed, warnings = [], []
+
         for key, config_option in self._schema:
-            config_option.pre_validation(self, key_name=key)
+            try:
+                config_option.pre_validation(self, key_name=key)
+                warnings.extend([(key, w) for w in config_option.warnings])
+                config_option.reset_warnings()
+            except ValidationError as e:
+                failed.append((key, e))
+
+        return failed, warnings
 
     def _post_validate(self):
 
+        failed, warnings = [], []
+
         for key, config_option in self._schema:
-            config_option.post_validation(self, key_name=key)
+            try:
+                config_option.post_validation(self, key_name=key)
+                warnings.extend([(key, w) for w in config_option.warnings])
+                config_option.reset_warnings()
+            except ValidationError as e:
+                failed.append((key, e))
+
+        return failed, warnings
 
     def validate(self):
 
-        self._pre_validate()
+        failed, warnings = self._pre_validate()
+
+        run_failed, run_warnings = self._validate()
 
-        failed, warnings = self._validate()
+        failed.extend(run_failed)
+        warnings.extend(run_warnings)
 
         # Only run the post validation steps if there are no failures, warnings
         # are okay.
         if len(failed) == 0:
-            self._post_validate()
+            post_failed, post_warnings = self._post_validate()
+            failed.extend(post_failed)
+            warnings.extend(post_warnings)
 
         return failed, warnings
 
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 70f610c4..78ea5455 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -393,6 +393,16 @@ class Extras(OptionallyRequired):
 
         config[key_name] = extras
 
+        if not extras:
+            return
+
+        self.warnings.append((
+            'The following files have been automatically included in the '
+            'documentation build and will be added to the HTML: {0}. This '
+            'behavior is deprecated. In version 1.0 and later they will '
+            "need to be explicitly listed in the '{1}' config setting."
+        ).format(','.join(extras), key_name))
+
 
 class Pages(Extras):
     """


Test patch:
diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py
index aead1c85..607d4244 100644
--- a/mkdocs/tests/build_tests.py
+++ b/mkdocs/tests/build_tests.py
@@ -28,6 +28,8 @@ def load_config(cfg=None):
         cfg['site_name'] = 'Example'
     if 'config_file_path' not in cfg:
         cfg['config_file_path'] = os.path.join(os.path.abspath('.'), 'mkdocs.yml')
+    if 'extra_css' not in cfg:
+        cfg['extra_css'] = ['css/extra.css']
     conf = config.Config(schema=config.DEFAULT_SCHEMA)
     conf.load_dict(cfg)
 
diff --git a/mkdocs/tests/config/base_tests.py b/mkdocs/tests/config/base_tests.py
index 28f42f0a..ce56cd93 100644
--- a/mkdocs/tests/config/base_tests.py
+++ b/mkdocs/tests/config/base_tests.py
@@ -5,6 +5,7 @@ import unittest
 
 from mkdocs import exceptions
 from mkdocs.config import base, defaults
+from mkdocs.config.config_options import BaseConfigOption
 
 
 class ConfigBaseTests(unittest.TestCase):
@@ -125,3 +126,111 @@ class ConfigBaseTests(unittest.TestCase):
                               base.load_config, config_file=config_file.name)
         finally:
             os.remove(config_file.name)
+
+    def test_pre_validation_error(self):
+        class InvalidConfigOption(BaseConfigOption):
+            def pre_validation(self, config, key_name):
+                raise base.ValidationError('pre_validation error')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 1)
+        self.assertEqual(errors[0][0], 'invalid_option')
+        self.assertEqual(str(errors[0][1]), 'pre_validation error')
+        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
+        self.assertEqual(len(warnings), 0)
+
+    def test_run_validation_error(self):
+        class InvalidConfigOption(BaseConfigOption):
+            def run_validation(self, value):
+                raise base.ValidationError('run_validation error')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 1)
+        self.assertEqual(errors[0][0], 'invalid_option')
+        self.assertEqual(str(errors[0][1]), 'run_validation error')
+        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
+        self.assertEqual(len(warnings), 0)
+
+    def test_post_validation_error(self):
+        class InvalidConfigOption(BaseConfigOption):
+            def post_validation(self, config, key_name):
+                raise base.ValidationError('post_validation error')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 1)
+        self.assertEqual(errors[0][0], 'invalid_option')
+        self.assertEqual(str(errors[0][1]), 'post_validation error')
+        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
+        self.assertEqual(len(warnings), 0)
+
+    def test_pre_and_run_validation_errors(self):
+        """ A pre_validation error does not stop run_validation from running. """
+        class InvalidConfigOption(BaseConfigOption):
+            def pre_validation(self, config, key_name):
+                raise base.ValidationError('pre_validation error')
+
+            def run_validation(self, value):
+                raise base.ValidationError('run_validation error')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 2)
+        self.assertEqual(errors[0][0], 'invalid_option')
+        self.assertEqual(str(errors[0][1]), 'pre_validation error')
+        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
+        self.assertEqual(errors[1][0], 'invalid_option')
+        self.assertEqual(str(errors[1][1]), 'run_validation error')
+        self.assertTrue(isinstance(errors[1][1], base.ValidationError))
+        self.assertEqual(len(warnings), 0)
+
+    def test_run_and_post_validation_errors(self):
+        """ A run_validation error stops post_validation from running. """
+        class InvalidConfigOption(BaseConfigOption):
+            def run_validation(self, value):
+                raise base.ValidationError('run_validation error')
+
+            def post_validation(self, config, key_name):
+                raise base.ValidationError('post_validation error')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 1)
+        self.assertEqual(errors[0][0], 'invalid_option')
+        self.assertEqual(str(errors[0][1]), 'run_validation error')
+        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
+        self.assertEqual(len(warnings), 0)
+
+    def test_validation_warnings(self):
+        class InvalidConfigOption(BaseConfigOption):
+            def pre_validation(self, config, key_name):
+                self.warnings.append('pre_validation warning')
+
+            def run_validation(self, value):
+                self.warnings.append('run_validation warning')
+
+            def post_validation(self, config, key_name):
+                self.warnings.append('post_validation warning')
+
+        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))
+
+        errors, warnings = c.validate()
+
+        self.assertEqual(len(errors), 0)
+        self.assertEqual(warnings, [
+            ('invalid_option', 'pre_validation warning'),
+            ('invalid_option', 'run_validation warning'),
+            ('invalid_option', 'post_validation warning'),
+        ])
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index de9396bd..284dbdc6 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -289,7 +289,7 @@ class ExtrasTest(unittest.TestCase):
         self.assertRaises(config_options.ValidationError,
                           option.validate, {})
 
-    def test_talk(self):
+    def test_walk(self):
 
         option = config_options.Extras(utils.is_markdown_file)
 
diff --git a/mkdocs/tests/config/config_tests.py b/mkdocs/tests/config/config_tests.py
index 497cfa6f..ff14478b 100644
--- a/mkdocs/tests/config/config_tests.py
+++ b/mkdocs/tests/config/config_tests.py
@@ -198,7 +198,6 @@ class ConfigTests(unittest.TestCase):
         )
 
         conf = {
-            'site_name': 'Example',
             'config_file_path': j(os.path.abspath('..'), 'mkdocs.yml')
         }
 
@@ -211,7 +210,11 @@ class ConfigTests(unittest.TestCase):
             c = config.Config(schema=(
                 ('docs_dir', config_options.Dir(default='docs')),
                 ('site_dir', config_options.SiteDir(default='site')),
+                ('config_file_path', config_options.Type(utils.string_types))
             ))
             c.load_dict(patch)
 
-            self.assertRaises(config_options.ValidationError, c.validate)
+            errors, warnings = c.validate()
+
+            self.assertEqual(len(errors), 1)
+            self.assertEqual(warnings, [])