Skip to content

Commit 4154a94

Browse files
committed
Added experimental group plugin
1 parent b36af18 commit 4154a94

File tree

7 files changed

+367
-0
lines changed

7 files changed

+367
-0
lines changed

material/plugins/group/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.

material/plugins/group/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.
20+
21+
from __future__ import annotations
22+
23+
from mkdocs.config.config_options import Type
24+
from mkdocs.config.base import Config
25+
26+
# -----------------------------------------------------------------------------
27+
# Classes
28+
# -----------------------------------------------------------------------------
29+
30+
# Group plugin configuration
31+
class GroupConfig(Config):
32+
enabled = Type(bool, default = False)
33+
plugins = Type(list | dict)

material/plugins/group/plugin.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.
20+
21+
import logging
22+
23+
from collections.abc import Callable
24+
from mkdocs.config.config_options import Plugins
25+
from mkdocs.config.defaults import MkDocsConfig
26+
from mkdocs.plugins import BasePlugin, event_priority
27+
28+
from .config import GroupConfig
29+
30+
# -----------------------------------------------------------------------------
31+
# Classes
32+
# -----------------------------------------------------------------------------
33+
34+
# Group plugin
35+
class GroupPlugin(BasePlugin[GroupConfig]):
36+
supports_multiple_instances = True
37+
38+
# Determine whether we're serving the site
39+
def on_startup(self, *, command, dirty):
40+
self.is_serve = command == "serve"
41+
self.is_dirty = dirty
42+
43+
# If the group is enabled, conditionally load plugins - at first, this might
44+
# sound easier than it actually is, as we need to jump through some hoops to
45+
# ensure correct ordering among plugins. We're effectively initializing the
46+
# plugins that are part of the group after all MkDocs finished initializing
47+
# all other plugins, so we need to patch the order of the methods. Moreover,
48+
# we must use MkDocs existing plugin collection, or we might have collisions
49+
# with other plugins that are not part of the group. As so often, this is a
50+
# little hacky, but has huge potential making plugin configuration easier.
51+
# There's one little caveat: the `__init__` and `on_startup` methods of the
52+
# plugins that are part of the group are called after all other plugins, so
53+
# the `event_priority` decorator for `on_startup` events and is effectively
54+
# useless. However, the `on_startup` event is only intended to set up the
55+
# plugin and doesn't receive anything else than the invoked command and
56+
# whether we're running a dirty build, so there should be no problems.
57+
@event_priority(150)
58+
def on_config(self, config):
59+
if not self.config.enabled:
60+
return
61+
62+
# Retrieve plugin collection from configuration
63+
option: Plugins = dict(config._schema)["plugins"]
64+
assert isinstance(option, Plugins)
65+
66+
# Load all plugins in group
67+
self.plugins: dict[str, BasePlugin] = {}
68+
for name, plugin in self._load(option):
69+
self.plugins[name] = plugin
70+
71+
# Patch order of plugin methods
72+
for events in option.plugins.events.values():
73+
self._patch(events, config)
74+
75+
# Invoke `on_startup` event for plugins in group
76+
command = "serve" if self.is_serve else "build"
77+
for method in option.plugins.events["startup"]:
78+
if method.__self__ in self.plugins.values():
79+
method(command = command, dirty = self.is_dirty)
80+
81+
# -------------------------------------------------------------------------
82+
83+
# Retrieve priority of plugin method
84+
def _get_priority(self, method: Callable):
85+
return getattr(method, "mkdocs_priority", 0)
86+
87+
# Retrieve position of plugin
88+
def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int:
89+
for at, (_, candidate) in enumerate(config.plugins.items()):
90+
if plugin == candidate:
91+
return at
92+
93+
# -------------------------------------------------------------------------
94+
95+
# Load plugins that are part of the group
96+
def _load(self, option: Plugins):
97+
for name, data in option._parse_configs(self.config.plugins):
98+
yield option.load_plugin_with_namespace(name, data)
99+
100+
# -------------------------------------------------------------------------
101+
102+
# Patch order of plugin events - all other plugin methods are already in the
103+
# right order, so we only need to check those that are part of the group and
104+
# bubble them up into the right location. Some plugin methods may define
105+
# priorities, so we need to make sure to order correctly within those.
106+
def _patch(self, methods: list[Callable], config: MkDocsConfig):
107+
position = self._get_position(self, config)
108+
for at in reversed(range(1, len(methods))):
109+
tail = methods[at - 1]
110+
head = methods[at]
111+
112+
# Skip if the plugin is not part of the group
113+
if not head.__self__ in self.plugins.values():
114+
continue
115+
116+
# Skip if the previous method has a higher priority than the current
117+
# one, because we know we can't swap them anyway
118+
if self._get_priority(tail) > self._get_priority(head):
119+
continue
120+
121+
# Both methods have the same priority, so we check if the ordering
122+
# of both methods is violated, and if it is, swap them
123+
if (position < self._get_position(tail.__self__, config)):
124+
methods[at], methods[at - 1] = tail, head
125+
126+
# -----------------------------------------------------------------------------
127+
# Data
128+
# -----------------------------------------------------------------------------
129+
130+
# Set up logging
131+
log = logging.getLogger("mkdocs.material.group")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Funding = "https://github.com/sponsors/squidfunk"
5858

5959
[project.entry-points."mkdocs.plugins"]
6060
"material/blog" = "material.plugins.blog.plugin:BlogPlugin"
61+
"material/group" = "material.plugins.group.plugin:GroupPlugin"
6162
"material/info" = "material.plugins.info.plugin:InfoPlugin"
6263
"material/offline" = "material.plugins.offline.plugin:OfflinePlugin"
6364
"material/search" = "material.plugins.search.plugin:SearchPlugin"

src/plugins/group/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.

src/plugins/group/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.
20+
21+
from __future__ import annotations
22+
23+
from mkdocs.config.config_options import Type
24+
from mkdocs.config.base import Config
25+
26+
# -----------------------------------------------------------------------------
27+
# Classes
28+
# -----------------------------------------------------------------------------
29+
30+
# Group plugin configuration
31+
class GroupConfig(Config):
32+
enabled = Type(bool, default = False)
33+
plugins = Type(list | dict)

src/plugins/group/plugin.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright (c) 2016-2023 Martin Donath <[email protected]>
2+
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.
20+
21+
import logging
22+
23+
from collections.abc import Callable
24+
from mkdocs.config.config_options import Plugins
25+
from mkdocs.config.defaults import MkDocsConfig
26+
from mkdocs.plugins import BasePlugin, event_priority
27+
28+
from .config import GroupConfig
29+
30+
# -----------------------------------------------------------------------------
31+
# Classes
32+
# -----------------------------------------------------------------------------
33+
34+
# Group plugin
35+
class GroupPlugin(BasePlugin[GroupConfig]):
36+
supports_multiple_instances = True
37+
38+
# Determine whether we're serving the site
39+
def on_startup(self, *, command, dirty):
40+
self.is_serve = command == "serve"
41+
self.is_dirty = dirty
42+
43+
# If the group is enabled, conditionally load plugins - at first, this might
44+
# sound easier than it actually is, as we need to jump through some hoops to
45+
# ensure correct ordering among plugins. We're effectively initializing the
46+
# plugins that are part of the group after all MkDocs finished initializing
47+
# all other plugins, so we need to patch the order of the methods. Moreover,
48+
# we must use MkDocs existing plugin collection, or we might have collisions
49+
# with other plugins that are not part of the group. As so often, this is a
50+
# little hacky, but has huge potential making plugin configuration easier.
51+
# There's one little caveat: the `__init__` and `on_startup` methods of the
52+
# plugins that are part of the group are called after all other plugins, so
53+
# the `event_priority` decorator for `on_startup` events and is effectively
54+
# useless. However, the `on_startup` event is only intended to set up the
55+
# plugin and doesn't receive anything else than the invoked command and
56+
# whether we're running a dirty build, so there should be no problems.
57+
@event_priority(150)
58+
def on_config(self, config):
59+
if not self.config.enabled:
60+
return
61+
62+
# Retrieve plugin collection from configuration
63+
option: Plugins = dict(config._schema)["plugins"]
64+
assert isinstance(option, Plugins)
65+
66+
# Load all plugins in group
67+
self.plugins: dict[str, BasePlugin] = {}
68+
for name, plugin in self._load(option):
69+
self.plugins[name] = plugin
70+
71+
# Patch order of plugin methods
72+
for events in option.plugins.events.values():
73+
self._patch(events, config)
74+
75+
# Invoke `on_startup` event for plugins in group
76+
command = "serve" if self.is_serve else "build"
77+
for method in option.plugins.events["startup"]:
78+
if method.__self__ in self.plugins.values():
79+
method(command = command, dirty = self.is_dirty)
80+
81+
# -------------------------------------------------------------------------
82+
83+
# Retrieve priority of plugin method
84+
def _get_priority(self, method: Callable):
85+
return getattr(method, "mkdocs_priority", 0)
86+
87+
# Retrieve position of plugin
88+
def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int:
89+
for at, (_, candidate) in enumerate(config.plugins.items()):
90+
if plugin == candidate:
91+
return at
92+
93+
# -------------------------------------------------------------------------
94+
95+
# Load plugins that are part of the group
96+
def _load(self, option: Plugins):
97+
for name, data in option._parse_configs(self.config.plugins):
98+
yield option.load_plugin_with_namespace(name, data)
99+
100+
# -------------------------------------------------------------------------
101+
102+
# Patch order of plugin events - all other plugin methods are already in the
103+
# right order, so we only need to check those that are part of the group and
104+
# bubble them up into the right location. Some plugin methods may define
105+
# priorities, so we need to make sure to order correctly within those.
106+
def _patch(self, methods: list[Callable], config: MkDocsConfig):
107+
position = self._get_position(self, config)
108+
for at in reversed(range(1, len(methods))):
109+
tail = methods[at - 1]
110+
head = methods[at]
111+
112+
# Skip if the plugin is not part of the group
113+
if not head.__self__ in self.plugins.values():
114+
continue
115+
116+
# Skip if the previous method has a higher priority than the current
117+
# one, because we know we can't swap them anyway
118+
if self._get_priority(tail) > self._get_priority(head):
119+
continue
120+
121+
# Both methods have the same priority, so we check if the ordering
122+
# of both methods is violated, and if it is, swap them
123+
if (position < self._get_position(tail.__self__, config)):
124+
methods[at], methods[at - 1] = tail, head
125+
126+
# -----------------------------------------------------------------------------
127+
# Data
128+
# -----------------------------------------------------------------------------
129+
130+
# Set up logging
131+
log = logging.getLogger("mkdocs.material.group")

0 commit comments

Comments
 (0)