From 76709ab540b35e5fdd162d6f189c7e609fb6ad79 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Thu, 6 Oct 2022 23:25:53 +0200 Subject: [PATCH] Expand plugin testing --- mkdocs/plugins.py | 11 +- mkdocs/tests/config/config_options_tests.py | 202 +++++++++++++++++++- mkdocs/tests/plugin_tests.py | 183 ++---------------- 3 files changed, 227 insertions(+), 169 deletions(-) diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py index 49db0444..1167c9f5 100644 --- a/mkdocs/plugins.py +++ b/mkdocs/plugins.py @@ -6,7 +6,6 @@ from __future__ import annotations import logging import sys -from collections import OrderedDict from typing import ( TYPE_CHECKING, Any, @@ -14,6 +13,7 @@ from typing import ( Dict, Generic, List, + MutableMapping, Optional, Tuple, Type, @@ -461,7 +461,7 @@ def event_priority(priority: float) -> Callable[[T], T]: return decorator -class PluginCollection(OrderedDict): +class PluginCollection(dict, MutableMapping[str, BasePlugin]): """ A collection of plugins. @@ -480,8 +480,11 @@ class PluginCollection(OrderedDict): self.events[event_name], method, key=lambda m: -getattr(m, 'mkdocs_priority', 0) ) - def __setitem__(self, key: str, value: BasePlugin, **kwargs) -> None: - super().__setitem__(key, value, **kwargs) + def __getitem__(self, key: str) -> BasePlugin: + return super().__getitem__(key) + + def __setitem__(self, key: str, value: BasePlugin) -> None: + super().__setitem__(key, value) # Register all of the event methods defined for this Plugin. for event_name in (x for x in dir(value) if x.startswith('on_')): method = getattr(value, event_name, None) diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py index 4e7de950..a0a72a43 100644 --- a/mkdocs/tests/config/config_options_tests.py +++ b/mkdocs/tests/config/config_options_tests.py @@ -20,6 +20,7 @@ else: import mkdocs from mkdocs.config import config_options as c from mkdocs.config.base import Config +from mkdocs.plugins import BasePlugin, PluginCollection from mkdocs.tests.base import tempdir from mkdocs.theme import Theme from mkdocs.utils import write_file, yaml_load @@ -1619,6 +1620,205 @@ class MarkdownExtensionsTest(TestCase): self.assertIsNone(conf.mdx_configs.get('toc')) +class _FakePluginConfig(Config): + foo = c.Type(str, default='default foo') + bar = c.Type(int, default=0) + dir = c.Optional(c.Dir(exists=False)) + + +class FakePlugin(BasePlugin[_FakePluginConfig]): + pass + + +class FakeEntryPoint: + @property + def name(self): + return 'sample' + + def load(self): + return FakePlugin + + +@patch('mkdocs.plugins.entry_points', return_value=[FakeEntryPoint()]) +class PluginsTest(TestCase): + def test_plugin_config_without_options(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': ['sample'], + } + conf = self.get_config(Schema, cfg) + + assert_type(conf.plugins, PluginCollection) + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertIn('sample', conf.plugins) + + plugin = conf.plugins['sample'] + assert_type(plugin, BasePlugin) + self.assertIsInstance(plugin, FakePlugin) + self.assertIsInstance(plugin.config, _FakePluginConfig) + expected = { + 'foo': 'default foo', + 'bar': 0, + 'dir': None, + } + self.assertEqual(plugin.config, expected) + + def test_plugin_config_with_options(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': [ + { + 'sample': { + 'foo': 'foo value', + 'bar': 42, + }, + } + ], + } + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertIn('sample', conf.plugins) + self.assertIsInstance(conf.plugins['sample'], BasePlugin) + expected = { + 'foo': 'foo value', + 'bar': 42, + 'dir': None, + } + self.assertEqual(conf.plugins['sample'].config, expected) + + def test_plugin_config_as_dict(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': { + 'sample': { + 'foo': 'foo value', + 'bar': 42, + }, + }, + } + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertIn('sample', conf.plugins) + self.assertIsInstance(conf.plugins['sample'], BasePlugin) + expected = { + 'foo': 'foo value', + 'bar': 42, + 'dir': None, + } + self.assertEqual(conf.plugins['sample'].config, expected) + + def test_plugin_config_empty_list_with_empty_default(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins(default=[]) + + cfg: Dict[str, Any] = {'plugins': []} + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertEqual(len(conf.plugins), 0) + + def test_plugin_config_empty_list_with_default(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins(default=['sample']) + + # Default is ignored + cfg: Dict[str, Any] = {'plugins': []} + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertEqual(len(conf.plugins), 0) + + def test_plugin_config_none_with_empty_default(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins(default=[]) + + cfg = {'plugins': None} + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertEqual(len(conf.plugins), 0) + + def test_plugin_config_none_with_default(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins(default=['sample']) + + # Default is used. + cfg = {'plugins': None} + conf = self.get_config(Schema, cfg) + + self.assertIsInstance(conf.plugins, PluginCollection) + self.assertIn('sample', conf.plugins) + self.assertIsInstance(conf.plugins['sample'], BasePlugin) + expected = { + 'foo': 'default foo', + 'bar': 0, + 'dir': None, + } + self.assertEqual(conf.plugins['sample'].config, expected) + + def test_plugin_config_uninstalled(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = {'plugins': ['uninstalled']} + with self.expect_error(plugins='The "uninstalled" plugin is not installed'): + self.get_config(Schema, cfg) + + def test_plugin_config_not_list(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = {'plugins': 'sample'} + with self.expect_error(plugins="Invalid Plugins configuration. Expected a list or dict."): + self.get_config(Schema, cfg) + + def test_plugin_config_multivalue_dict(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': [ + { + 'sample': { + 'foo': 'foo value', + 'bar': 42, + }, + 'extra_key': 'baz', + } + ], + } + with self.expect_error(plugins="Invalid Plugins configuration"): + self.get_config(Schema, cfg) + + def test_plugin_config_not_string_or_dict(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': [('not a string or dict',)], + } + with self.expect_error(plugins="'('not a string or dict',)' is not a valid plugin name."): + self.get_config(Schema, cfg) + + def test_plugin_config_options_not_dict(self, mock_class) -> None: + class Schema(Config): + plugins = c.Plugins() + + cfg = { + 'plugins': [{'sample': 'not a dict'}], + } + with self.expect_error(plugins="Invalid config options for the 'sample' plugin."): + self.get_config(Schema, cfg) + + class TestHooks(TestCase): class Schema(Config): plugins = c.Plugins(default=[]) @@ -1646,7 +1846,7 @@ class TestHooks(TestCase): {**conf.plugins.events, 'page_markdown': [hook.on_page_markdown]}, conf.plugins.events, ) - self.assertEqual(hook.on_page_markdown('foo foo'), 'zoo zoo') + self.assertEqual(hook.on_page_markdown('foo foo'), 'zoo zoo') # type: ignore[call-arg] self.assertFalse(hasattr(hook, 'on_nav')) diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py index 80c8231b..4c33f06a 100644 --- a/mkdocs/tests/plugin_tests.py +++ b/mkdocs/tests/plugin_tests.py @@ -3,9 +3,17 @@ import os import unittest -from unittest import mock +from typing import TYPE_CHECKING, Optional -from mkdocs import config, plugins +if TYPE_CHECKING: + from typing_extensions import assert_type +else: + + def assert_type(val, typ): + return None + + +from mkdocs import plugins from mkdocs.commands import build from mkdocs.config import base from mkdocs.config import config_options as c @@ -39,7 +47,7 @@ class DummyPlugin(plugins.BasePlugin[_DummyPluginConfig]): class TestPluginClass(unittest.TestCase): - def test_valid_plugin_options(self): + def test_valid_plugin_options(self) -> None: test_dir = 'test' options = { @@ -51,20 +59,24 @@ class TestPluginClass(unittest.TestCase): cfg_fname = os.path.abspath(cfg_fname) cfg_dirname = os.path.dirname(cfg_fname) - expected = os.path.join(cfg_dirname, test_dir) - expected = { 'foo': 'some value', 'bar': 0, - 'dir': expected, + 'dir': os.path.join(cfg_dirname, test_dir), } plugin = DummyPlugin() errors, warnings = plugin.load_config(options, config_file_path=cfg_fname) - self.assertEqual(plugin.config, expected) self.assertEqual(errors, []) self.assertEqual(warnings, []) + assert_type(plugin.config, _DummyPluginConfig) + self.assertEqual(plugin.config, expected) + + assert_type(plugin.config.bar, int) + self.assertEqual(plugin.config.bar, 0) + assert_type(plugin.config.dir, Optional[str]) + def test_invalid_plugin_options(self): plugin = DummyPlugin() errors, warnings = plugin.load_config({'foo': 42}) @@ -285,160 +297,3 @@ class TestPluginCollection(unittest.TestCase): self.assertEqual(str(build_errors[2]), 'page content error') self.assertIs(build_errors[3].__class__, ValueError) self.assertEqual(str(build_errors[3]), 'post page error') - - -MockEntryPoint = mock.Mock() -MockEntryPoint.configure_mock(**{'name': 'sample', 'load.return_value': DummyPlugin}) - - -@mock.patch('mkdocs.plugins.entry_points', return_value=[MockEntryPoint]) -class TestPluginConfig(unittest.TestCase): - def test_plugin_config_without_options(self, mock_class): - cfg = {'plugins': ['sample']} - option = c.Plugins() - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertIn('sample', cfg['plugins']) - self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin) - expected = { - 'foo': 'default foo', - 'bar': 0, - 'dir': None, - } - self.assertEqual(cfg['plugins']['sample'].config, expected) - - def test_plugin_config_with_options(self, mock_class): - cfg = { - 'plugins': [ - { - 'sample': { - 'foo': 'foo value', - 'bar': 42, - }, - } - ], - } - option = c.Plugins() - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertIn('sample', cfg['plugins']) - self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin) - expected = { - 'foo': 'foo value', - 'bar': 42, - 'dir': None, - } - self.assertEqual(cfg['plugins']['sample'].config, expected) - - def test_plugin_config_as_dict(self, mock_class): - cfg = { - 'plugins': { - 'sample': { - 'foo': 'foo value', - 'bar': 42, - }, - }, - } - option = c.Plugins() - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertIn('sample', cfg['plugins']) - self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin) - expected = { - 'foo': 'foo value', - 'bar': 42, - 'dir': None, - } - self.assertEqual(cfg['plugins']['sample'].config, expected) - - def test_plugin_config_empty_list_with_empty_default(self, mock_class): - cfg = {'plugins': []} - option = c.Plugins(default=[]) - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertEqual(len(cfg['plugins']), 0) - - def test_plugin_config_empty_list_with_default(self, mock_class): - # Default is ignored - cfg = {'plugins': []} - option = c.Plugins(default=['sample']) - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertEqual(len(cfg['plugins']), 0) - - def test_plugin_config_none_with_empty_default(self, mock_class): - cfg = {'plugins': None} - option = c.Plugins(default=[]) - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertEqual(len(cfg['plugins']), 0) - - def test_plugin_config_none_with_default(self, mock_class): - # Default is used. - cfg = {'plugins': None} - option = c.Plugins(default=['sample']) - cfg['plugins'] = option.validate(cfg['plugins']) - - self.assertIsInstance(cfg['plugins'], plugins.PluginCollection) - self.assertIn('sample', cfg['plugins']) - self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin) - expected = { - 'foo': 'default foo', - 'bar': 0, - 'dir': None, - } - self.assertEqual(cfg['plugins']['sample'].config, expected) - - def test_plugin_config_uninstalled(self, mock_class): - cfg = {'plugins': ['uninstalled']} - option = c.Plugins() - with self.assertRaises(config.base.ValidationError): - option.validate(cfg['plugins']) - - def test_plugin_config_not_list(self, mock_class): - cfg = {'plugins': 'sample'} # should be a list - option = c.Plugins() - with self.assertRaises(config.base.ValidationError): - option.validate(cfg['plugins']) - - def test_plugin_config_multivalue_dict(self, mock_class): - cfg = { - 'plugins': [ - { - 'sample': { - 'foo': 'foo value', - 'bar': 42, - }, - 'extra_key': 'baz', - } - ], - } - option = c.Plugins() - with self.assertRaises(config.base.ValidationError): - option.validate(cfg['plugins']) - - def test_plugin_config_not_string_or_dict(self, mock_class): - cfg = { - 'plugins': [('not a string or dict',)], - } - option = c.Plugins() - with self.assertRaises(config.base.ValidationError): - option.validate(cfg['plugins']) - - def test_plugin_config_options_not_dict(self, mock_class): - cfg = { - 'plugins': [ - { - 'sample': 'not a dict', - } - ], - } - option = c.Plugins() - with self.assertRaises(config.base.ValidationError): - option.validate(cfg['plugins'])