Let plugins choose the priority order of their events (#2973)

Add a decorator that sets a priority value for plugins' events. Events with higher priority are called first. Events without a chosen priority get a default of 0. Events that have the same priority are ordered as they appear in the config.
This commit is contained in:
Oleh Prypin
2022-09-14 17:33:06 +02:00
committed by GitHub
parent 283c36250d
commit 62d8a6f44d
6 changed files with 131 additions and 2 deletions

View File

@@ -60,6 +60,11 @@ body.homepage>div.container>div.row>div.col-md-9 {
/* mkdocstrings */
.doc-object {
padding-left: 10px;
border-left: 4px solid rgba(230, 230, 230);
}
.doc-contents .field-body p:first-of-type {
display: inline;
}

View File

@@ -295,6 +295,16 @@ page events are called after the [post_template] event and before the
show_root_heading: false
show_root_toc_entry: false
### Event Priorities
For each event type, corresponding methods of plugins are called in the order that the plugins appear in the `plugins` [config][].
Since MkDocs 1.4, plugins can choose to set a priority value for their events. Events with higher priority are called first. Events without a chosen priority get a default of 0. Events that have the same priority are ordered as they appear in the config.
::: mkdocs.plugins.event_priority
options:
show_root_heading: true
### Handling Errors
MkDocs defines four error types:

View File

@@ -16,6 +16,7 @@ else:
import jinja2.environment
from mkdocs import utils
from mkdocs.config.base import Config, ConfigErrors, ConfigWarnings, PlainConfigSchema
from mkdocs.livereload import LiveReloadServer
from mkdocs.structure.files import Files
@@ -346,6 +347,37 @@ for k in EVENTS:
T = TypeVar('T')
def event_priority(priority: float) -> Callable[[T], T]:
"""A decorator to set an event priority for an event handler method.
Recommended priority values:
`100` "first", `50` "early", `0` "default", `-50` "late", `-100` "last".
As different plugins discover more precise relations to each other, the values should be further tweaked.
```python
@plugins.event_priority(-100) # Wishing to run this after all other plugins' `on_files` events.
def on_files(self, files, config, **kwargs):
...
```
New in MkDocs 1.4.
Recommended shim for backwards compatibility:
```python
try:
from mkdocs.plugins import event_priority
except ImportError:
event_priority = lambda priority: lambda f: f # No-op fallback
```
"""
def decorator(event_method):
event_method.mkdocs_priority = priority
return event_method
return decorator
class PluginCollection(OrderedDict):
"""
A collection of plugins.
@@ -361,7 +393,9 @@ class PluginCollection(OrderedDict):
def _register_event(self, event_name: str, method: Callable) -> None:
"""Register a method for an event."""
self.events[event_name].append(method)
utils.insort(
self.events[event_name], method, key=lambda m: -getattr(m, 'mkdocs_priority', 0)
)
def __setitem__(self, key: str, value: BasePlugin, **kwargs) -> None:
if not isinstance(value, BasePlugin):

View File

@@ -117,6 +117,45 @@ class TestPluginCollection(unittest.TestCase):
},
)
def test_event_priorities(self):
class PrioPlugin(plugins.BasePlugin):
config_scheme = base.get_schema(_DummyPluginConfig)
@plugins.event_priority(100)
def on_pre_page(self, content, **kwargs):
pass
@plugins.event_priority(-100)
def on_nav(self, item, **kwargs):
pass
def on_page_read_source(self, **kwargs):
pass
@plugins.event_priority(-50)
def on_post_build(self, **kwargs):
pass
collection = plugins.PluginCollection()
collection['dummy'] = dummy = DummyPlugin()
collection['prio'] = prio = PrioPlugin()
self.assertEqual(
collection.events['pre_page'],
[prio.on_pre_page, dummy.on_pre_page],
)
self.assertEqual(
collection.events['nav'],
[dummy.on_nav, prio.on_nav],
)
self.assertEqual(
collection.events['page_read_source'],
[dummy.on_page_read_source, prio.on_page_read_source],
)
self.assertEqual(
collection.events['post_build'],
[prio.on_post_build],
)
def test_set_plugin_on_collection(self):
collection = plugins.PluginCollection()
plugin = DummyPlugin()

View File

@@ -285,6 +285,22 @@ class UtilsTests(unittest.TestCase):
[1, 2, 3, 4, 5, 6, 7, 8],
)
def test_insort(self):
a = [1, 2, 3]
utils.insort(a, 5)
self.assertEqual(a, [1, 2, 3, 5])
utils.insort(a, -1)
self.assertEqual(a, [-1, 1, 2, 3, 5])
utils.insort(a, 2)
self.assertEqual(a, [-1, 1, 2, 2, 3, 5])
utils.insort(a, 4)
self.assertEqual(a, [-1, 1, 2, 2, 3, 4, 5])
def test_insort_key(self):
a = [(1, 'a'), (1, 'b'), (2, 'c')]
utils.insort(a, (1, 'a'), key=lambda v: v[0])
self.assertEqual(a, [(1, 'a'), (1, 'b'), (1, 'a'), (2, 'c')])
def test_get_themes(self):
themes = utils.get_theme_names()
self.assertIn('mkdocs', themes)

View File

@@ -17,7 +17,18 @@ import warnings
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import PurePath
from typing import IO, TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
from typing import (
IO,
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
MutableSequence,
Optional,
Tuple,
TypeVar,
)
from urllib.parse import urlsplit
if sys.version_info >= (3, 10):
@@ -34,6 +45,8 @@ from mkdocs import exceptions
if TYPE_CHECKING:
from mkdocs.structure.pages import Page
T = TypeVar('T')
log = logging.getLogger(__name__)
markdown_extensions = (
@@ -132,6 +145,18 @@ def reduce_list(data_set: Iterable[str]) -> List[str]:
return list(dict.fromkeys(data_set))
if sys.version_info >= (3, 10):
from bisect import insort
else:
def insort(a: MutableSequence[T], x: T, *, key=lambda v: v) -> None:
kx = key(x)
i = len(a)
while i > 0 and kx < key(a[i - 1]):
i -= 1
a.insert(i, x)
def copy_file(source_path: str, output_path: str) -> None:
"""
Copy source_path to output_path, making sure any parent directories exist.