Repository: mkdocs/mkdocs
Pull request #2931 (2022-08-14 16:34:15)

Refactor plugin events, move docs to source code, type annotations
- Move plugin events docs into source code + refactor
    * Create real (no-op) methods for each event in the base class.
    * Refactor event dispatcher to not check for methods' existence, instead just call them.
    * Move documentation from Markdown into docstrings of these methods.
    * Activate the 'mkdocstrings' plugin.
    * Use 'mkdocstrings' to insert documentation from those docstrings into the site.
- Add type annotations for plugin events, include them in the docs
- Drop py36 integration tests because mkdocstrings is >=3.7


Patch:
diff --git a/docs/css/extra.css b/docs/css/extra.css
index 3dce20dd..3a015280 100644
--- a/docs/css/extra.css
+++ b/docs/css/extra.css
@@ -30,6 +30,10 @@ dd {
     padding-left: 20px;
 }
 
+.doc-contents .field-body p:first-of-type {
+    display: inline;
+}
+
 .card-body svg {
     width: 100%;
     padding: 0 50px;
diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md
index 5ee045b1..e7952b32 100644
--- a/docs/dev-guide/plugins.md
+++ b/docs/dev-guide/plugins.md
@@ -165,98 +165,35 @@ entire site.
 
 ##### on_serve
 
-*   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
-    it is activated. For example, additional files or directories could be added
-    to the list of "watched" files for auto-reloading.
-
-    Parameters:
-    : __server:__ `livereload.Server` instance
-    : __config:__ global configuration object
-    : __builder:__ a callable which gets passed to each call to `server.watch`
-
-    Returns:
-    : `livereload.Server` instance
+::: mkdocs.plugins.BasePlugin.on_serve
 
 ##### on_config
 
-*   The `config` event is the first event called on build and is run immediately
-    after the user configuration is loaded and validated. Any alterations to the
-    config should be made here.
-
-    Parameters:
-    : __config:__ global configuration object
-
-    Returns:
-    : global configuration object
+::: mkdocs.plugins.BasePlugin.on_config
 
 ##### on_pre_build
 
-*   The `pre_build` event does not alter any variables. Use this event to call
-    pre-build scripts.
-
-    Parameters:
-    : __config:__ global configuration object
+::: mkdocs.plugins.BasePlugin.on_pre_build
 
 ##### on_files
 
-*   The `files` event is called after the files collection is populated from the
-    `docs_dir`. Use this event to add, remove, or alter files in the
-    collection. Note that Page objects have not yet been associated with the
-    file objects in the collection. Use [Page Events] to manipulate page
-    specific data.
-
-    Parameters:
-    : __files:__ global files collection
-    : __config:__ global configuration object
-
-    Returns:
-    : global files collection
+::: mkdocs.plugins.BasePlugin.on_files
 
 ##### on_nav
 
-*   The `nav` event is called after the site navigation is created and can
-    be used to alter the site navigation.
-
-    Parameters:
-    : __nav:__ global navigation object
-    : __config:__ global configuration object
-    : __files:__ global files collection
-
-    Returns:
-    : global navigation object
+::: mkdocs.plugins.BasePlugin.on_nav
 
 ##### on_env
 
-*   The `env` event is called after the Jinja template environment is created
-    and can be used to alter the [Jinja environment](https://jinja.palletsprojects.com/en/latest/api/#jinja2.Environment).
-
-    Parameters:
-    : __env:__ global Jinja environment
-    : __config:__ global configuration object
-    : __files:__ global files collection
-
-    Returns:
-    : global Jinja Environment
+::: mkdocs.plugins.BasePlugin.on_env
 
 ##### on_post_build
 
-*   The `post_build` event does not alter any variables. Use this event to call
-    post-build scripts.
-
-    Parameters:
-    : __config:__ global configuration object
+::: mkdocs.plugins.BasePlugin.on_post_build
 
 ##### on_build_error
 
-*   The `build_error` event is called after an exception of any kind
-    is caught by MkDocs during the build process.
-    Use this event to clean things up before MkDocs terminates. Note that any other
-    events which were scheduled to run after the error will have been skipped. See
-    [Handling Errors] for more details.
-
-    Parameters:
-    : __error:__ exception raised
+::: mkdocs.plugins.BasePlugin.on_build_error
 
 #### Template Events
 
@@ -267,45 +204,15 @@ called after the [env] event and before any [page events].
 
 ##### on_pre_template
 
-*   The `pre_template` event is called immediately after the subject template is
-    loaded and can be used to alter the template.
-
-    Parameters:
-    : __template__: a Jinja2 [Template] object
-    : __template_name__: string filename of template
-    : __config:__ global configuration object
-
-    Returns:
-    : a Jinja2 [Template] object
+::: mkdocs.plugins.BasePlugin.on_pre_template
 
 ##### on_template_context
 
-*   The `template_context` event is called immediately after the context is created
-    for the subject template and can be used to alter the context for that specific
-    template only.
-
-    Parameters:
-    : __context__: dict of template context variables
-    : __template_name__: string filename of template
-    : __config:__ global configuration object
-
-    Returns:
-    : dict of template context variables
+::: mkdocs.plugins.BasePlugin.on_template_context
 
 ##### on_post_template
 
-*   The `post_template` event is called after the template is rendered, but before
-    it is written to disc and can be used to alter the output of the template.
-    If an empty string is returned, the template is skipped and nothing is is
-    written to disc.
-
-    Parameters:
-    : __output_content__: output of rendered template as string
-    : __template_name__: string filename of template
-    : __config:__ global configuration object
-
-    Returns:
-    : output of rendered template as string
+::: mkdocs.plugins.BasePlugin.on_post_template
 
 #### Page Events
 
@@ -315,88 +222,27 @@ page events are called after the [post_template] event and before the
 
 ##### on_pre_page
 
-*   The `pre_page` event is called before any actions are taken on the subject
-    page and can be used to alter the `Page` instance.
-
-    Parameters:
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-    : __files:__ global files collection
-
-    Returns:
-    : `mkdocs.nav.Page` instance
+::: mkdocs.plugins.BasePlugin.on_pre_page
 
 ##### on_page_read_source
 
-*   The `on_page_read_source` event can replace the default mechanism to read
-    the contents of a page's source from the filesystem.
-
-    Parameters:
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-
-    Returns:
-    : The raw source for a page as unicode string. If `None` is returned, the
-      default loading from a file will be performed.
+::: mkdocs.plugins.BasePlugin.on_page_read_source
 
 ##### on_page_markdown
 
-*   The `page_markdown` event is called after the page's markdown is loaded
-    from file and can be used to alter the Markdown source text. The meta-
-    data has been stripped off and is available as `page.meta` at this point.
-
-    Parameters:
-    : __markdown:__ Markdown source text of page as string
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-    : __files:__ global files collection
-
-    Returns:
-    : Markdown source text of page as string
+::: mkdocs.plugins.BasePlugin.on_page_markdown
 
 ##### on_page_content
 
-*   The `page_content` event is called after the Markdown text is rendered to
-    HTML (but before being passed to a template) and can be used to alter the
-    HTML body of the page.
-
-    Parameters:
-    : __html:__ HTML rendered from Markdown source as string
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-    : __files:__ global files collection
-
-    Returns:
-    : HTML rendered from Markdown source as string
+::: mkdocs.plugins.BasePlugin.on_page_content
 
 ##### on_page_context
 
-*   The `page_context` event is called after the context for a page is created
-    and can be used to alter the context for that specific page only.
-
-    Parameters:
-    : __context__: dict of template context variables
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-    : __nav:__ global navigation object
-
-    Returns:
-    : dict of template context variables
+::: mkdocs.plugins.BasePlugin.on_page_context
 
 ##### on_post_page
 
-*   The `post_page` event is called after the template is rendered, but
-    before it is written to disc and can be used to alter the output of the
-    page. If an empty string is returned, the page is skipped and nothing is
-    written to disc.
-
-    Parameters:
-    : __output:__ output of rendered template as string
-    : __page:__ `mkdocs.nav.Page` instance
-    : __config:__ global configuration object
-
-    Returns:
-    : output of rendered template as string
+::: mkdocs.plugins.BasePlugin.on_post_page
 
 ### Handling Errors
 
@@ -503,6 +349,4 @@ tell MkDocs to use it via the config.
 [Template Events]: #template-events
 [MkDocs Plugins]: https://github.com/mkdocs/mkdocs/wiki/MkDocs-Plugins
 [on_build_error]: #on_build_error
-[Handling Errors]: #handling-errors
 [config_scheme]: #config_scheme
-[Template]: http://code.nabla.net/doc/jinja2/api/jinja2/environment/jinja2.environment.Template.html
diff --git a/mkdocs.yml b/mkdocs.yml
index 7b0b77f7..7b5e587c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -60,3 +60,13 @@ plugins:
             user-guide/plugins.md: dev-guide/plugins.md
             user-guide/custom-themes.md: dev-guide/themes.md
             user-guide/styling-your-docs.md: user-guide/choosing-your-theme.md
+    - mkdocstrings:
+        handlers:
+          python:
+            options:
+              docstring_section_style: list
+              show_root_toc_entry: false
+              show_source: false
+
+watch:
+    - mkdocs
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index d71238e7..f66c6021 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -6,10 +6,16 @@ Implements the plugin API for MkDocs.
 
 import logging
 from collections import OrderedDict
+from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar
 
 import importlib_metadata
+import jinja2.environment
 
 from mkdocs.config.base import Config
+from mkdocs.livereload import LiveReloadServer
+from mkdocs.structure.files import Files
+from mkdocs.structure.nav import Navigation
+from mkdocs.structure.pages import Page
 
 log = logging.getLogger('mkdocs.plugins')
 
@@ -61,7 +67,9 @@ class BasePlugin:
     config_scheme = ()
     config = {}
 
-    def load_config(self, options, config_file_path=None):
+    def load_config(
+        self, options: Dict[str, Any], config_file_path: Optional[str] = None
+    ) -> Tuple[List[str], List[str]]:
         """Load config from a dict of options. Returns a tuple of (errors, warnings)."""
 
         self.config = Config(schema=self.config_scheme, config_file_path=config_file_path)
@@ -69,6 +77,281 @@ class BasePlugin:
 
         return self.config.validate()
 
+    # Global events
+
+    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
+        it is activated. For example, additional files or directories could be added
+        to the list of "watched" files for auto-reloading.
+
+        Parameters:
+            server: `livereload.Server` instance
+            config: global configuration object
+            builder: a callable which gets passed to each call to `server.watch`
+
+        Returns:
+            `livereload.Server` instance
+        """
+        return server
+
+    def on_config(self, config: Config) -> Optional[Config]:
+        """
+        The `config` event is the first event called on build and is run immediately
+        after the user configuration is loaded and validated. Any alterations to the
+        config should be made here.
+
+        Parameters:
+            config: global configuration object
+
+        Returns:
+            global configuration object
+        """
+        return config
+
+    def on_pre_build(self, config: Config) -> None:
+        """
+        The `pre_build` event does not alter any variables. Use this event to call
+        pre-build scripts.
+
+        Parameters:
+            config: global configuration object
+        """
+
+    def on_files(self, files: Files, config: Config) -> Optional[Files]:
+        """
+        The `files` event is called after the files collection is populated from the
+        `docs_dir`. Use this event to add, remove, or alter files in the
+        collection. Note that Page objects have not yet been associated with the
+        file objects in the collection. Use [Page Events](plugins.md#page-events) to manipulate page
+        specific data.
+
+        Parameters:
+            files: global files collection
+            config: global configuration object
+
+        Returns:
+            global files collection
+        """
+        return files
+
+    def on_nav(self, nav: Navigation, config: Config, files: Files) -> Optional[Navigation]:
+        """
+        The `nav` event is called after the site navigation is created and can
+        be used to alter the site navigation.
+
+        Parameters:
+            nav: global navigation object
+            config: global configuration object
+            files: global files collection
+
+        Returns:
+            global navigation object
+        """
+        return nav
+
+    def on_env(
+        self, env: jinja2.Environment, config: Config, files: Files
+    ) -> Optional[jinja2.Environment]:
+        """
+        The `env` event is called after the Jinja template environment is created
+        and can be used to alter the
+        [Jinja environment](https://jinja.palletsprojects.com/en/latest/api/#jinja2.Environment).
+
+        Parameters:
+            env: global Jinja environment
+            config: global configuration object
+            files: global files collection
+
+        Returns:
+            global Jinja Environment
+        """
+        return env
+
+    def on_post_build(self, config: Config) -> None:
+        """
+        The `post_build` event does not alter any variables. Use this event to call
+        post-build scripts.
+
+        Parameters:
+            config: global configuration object
+        """
+
+    def on_build_error(self, error: Exception) -> None:
+        """
+        The `build_error` event is called after an exception of any kind
+        is caught by MkDocs during the build process.
+        Use this event to clean things up before MkDocs terminates. Note that any other
+        events which were scheduled to run after the error will have been skipped. See
+        [Handling Errors](plugins.md#handling-errors) for more details.
+
+        Parameters:
+            error: exception raised
+        """
+
+    # Template events
+
+    def on_pre_template(
+        self, template: jinja2.Template, template_name: str, config: Config
+    ) -> Optional[jinja2.Template]:
+        """
+        The `pre_template` event is called immediately after the subject template is
+        loaded and can be used to alter the template.
+
+        Parameters:
+            template: a Jinja2 [Template](https://jinja.palletsprojects.com/en/latest/api/#jinja2.Template) object
+            template_name: string filename of template
+            config: global configuration object
+
+        Returns:
+            a Jinja2 [Template](https://jinja.palletsprojects.com/en/latest/api/#jinja2.Template) object
+        """
+        return template
+
+    def on_template_context(
+        self, context: Dict[str, Any], template_name: str, config: Config
+    ) -> Optional[Dict[str, Any]]:
+        """
+        The `template_context` event is called immediately after the context is created
+        for the subject template and can be used to alter the context for that specific
+        template only.
+
+        Parameters:
+            context: dict of template context variables
+            template_name: string filename of template
+            config: global configuration object
+
+        Returns:
+            dict of template context variables
+        """
+        return context
+
+    def on_post_template(self, output_content: str, template_name: str, config: Config) -> str:
+        """
+        The `post_template` event is called after the template is rendered, but before
+        it is written to disc and can be used to alter the output of the template.
+        If an empty string is returned, the template is skipped and nothing is is
+        written to disc.
+
+        Parameters:
+            output_content: output of rendered template as string
+            template_name: string filename of template
+            config: global configuration object
+
+        Returns:
+            output of rendered template as string
+        """
+        return output_content
+
+    # Page events
+
+    def on_pre_page(self, page: Page, config: Config, files: Files) -> Optional[Page]:
+        """
+        The `pre_page` event is called before any actions are taken on the subject
+        page and can be used to alter the `Page` instance.
+
+        Parameters:
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+            files: global files collection
+
+        Returns:
+            `mkdocs.nav.Page` instance
+        """
+        return page
+
+    def on_page_read_source(self, page: Page, config: Config) -> Optional[str]:
+        """
+        The `on_page_read_source` event can replace the default mechanism to read
+        the contents of a page's source from the filesystem.
+
+        Parameters:
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+
+        Returns:
+            The raw source for a page as unicode string. If `None` is returned, the
+                default loading from a file will be performed.
+        """
+        return None
+
+    def on_page_markdown(
+        self, markdown: str, page: Page, config: Config, files: Files
+    ) -> Optional[str]:
+        """
+        The `page_markdown` event is called after the page's markdown is loaded
+        from file and can be used to alter the Markdown source text. The meta-
+        data has been stripped off and is available as `page.meta` at this point.
+
+        Parameters:
+            markdown: Markdown source text of page as string
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+            files: global files collection
+
+        Returns:
+            Markdown source text of page as string
+        """
+        return markdown
+
+    def on_page_content(self, html: str, page: Page, config: Config, files: Files) -> Optional[str]:
+        """
+        The `page_content` event is called after the Markdown text is rendered to
+        HTML (but before being passed to a template) and can be used to alter the
+        HTML body of the page.
+
+        Parameters:
+            html: HTML rendered from Markdown source as string
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+            files: global files collection
+
+        Returns:
+            HTML rendered from Markdown source as string
+        """
+        return html
+
+    def on_page_context(
+        self, context: Dict[str, Any], page: Page, config: Config, nav: Navigation
+    ) -> Optional[Dict[str, Any]]:
+        """
+        The `page_context` event is called after the context for a page is created
+        and can be used to alter the context for that specific page only.
+
+        Parameters:
+            context: dict of template context variables
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+            nav: global navigation object
+
+        Returns:
+            dict of template context variables
+        """
+        return context
+
+    def on_post_page(self, output: str, page: Page, config: Config) -> str:
+        """
+        The `post_page` event is called after the template is rendered, but
+        before it is written to disc and can be used to alter the output of the
+        page. If an empty string is returned, the page is skipped and nothing is
+        written to disc.
+
+        Parameters:
+            output: output of rendered template as string
+            page: `mkdocs.nav.Page` instance
+            config: global configuration object
+
+        Returns:
+            output of rendered template as string
+        """
+        return output
+
+
+T = TypeVar('T')
+
 
 class PluginCollection(OrderedDict):
     """
@@ -81,13 +364,8 @@ class PluginCollection(OrderedDict):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.events = {x: [] for x in EVENTS}
-
-    def _register_event(self, event_name, method):
-        """Register a method for an event."""
-        self.events[event_name].append(method)
 
-    def __setitem__(self, key, value, **kwargs):
+    def __setitem__(self, key: str, value: BasePlugin, **kwargs):
         if not isinstance(value, BasePlugin):
             raise TypeError(
                 f'{self.__module__}.{self.__name__} only accepts values which'
@@ -95,13 +373,8 @@ class PluginCollection(OrderedDict):
                 ' subclasses'
             )
         super().__setitem__(key, value, **kwargs)
-        # 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)
-            if callable(method):
-                self._register_event(event_name[3:], method)
 
-    def run_event(self, name, item=None, **kwargs):
+    def run_event(self, name: str, item: T = None, **kwargs) -> T:
         """
         Run all registered methods of an event.
 
@@ -111,8 +384,11 @@ class PluginCollection(OrderedDict):
         be modified by the event method.
         """
 
+        name = 'on_' + name
         pass_item = item is not None
-        for method in self.events[name]:
+        getattr(BasePlugin, name)  # just to produce AttributeError
+        for plugin in list(self.values()):  # can change size during iteration
+            method = getattr(plugin, name)
             if pass_item:
                 result = method(item, **kwargs)
             else:
diff --git a/requirements/project-min.txt b/requirements/project-min.txt
index 7dc9825c..966cfb0e 100644
--- a/requirements/project-min.txt
+++ b/requirements/project-min.txt
@@ -1,6 +1,6 @@
 babel==2.9.0
 click==8.0
-Jinja2==2.10.2
+Jinja2==2.11.1
 markupsafe<=2.0.1
 Markdown==3.3
 PyYAML==5.1
@@ -15,3 +15,4 @@ importlib_metadata==4.3
 packaging==20.5
 mergedeep==1.3.4
 colorama==0.4; platform_system == 'Windows'
+mkdocstrings-python==0.7.1; python_version >= '3.7'
diff --git a/requirements/project.txt b/requirements/project.txt
index bcf8e8d3..5431da5e 100644
--- a/requirements/project.txt
+++ b/requirements/project.txt
@@ -1,6 +1,6 @@
 babel>=2.9.0
 click>=8.0
-Jinja2>=2.10.2
+Jinja2>=2.11.1
 Markdown>=3.3,<3.4
 PyYAML>=5.2
 watchdog>=2.0.0
@@ -14,3 +14,4 @@ importlib_metadata>=4.3
 packaging>=20.5
 mergedeep>=1.3.4
 colorama>=0.4; platform_system == 'Windows'
+mkdocstrings-python>=0.7.1; python_version >= '3.7'
diff --git a/tox.ini b/tox.ini
index 409399bb..f699b4d5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,20 +1,21 @@
 [tox]
 envlist =
-    py{36,37,38,39,310,py3}-{unittests,min-req,integration,integration-no-babel},
+    py36-{unittests,min-req},
+    py{37,38,39,310,py3}-{unittests,min-req,integration,integration-no-babel},
     black, flake8, markdown-lint, linkchecker, jshint, csslint, nobabel, codespell
 
 [testenv]
 passenv = LANG
 deps=
-    py{36,37,38,39,310,py3}-{unittests,integration}: -rrequirements/project.txt
-    py{36,37,38,39,310,py3}-min-req: -rrequirements/project-min.txt
+    py{37,38,39,310,py3}-integration: -rrequirements/project.txt
+    py{37,38,39,310,py3}-min-req: -rrequirements/project-min.txt
     py{36,37,38,39,310,py3}-{unittests,min-req}: -rrequirements/test.txt
 commands=
     {envpython} --version
     py{36,37,38,39,310,py3}-{unittests,min-req}:  {envbindir}/coverage run --source=mkdocs --omit 'mkdocs/tests/*' -m unittest discover -p '*tests.py' mkdocs --top-level-directory .
     py{36,37,38,39,310,py3}-unittests: {envbindir}/coverage xml
     py{36,37,38,39,310,py3}-unittests: {envbindir}/coverage report --show-missing
-    py{36,37,38,39,310,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds
+    py{37,38,39,310,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds
 
 [testenv:black]
 deps=-rrequirements/test.txt
@@ -64,7 +65,7 @@ whitelist_externals = csslint
 passenv=*
 commands=csslint mkdocs/
 
-[testenv:py{36,37,38,39,310,py3}-integration-no-babel]
+[testenv:py{37,38,39,310,py3}-integration-no-babel]
 passenv = LANG
 deps=-rrequirements/project.txt
 commands=


Test patch:
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index 4047c175..a8896df0 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -145,7 +145,7 @@ class TestPluginCollection(unittest.TestCase):
 
     def test_run_unknown_event_on_collection(self):
         collection = plugins.PluginCollection()
-        with self.assertRaises(KeyError):
+        with self.assertRaises(AttributeError):
             collection.run_event('unknown', 'page content')
 
     def test_run_build_error_event(self):
diff --git a/requirements/test.txt b/requirements/test.txt
index 62f2c35f..abc37df7 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -2,3 +2,4 @@ coverage
 black
 isort
 flake8
+babel>=2.9.0