Repository: mkdocs/mkdocs

Issue:
ConfigItems overwrites all data with last entry in the list of config items
`ConfigItems` does not work as expected and results in a list of items that contains only the same values.

### Reproduciton

`plugin.py`
```python
class TestPlugin(BasePlugin):
    config_scheme = (
        (
            "items", mkdocs.config.config_options.ConfigItems(
                ("value", mkdocs.config.config_options.Type(str)),
            ),
        ),
    )

    def on_config(self, config):
        print(self.config)

```

`mkdocs.yml`
```yaml
site_name: Test Site

plugins:
    - test:
        items:
            - value: a
            - value: b
```

This results in:
```
INFO     -  Building documentation...
{'items': [{'value': 'b'}, {'value': 'b'}]}
INFO     -  Cleaning site directory
INFO     -  Documentation built in 0.05 seconds
INFO     -  [12:21:39] Watching paths for changes: 'docs', 'mkdocs.yml'
INFO     -  [12:21:39] Serving on http://127.0.0.1:8000/
```

Note that both "value" values are `b` rather than the first one being `a`.


### Cause

`ConfigItems` creates a single `SubConfig` instance when it is instantiated:
https://github.com/mkdocs/mkdocs/blob/a17a25c60eb6622556bd74735fab84403f319dd5/mkdocs/config/config_options.py#L77

It then uses this instance to do validation:
https://github.com/mkdocs/mkdocs/blob/a17a25c60eb6622556bd74735fab84403f319dd5/mkdocs/config/config_options.py#L94

The `SubConfig` instance uses `load_dict` to populate `self.data` before validation.
https://github.com/mkdocs/mkdocs/blob/a17a25c60eb6622556bd74735fab84403f319dd5/mkdocs/config/config_options.py#L60

The `load_dict` function uses `update` to make changes to `self.data` on the `SubConfig` instance:
https://github.com/mkdocs/mkdocs/blob/a17a25c60eb6622556bd74735fab84403f319dd5/mkdocs/config/base.py#L132

Since there is only one `SubConfig` instance, when this dictionary gets updated it gets updated across all references to it.


### Fix

A number of popular plugins already create their own `ConfigItems` to address this by making `item_config` a property that returns a separate `SubConfig` instance each time it is used. This seems like a reasonable approach.

https://github.com/pieterdavid/mkdocs-doxygen-plugin/blob/49b841b649acf58b668c0b805ab27f9e454920f4/mkdocsdoxygen/configitems.py#L24-L26







Discussion:
Thank you. Yes, this is a real issue.

It's in the same class of issues as https://github.com/mkdocs/mkdocs/issues/2289

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
@@ -49,19 +49,17 @@ def post_validation(self, config, key_name):
         """
 
 
-class SubConfig(BaseConfigOption, Config):
+class SubConfig(BaseConfigOption):
     def __init__(self, *config_options):
-        BaseConfigOption.__init__(self)
-        Config.__init__(self, config_options)
+        super().__init__()
         self.default = {}
-
-    def validate(self, value):
-        self.load_dict(value)
-        return self.run_validation(value)
+        self.config_options = config_options
 
     def run_validation(self, value):
-        Config.validate(self)
-        return self
+        config = Config(self.config_options)
+        config.load_dict(value)
+        config.validate()
+        return config
 
 
 class ConfigItems(BaseConfigOption):


Test patch:
diff --git a/mkdocs/tests/config/config_tests.py b/mkdocs/tests/config/config_tests.py
--- a/mkdocs/tests/config/config_tests.py
+++ b/mkdocs/tests/config/config_tests.py
@@ -283,6 +283,26 @@ def test_doc_dir_in_site_dir(self):
             self.assertEqual(len(errors), 1)
             self.assertEqual(warnings, [])
 
+    SUBCONFIG_TEST_SCHEMA = {
+        "items": mkdocs.config.config_options.ConfigItems(
+            ("value", mkdocs.config.config_options.Type(str)),
+        ),
+    }.items()
+
+    def test_subconfig_with_multiple_items(self):
+        # This had a bug where subsequent items would get merged into the same dict.
+        conf = config.Config(schema=self.SUBCONFIG_TEST_SCHEMA)
+        conf.load_dict(
+            {
+                'items': [
+                    {'value': 'a'},
+                    {'value': 'b'},
+                ]
+            }
+        )
+        conf.validate()
+        self.assertEqual(conf['items'], [{'value': 'a'}, {'value': 'b'}])
+
     def test_multiple_markdown_config_instances(self):
         # This had a bug where an extension config would persist to separate
         # config instances that didn't specify extensions.