Repository: mkdocs/mkdocs

Issue:
`enabled` setting for all plugins
It seems that so many plugins have an `enabled` setting these days that we may as well just add it in MkDocs. It will not run the plugin at all then. But if a plugin already has this setting explicitly, then MkDocs will not do anything, so the plugin can still decide what to do when it's disabled.


Discussion:
Yep, definitely agree. This will standardize the way to enable/disable plugins, which would be a great thing to have.
Likewise, I think that'd be useful
> But if a plugin already has this setting explicitly, then MkDocs will not do anything, so the plugin can still decide what to do when it's disabled.

Yes, yes, yes. That would be amazing. If this would mean that we can drop the boilerplate we have to write in each and every event handler of each plugin, so the event handlers are not registered in the first place, this would be absolutely amazing:

``` py
def on_files(self, files, *, config):
    if not self.config.enabled:
        return

    # Do stuff
```

In the plugins of Material for MkDocs there are currently 43 occurrences of this pattern 😅 So the only adjustment we'd need in our plugins is to remove the `enabled` setting from the subclass configuration, correct?

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
@@ -45,6 +45,8 @@
 T = TypeVar('T')
 SomeConfig = TypeVar('SomeConfig', bound=Config)
 
+log = logging.getLogger(__name__)
+
 
 class SubConfig(Generic[SomeConfig], BaseConfigOption[SomeConfig]):
     """
@@ -1137,6 +1139,17 @@ def load_plugin(self, name: str, config) -> plugins.BasePlugin:
                 "because the plugin doesn't declare `supports_multiple_instances`."
             )
 
+        # Only if the plugin doesn't have its own "enabled" config, apply a generic one.
+        if 'enabled' in config and not any(pair[0] == 'enabled' for pair in plugin.config_scheme):
+            enabled = config.pop('enabled')
+            if not isinstance(enabled, bool):
+                raise ValidationError(
+                    f"Plugin '{name}' option 'enabled': Expected boolean but received: {type(enabled)}"
+                )
+            if not enabled:
+                log.debug(f"Plugin '{inst_name}' is disabled in the config, skipping.")
+                return plugin
+
         errors, warns = plugin.load_config(
             config, self._config.config_file_path if self._config else None
         )


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
@@ -1926,6 +1926,15 @@ class FakePlugin2(BasePlugin[_FakePlugin2Config]):
     supports_multiple_instances = True
 
 
+class _EnabledPluginConfig(Config):
+    enabled = c.Type(bool, default=True)
+    bar = c.Type(int, default=0)
+
+
+class EnabledPlugin(BasePlugin[_EnabledPluginConfig]):
+    pass
+
+
 class ThemePlugin(BasePlugin[_FakePluginConfig]):
     pass
 
@@ -1949,6 +1958,7 @@ def load(self):
         return_value=[
             FakeEntryPoint('sample', FakePlugin),
             FakeEntryPoint('sample2', FakePlugin2),
+            FakeEntryPoint('sample-e', EnabledPlugin),
             FakeEntryPoint('readthedocs/sub_plugin', ThemePlugin),
             FakeEntryPoint('overridden', FakePlugin2),
             FakeEntryPoint('readthedocs/overridden', ThemePlugin2),
@@ -2096,6 +2106,49 @@ class Schema(Config):
         self.assertEqual(set(conf.plugins), {'overridden'})
         self.assertIsInstance(conf.plugins['overridden'], FakePlugin2)
 
+    def test_plugin_config_enabled_for_any_plugin(self) -> None:
+        class Schema(Config):
+            theme = c.Theme(default='mkdocs')
+            plugins = c.Plugins(theme_key='theme')
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample': {'enabled': False, 'bar': 3}}}
+        conf = self.get_config(Schema, cfg)
+        self.assertEqual(set(conf.plugins), set())
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample': {'enabled': True, 'bar': 3}}}
+        conf = self.get_config(Schema, cfg)
+        self.assertEqual(set(conf.plugins), {'sample'})
+        self.assertEqual(conf.plugins['sample'].config.bar, 3)
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample': {'enabled': 5}}}
+        with self.expect_error(
+            plugins="Plugin 'sample' option 'enabled': Expected boolean but received: <class 'int'>"
+        ):
+            self.get_config(Schema, cfg)
+
+    def test_plugin_config_enabled_for_plugin_with_setting(self) -> None:
+        class Schema(Config):
+            theme = c.Theme(default='mkdocs')
+            plugins = c.Plugins(theme_key='theme')
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample-e': {'enabled': False, 'bar': 3}}}
+        conf = self.get_config(Schema, cfg)
+        self.assertEqual(set(conf.plugins), {'sample-e'})
+        self.assertEqual(conf.plugins['sample-e'].config.enabled, False)
+        self.assertEqual(conf.plugins['sample-e'].config.bar, 3)
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample-e': {'enabled': True, 'bar': 3}}}
+        conf = self.get_config(Schema, cfg)
+        self.assertEqual(set(conf.plugins), {'sample-e'})
+        self.assertEqual(conf.plugins['sample-e'].config.enabled, True)
+        self.assertEqual(conf.plugins['sample-e'].config.bar, 3)
+
+        cfg = {'theme': 'readthedocs', 'plugins': {'sample-e': {'enabled': 5}}}
+        with self.expect_error(
+            plugins="Plugin 'sample-e' option 'enabled': Expected type: <class 'bool'> but received: <class 'int'>"
+        ):
+            self.get_config(Schema, cfg)
+
     def test_plugin_config_with_multiple_instances(self) -> None:
         class Schema(Config):
             theme = c.Theme(default='mkdocs')