mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +07:00
Add plugin events that persist across builds in mkdocs serve
"One-time events" `on_startup(command)`, `on_shutdown`. Their presence also shows that a plugin *wants* to persist across builds. Otherwise they will be re-created, to not change any existing behavior.
This commit is contained in:
@@ -157,11 +157,23 @@ There are three kinds of events: [Global Events], [Page Events] and
|
||||
</details>
|
||||
<br>
|
||||
|
||||
#### Global Events
|
||||
#### One-time Events
|
||||
|
||||
Global events are called once per build at either the beginning or end of the
|
||||
build process. Any changes made in these events will have a global effect on the
|
||||
entire site.
|
||||
One-time events run once per `mkdocs` invocation. The only case where these tangibly differ from [global events](#global-events) is for `mkdocs serve`: global events, unlike these, will run multiple times -- once per *build*.
|
||||
|
||||
##### on_startup
|
||||
|
||||
::: mkdocs.plugins.BasePlugin.on_startup
|
||||
options:
|
||||
show_root_heading: false
|
||||
show_root_toc_entry: false
|
||||
|
||||
##### on_shutdown
|
||||
|
||||
::: mkdocs.plugins.BasePlugin.on_shutdown
|
||||
options:
|
||||
show_root_heading: false
|
||||
show_root_toc_entry: false
|
||||
|
||||
##### on_serve
|
||||
|
||||
@@ -170,6 +182,12 @@ entire site.
|
||||
show_root_heading: false
|
||||
show_root_toc_entry: false
|
||||
|
||||
#### Global Events
|
||||
|
||||
Global events are called once per build at either the beginning or end of the
|
||||
build process. Any changes made in these events will have a global effect on the
|
||||
entire site.
|
||||
|
||||
##### on_config
|
||||
|
||||
::: mkdocs.plugins.BasePlugin.on_config
|
||||
|
||||
@@ -7,11 +7,11 @@ import re
|
||||
|
||||
from graphviz import Digraph
|
||||
|
||||
g = Digraph("MkDocs", format="svg")
|
||||
g.attr(compound="true", bgcolor="transparent")
|
||||
g.graph_attr.update(fontname="inherit", tooltip=" ")
|
||||
g.node_attr.update(fontname="inherit", tooltip=" ", style="filled")
|
||||
g.edge_attr.update(fontname="inherit", tooltip=" ")
|
||||
graph = Digraph("MkDocs", format="svg")
|
||||
graph.attr(compound="true", bgcolor="transparent")
|
||||
graph.graph_attr.update(fontname="inherit", tooltip=" ")
|
||||
graph.node_attr.update(fontname="inherit", tooltip=" ", style="filled")
|
||||
graph.edge_attr.update(fontname="inherit", tooltip=" ")
|
||||
|
||||
|
||||
def strip_suffix(name):
|
||||
@@ -60,7 +60,7 @@ def edge(g, a, b, dashed=False, **kwargs):
|
||||
|
||||
|
||||
def ensure_order(a, b):
|
||||
edge(g, a, b, style="invis")
|
||||
edge(graph, a, b, style="invis")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -79,7 +79,7 @@ def event(g, name, parameters):
|
||||
g, f"cluster_{name}", href=f"#{name}", bgcolor="#ffff3388", pencolor="#00000088"
|
||||
) as c:
|
||||
label = "|".join(f"<{p}>{p}" for p in parameters.split())
|
||||
node(c, name, shape="record", label=label, fillcolor="#ffffff55")
|
||||
node(c, name, shape="record" if parameters else "point", label=label, fillcolor="#ffffff55")
|
||||
|
||||
|
||||
def placeholder_cluster(g, name):
|
||||
@@ -87,72 +87,74 @@ def placeholder_cluster(g, name):
|
||||
node(c, f"placeholder_{name}", label="...", fillcolor="transparent", color="transparent")
|
||||
|
||||
|
||||
event(g, "on_config", "config")
|
||||
event(g, "on_pre_build", "config")
|
||||
event(g, "on_files", "files config")
|
||||
event(g, "on_nav", "nav config files")
|
||||
event(graph, "on_startup", "command")
|
||||
|
||||
edge(g, "load_config", "on_config:config")
|
||||
edge(g, "on_config:config", "on_pre_build:config")
|
||||
edge(g, "on_config:config", "get_files")
|
||||
edge(g, "get_files", "on_files:files")
|
||||
edge(g, "on_files:files", "get_nav")
|
||||
edge(g, "get_nav", "on_nav:nav")
|
||||
edge(g, "on_files:files", "on_nav:files")
|
||||
with cluster(graph, "cluster_build", bgcolor="#dddddd11") as g:
|
||||
event(g, "on_config", "config")
|
||||
event(g, "on_pre_build", "config")
|
||||
event(g, "on_files", "files config")
|
||||
event(g, "on_nav", "nav config files")
|
||||
|
||||
with cluster(g, "cluster_populate_page") as c:
|
||||
event(c, "on_pre_page", "page config files")
|
||||
event(c, "on_page_read_source", "page config")
|
||||
event(c, "on_page_markdown", "markdown page config files")
|
||||
event(c, "on_page_content", "html page config files")
|
||||
edge(g, "load_config", "on_config:config")
|
||||
edge(g, "on_config:config", "on_pre_build:config")
|
||||
edge(g, "on_config:config", "get_files")
|
||||
edge(g, "get_files", "on_files:files")
|
||||
edge(g, "on_files:files", "get_nav")
|
||||
edge(g, "get_nav", "on_nav:nav")
|
||||
edge(g, "on_files:files", "on_nav:files")
|
||||
|
||||
edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed")
|
||||
edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed")
|
||||
edge(c, "on_page_markdown:markdown", "render_p", style="dashed")
|
||||
edge(c, "render_p", "on_page_content:html", style="dashed")
|
||||
with cluster(g, "cluster_populate_page") as c:
|
||||
event(c, "on_pre_page", "page config files")
|
||||
event(c, "on_page_read_source", "page config")
|
||||
event(c, "on_page_markdown", "markdown page config files")
|
||||
event(c, "on_page_content", "html page config files")
|
||||
|
||||
edge(g, "on_nav:files", "pages_point_a", arrowhead="none")
|
||||
edge(g, "pages_point_a", "on_pre_page:page", style="dashed")
|
||||
edge(g, "pages_point_a", "cluster_populate_page")
|
||||
edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed")
|
||||
edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed")
|
||||
edge(c, "on_page_markdown:markdown", "render_p", style="dashed")
|
||||
edge(c, "render_p", "on_page_content:html", style="dashed")
|
||||
|
||||
for i in 2, 3:
|
||||
placeholder_cluster(g, f"cluster_populate_page_{i}")
|
||||
edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed")
|
||||
edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed")
|
||||
|
||||
|
||||
event(g, "on_env", "env config files")
|
||||
|
||||
edge(g, "on_page_content:html", "pages_point_b", style="dashed")
|
||||
edge(g, "pages_point_b", "on_env:files")
|
||||
|
||||
|
||||
edge(g, "pages_point_b", "pages_point_c", arrowhead="none")
|
||||
edge(g, "pages_point_c", "on_page_context:page", style="dashed")
|
||||
|
||||
with cluster(g, "cluster_build_page") as c:
|
||||
event(c, "on_page_context", "context page config nav")
|
||||
event(c, "on_post_page", "output page config")
|
||||
|
||||
edge(c, "get_context", "on_page_context:context")
|
||||
edge(c, "on_page_context:context", "render")
|
||||
edge(c, "get_template", "render")
|
||||
edge(c, "render", "on_post_page:output")
|
||||
edge(c, "on_post_page:output", "write_file")
|
||||
|
||||
|
||||
edge(g, "on_nav:nav", "cluster_build_page")
|
||||
edge(g, "on_env:env", "cluster_build_page")
|
||||
|
||||
for i in 2, 3:
|
||||
placeholder_cluster(g, f"cluster_build_page_{i}")
|
||||
edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed")
|
||||
|
||||
|
||||
event(g, "on_post_build", "config")
|
||||
event(g, "on_serve", "server config")
|
||||
edge(g, "on_nav:files", "pages_point_a", arrowhead="none")
|
||||
edge(g, "pages_point_a", "on_pre_page:page", style="dashed")
|
||||
edge(g, "pages_point_a", "cluster_populate_page")
|
||||
|
||||
for i in 2, 3:
|
||||
placeholder_cluster(g, f"cluster_populate_page_{i}")
|
||||
edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed")
|
||||
edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed")
|
||||
|
||||
event(g, "on_env", "env config files")
|
||||
|
||||
edge(g, "on_page_content:html", "pages_point_b", style="dashed")
|
||||
edge(g, "pages_point_b", "on_env:files")
|
||||
|
||||
edge(g, "pages_point_b", "pages_point_c", arrowhead="none")
|
||||
edge(g, "pages_point_c", "on_page_context:page", style="dashed")
|
||||
|
||||
with cluster(g, "cluster_build_page") as c:
|
||||
event(c, "on_page_context", "context page config nav")
|
||||
event(c, "on_post_page", "output page config")
|
||||
|
||||
edge(c, "get_context", "on_page_context:context")
|
||||
edge(c, "on_page_context:context", "render")
|
||||
edge(c, "get_template", "render")
|
||||
edge(c, "render", "on_post_page:output")
|
||||
edge(c, "on_post_page:output", "write_file")
|
||||
|
||||
edge(g, "on_nav:nav", "cluster_build_page")
|
||||
edge(g, "on_env:env", "cluster_build_page")
|
||||
|
||||
for i in 2, 3:
|
||||
placeholder_cluster(g, f"cluster_build_page_{i}")
|
||||
edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed")
|
||||
|
||||
event(g, "on_post_build", "config")
|
||||
|
||||
event(graph, "on_serve", "server config")
|
||||
event(graph, "on_shutdown", "")
|
||||
|
||||
|
||||
ensure_order("on_startup", "cluster_build")
|
||||
ensure_order("on_pre_build", "on_files")
|
||||
ensure_order("on_nav", "cluster_populate_page")
|
||||
ensure_order("cluster_populate_page_2", "cluster_populate_page_3")
|
||||
@@ -161,8 +163,9 @@ ensure_order("pages_point_c", "cluster_build_page")
|
||||
ensure_order("cluster_build_page_2", "cluster_build_page_3")
|
||||
ensure_order("cluster_build_page", "on_post_build")
|
||||
ensure_order("on_post_build", "on_serve")
|
||||
ensure_order("on_serve", "on_shutdown")
|
||||
|
||||
|
||||
data = g.pipe()
|
||||
data = graph.pipe()
|
||||
data = data[data.index(b"<svg ") :]
|
||||
pathlib.Path(__file__).with_suffix(".svg").write_bytes(data)
|
||||
|
||||
645
docs/img/plugin-events.svg
generated
645
docs/img/plugin-events.svg
generated
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
@@ -240,7 +240,12 @@ def build_command(clean, **kwargs):
|
||||
from mkdocs.commands import build
|
||||
|
||||
_enable_warnings()
|
||||
build.build(config.load_config(**kwargs), dirty=not clean)
|
||||
cfg = config.load_config(**kwargs)
|
||||
cfg['plugins'].run_event('startup', command='build')
|
||||
try:
|
||||
build.build(cfg, dirty=not clean)
|
||||
finally:
|
||||
cfg['plugins'].run_event('shutdown')
|
||||
|
||||
|
||||
@cli.command(name="gh-deploy")
|
||||
@@ -263,7 +268,11 @@ def gh_deploy_command(
|
||||
|
||||
_enable_warnings()
|
||||
cfg = config.load_config(remote_branch=remote_branch, remote_name=remote_name, **kwargs)
|
||||
build.build(cfg, dirty=not clean)
|
||||
cfg['plugins'].run_event('startup', command='gh-deploy')
|
||||
try:
|
||||
build.build(cfg, dirty=not clean)
|
||||
finally:
|
||||
cfg['plugins'].run_event('shutdown')
|
||||
gh_deploy.gh_deploy(
|
||||
cfg,
|
||||
message=message,
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import gzip
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Sequence, Set, Union
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -265,8 +266,6 @@ def build(config: Config, live_server: bool = False, dirty: bool = False) -> Non
|
||||
logging.getLogger('mkdocs').addHandler(warning_counter)
|
||||
|
||||
try:
|
||||
import time
|
||||
|
||||
start = time.monotonic()
|
||||
|
||||
# Run `config` plugin events.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -42,17 +43,21 @@ def serve(
|
||||
def mount_path(config):
|
||||
return urlsplit(config['site_url'] or '/').path
|
||||
|
||||
def builder():
|
||||
get_config = functools.partial(
|
||||
load_config,
|
||||
config_file=config_file,
|
||||
dev_addr=dev_addr,
|
||||
strict=strict,
|
||||
theme=theme,
|
||||
theme_dir=theme_dir,
|
||||
site_dir=site_dir,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def builder(config=None):
|
||||
log.info("Building documentation...")
|
||||
config = load_config(
|
||||
config_file=config_file,
|
||||
dev_addr=dev_addr,
|
||||
strict=strict,
|
||||
theme=theme,
|
||||
theme_dir=theme_dir,
|
||||
site_dir=site_dir,
|
||||
**kwargs,
|
||||
)
|
||||
if config is None:
|
||||
config = get_config()
|
||||
|
||||
# combine CLI watch arguments with config file values
|
||||
if config["watch"] is None:
|
||||
@@ -66,11 +71,13 @@ def serve(
|
||||
live_server = livereload in ['dirty', 'livereload']
|
||||
dirty = livereload == 'dirty'
|
||||
build(config, live_server=live_server, dirty=dirty)
|
||||
return config
|
||||
|
||||
config = get_config()
|
||||
config['plugins'].run_event('startup', command='serve')
|
||||
|
||||
try:
|
||||
# Perform the initial build
|
||||
config = builder()
|
||||
builder(config)
|
||||
|
||||
host, port = config['dev_addr']
|
||||
server = LiveReloadServer(
|
||||
@@ -114,5 +121,6 @@ def serve(
|
||||
# Avoid ugly, unhelpful traceback
|
||||
raise Abort(f'{type(e).__name__}: {e}')
|
||||
finally:
|
||||
config['plugins'].run_event('shutdown')
|
||||
if isdir(site_dir):
|
||||
shutil.rmtree(site_dir)
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
import traceback
|
||||
import typing as t
|
||||
from typing import NamedTuple
|
||||
from typing import Dict, NamedTuple
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
import markdown
|
||||
@@ -705,6 +705,7 @@ class Plugins(OptionallyRequired):
|
||||
super().__init__(**kwargs)
|
||||
self.installed_plugins = plugins.get_plugins()
|
||||
self.config_file_path = None
|
||||
self.plugin_cache: Dict[str, plugins.BasePlugin] = {}
|
||||
|
||||
def pre_validation(self, config, key_name):
|
||||
self.config_file_path = config.config_file_path
|
||||
@@ -738,15 +739,22 @@ class Plugins(OptionallyRequired):
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError(f"Invalid config options for the '{name}' plugin.")
|
||||
|
||||
Plugin = self.installed_plugins[name].load()
|
||||
try:
|
||||
plugin = self.plugin_cache[name]
|
||||
except KeyError:
|
||||
Plugin = self.installed_plugins[name].load()
|
||||
|
||||
if not issubclass(Plugin, plugins.BasePlugin):
|
||||
raise ValidationError(
|
||||
f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of'
|
||||
f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}'
|
||||
)
|
||||
if not issubclass(Plugin, plugins.BasePlugin):
|
||||
raise ValidationError(
|
||||
f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of'
|
||||
f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}'
|
||||
)
|
||||
|
||||
plugin = Plugin()
|
||||
|
||||
if hasattr(plugin, 'on_startup') or hasattr(plugin, 'on_shutdown'):
|
||||
self.plugin_cache[name] = plugin
|
||||
|
||||
plugin = Plugin()
|
||||
errors, warnings = plugin.load_config(config, self.config_file_path)
|
||||
self.warnings.extend(warnings)
|
||||
errors_message = '\n'.join(f"Plugin '{name}' value: '{x}'. Error: {y}" for x, y in errors)
|
||||
|
||||
@@ -171,7 +171,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
|
||||
self._visible_epoch = self._wanted_epoch
|
||||
self._epoch_cond.notify_all()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
def shutdown(self, wait=False) -> None:
|
||||
self.observer.stop()
|
||||
with self._rebuild_cond:
|
||||
self._shutdown = True
|
||||
@@ -179,6 +179,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
|
||||
|
||||
if self.serve_thread.is_alive():
|
||||
super().shutdown()
|
||||
if wait:
|
||||
self.serve_thread.join()
|
||||
self.observer.join()
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
from importlib_metadata import EntryPoint, entry_points
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
import jinja2.environment
|
||||
|
||||
from mkdocs.config.base import Config, ConfigErrors, ConfigWarnings, PlainConfigSchema
|
||||
@@ -61,16 +66,48 @@ class BasePlugin:
|
||||
|
||||
return self.config.validate()
|
||||
|
||||
# (Note that event implementations shouldn't actually be static methods in subclasses)
|
||||
# One-time events
|
||||
|
||||
# Global events
|
||||
def on_startup(self, command: Literal['build', 'gh-deploy', 'serve']) -> None:
|
||||
"""
|
||||
The `startup` event runs once at the very beginning of an `mkdocs` invocation.
|
||||
|
||||
New in MkDocs 1.4.
|
||||
|
||||
The presence of an `on_startup` method (even if empty) migrates the plugin to the new
|
||||
system where the plugin object is kept across builds within one `mkdocs serve`.
|
||||
|
||||
Note that for initializing variables, the `__init__` method is still preferred.
|
||||
For initializing per-build variables (and whenever in doubt), use the `on_config` event.
|
||||
|
||||
Parameters:
|
||||
command: the command that MkDocs was invoked with, e.g. "serve" for `mkdocs serve`.
|
||||
"""
|
||||
|
||||
def on_shutdown(self) -> None:
|
||||
"""
|
||||
The `shutdown` event runs once at the very end of an `mkdocs` invocation, before exiting.
|
||||
|
||||
This event is relevant only for support of `mkdocs serve`, otherwise within a
|
||||
single build it's undistinguishable from `on_post_build`.
|
||||
|
||||
New in MkDocs 1.4.
|
||||
|
||||
The presence of an `on_shutdown` method (even if empty) migrates the plugin to the new
|
||||
system where the plugin object is kept across builds within one `mkdocs serve`.
|
||||
|
||||
Note the `on_post_build` method is still preferred for cleanups, when possible, as it has
|
||||
a much higher chance of actually triggering. `on_shutdown` is "best effort" because it
|
||||
relies on detecting a graceful shutdown of MkDocs.
|
||||
"""
|
||||
|
||||
def on_serve(
|
||||
self, server: LiveReloadServer, config: Config, builder: Callable
|
||||
) -> Optional[LiveReloadServer]:
|
||||
"""
|
||||
The `serve` event is only called when the `serve` command is used during
|
||||
development. It is passed the `Server` instance which can be modified before
|
||||
development. It runs only once, after the first build finishes.
|
||||
It is passed the `Server` instance which can be modified before
|
||||
it is activated. For example, additional files or directories could be added
|
||||
to the list of "watched" files for auto-reloading.
|
||||
|
||||
@@ -84,6 +121,8 @@ class BasePlugin:
|
||||
"""
|
||||
return server
|
||||
|
||||
# Global events
|
||||
|
||||
def on_config(self, config: Config) -> Optional[Config]:
|
||||
"""
|
||||
The `config` event is the first event called on build and is run immediately
|
||||
@@ -395,7 +434,10 @@ class PluginCollection(OrderedDict):
|
||||
be modified by the event method.
|
||||
"""
|
||||
pass_item = item is not None
|
||||
for method in self.events[name]:
|
||||
events = self.events[name]
|
||||
if events:
|
||||
log.debug(f'Running {len(events)} `{name}` events')
|
||||
for method in events:
|
||||
if pass_item:
|
||||
result = method(item, **kwargs)
|
||||
else:
|
||||
|
||||
@@ -97,6 +97,8 @@ class TestPluginCollection(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
collection.events,
|
||||
{
|
||||
'startup': [],
|
||||
'shutdown': [],
|
||||
'serve': [],
|
||||
'config': [],
|
||||
'pre_build': [plugin.on_pre_build],
|
||||
|
||||
@@ -7,6 +7,7 @@ watchdog==2.0.0
|
||||
ghp-import==1.0
|
||||
pyyaml_env_tag==0.1
|
||||
importlib_metadata==4.3; python_version < "3.10"
|
||||
typing_extensions==3.10; python_version < "3.8"
|
||||
packaging==20.5
|
||||
mergedeep==1.3.4
|
||||
babel==2.9.0
|
||||
|
||||
@@ -7,6 +7,7 @@ watchdog>=2.0.0
|
||||
ghp-import>=1.0
|
||||
pyyaml_env_tag>=0.1
|
||||
importlib_metadata>=4.3; python_version < "3.10"
|
||||
typing_extensions>=3.10; python_version < "3.8"
|
||||
packaging>=20.5
|
||||
mergedeep>=1.3.4
|
||||
babel>=2.9.0
|
||||
|
||||
Reference in New Issue
Block a user