Repository: mkdocs/mkdocs

Issue:
` SubConfig` ignore configuration failures
While developing [mkdocs-gallery]() I found out that the `SubConfig` class does not retrieve the failures and warnings from the nested options.

This is because `run_validation` executes `Config.validate` but does not look at its results (`failures, warnings`):
https://github.com/mkdocs/mkdocs/blob/97eafb9094fdd56d19022ae8fda332e728df674b/mkdocs/config/config_options.py#L61



Discussion:

Resolving patch:
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -51,15 +51,34 @@ def post_validation(self, config, key_name):
 
 
 class SubConfig(BaseConfigOption):
-    def __init__(self, *config_options):
+    """
+    Subconfig Config Option
+
+    A set of `config_options` grouped under a single config option.
+    By default, validation errors and warnings resulting from validating
+    `config_options` are ignored (`validate=False`). Users should typically
+    enable validation with `validate=True`.
+    """
+
+    def __init__(self, *config_options, validate=False):
         super().__init__()
         self.default = {}
         self.config_options = config_options
+        self._do_validation = validate
 
     def run_validation(self, value):
         config = Config(self.config_options)
         config.load_dict(value)
-        config.validate()
+        failed, warnings = config.validate()
+
+        if self._do_validation:
+            # Capture errors and warnings
+            self.warnings = warnings
+            if failed:
+                # Get the first failing one
+                key, err = failed[0]
+                raise ValidationError(f"Sub-option {key!r} configuration error: {err}")
+
         return config
 
 


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -7,6 +7,7 @@
 import mkdocs
 from mkdocs.config import config_options
 from mkdocs.config.base import Config
+from mkdocs.exceptions import ConfigurationError
 from mkdocs.tests.base import tempdir
 from mkdocs.utils import yaml_load
 
@@ -854,6 +855,66 @@ def test_defined(self):
             option.validate('somevalue')
 
 
+class SubConfigTest(unittest.TestCase):
+    def test_subconfig_wrong_type(self):
+        # Test that an error is raised if subconfig does not receive a dict
+        option = config_options.SubConfig()
+        with self.assertRaises(ConfigurationError):
+            option.validate("not_a_dict")
+        with self.assertRaises(ConfigurationError):
+            option.validate(("not_a_dict",))
+        with self.assertRaises(ConfigurationError):
+            option.validate(["not_a_dict"])
+
+    def test_subconfig_default(self):
+        """Default behaviour of subconfig: validation is ignored"""
+
+        # Nominal
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict(c='foo'))
+        assert option.warnings == []
+        assert res == dict(c='foo')
+
+        # Invalid option ! No error
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict(c=True))
+        assert option.warnings == []
+        assert res == dict(c=True)
+
+        # Missing option ! Will be considered optional with default None
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict())
+        assert option.warnings == []
+        assert res == dict(c=None)
+
+        # Unknown option ! No warning
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate({"unknown_key_is_ok": 0})
+        assert option.warnings == []
+        assert res == {"c": None, "unknown_key_is_ok": 0}
+
+    def test_subconfig_strict(self):
+        """Strict validation mode for subconfigs."""
+
+        # Unknown option: warning
+        option = config_options.SubConfig(validate=True)
+        res = option.validate({"unknown": 0})
+        assert option.warnings == [('unknown', 'Unrecognised configuration name: unknown')]
+        assert res == {"unknown": 0}
+
+        # Invalid option: error
+        option = config_options.SubConfig(
+            ('c', config_options.Choice(('foo', 'bar'))),
+            validate=True,
+        )
+        with self.assertRaises(config_options.ValidationError):
+            option.validate(dict(c=True))
+
+        # Nominal
+        res = option.validate(dict(c='foo'))
+        assert res == dict(c='foo')
+
+
 class MarkdownExtensionsTest(unittest.TestCase):
     @patch('markdown.Markdown')
     def test_simple_list(self, mockMd):