Skip to content

Event Based Plugin API

Waylan Limberg edited this page Aug 26, 2019 · 17 revisions

Proposal

This is a proposal for a Plugin API based on events. This is a living document.


The actual implementation of the Plugin API was first committed in 73b6165 (see #1223). Please consult the documentation for use of the Plugin API. This document is out-of-date and exists for historical purposes only.


Plugin Registration

Plugins would be packaged as Python libs (distributed on PyPI separate from MkDocs) and each would register as a Plugin via a setuptools entry_point (like themes do now). Add the following to your setup.py script:

entry_points={
    'mkdocs.plugins': [
        'pluginname = path.to.some_plugin:SomePluginClass',
    ]
}

The pluginname would be the name used by users (in the config file) and path.to.some_plugin:SomePluginClass would be the importable plugin itself (from path.to.some_plugin import SomePluginClass) where SomePluginClass is the class which defines the plugin behavior. Naturally, multiple Plugin classes could exist in the same module. Simply define each as a separate entry_point.

entry_points={
    'mkdocs.plugins': [
        'featureA = path.to.my_plugins:PluginA',
        'featureB = path.to.my_plugins:PluginB'
    ]
}

Note that registering a plugin does not activate it. Like with themes, it only makes MkDocs aware of it. The user still needs to tell MkDocs to use if via the config.

Activating Plugins

To activate a plugin, the user needs to add it to their config file as an item in the list of plugins. A plugin item may be one of either a string or an associative array (YAML's key value construct, Python dict). If a string, that string must be the name of a registered (and installed) Plugin with no config settings. If an associative array, The array must have one key which is the plugin's name (as registered via an entry_point). The value would be another associative array of key and value config settings specific to the plugin. For example:

plugins:
    - asimpleplugin
    - fancyplugin:
        setting_1: some value
        setting_2: other value

Plugins will be expected to supply validation for their config settings. The existing config validation may need to be modified to allow nested Config classes for this to work.

While plugins may be able to access the entire config (and alter values via post-validation, etc), they would only be permitted to add setting keys nested within their own config object. No global config settings will be permitted to be added by a plugin.

Events

Plugins work by registering callables with an event. During the build process, MkDocs will come upon events during different stages and call out to the registered callables (passing the config, globals, page, etc) and those callables can modify and return the passed objects. The events available would be (a work in progress):

event vars Possible usage
post-config config Alter config or prepossessing based on config values
pre-nav config, nav-item Alter/intercept a nav item's creation
post-nav config, nav-item, global nav (so far) Alter a just created nav item
pre-build config, global template vars, global nav Alter global nav, template vars, etc.
pre-page config, template, context Alter single page and/or template
post-page config, rendered template Page specific post-processing
post-build config project wide postprocessing
deploy config provide a deploy script (only fired from deploy command)
pre-serve config, server_instance add addition files/dirs to watcher (only fired from serve command)

The specific events may need some work. For example, is the pre/post-page events enough or should we also have pre/post-template events specific to template rendering?

Implementation

A base Plugin class will be defined by MkDocs which each plugin should subclass. On init, the class would accept the plugin specific settings as arguments and call user defined validation (via a scheme defined in the subclass) on the settings. MkDocs will store an instance of each Plugin in a list (probably replacing the plugins config with the list of instances, just like it replaces the list of pages with Page instances). The order of the plugins as they are defined in the config is retained, which may matter in some cases.

Each class would also have one or more "events" defined, which would be called by MkDocs in order (the order they are listed in the config) from the appropriate point in MkDocs' execution path. Each "event" would be defined as a method on the subclass which accepted the required keywords (and **kwargs to future-proof it).

class MyPlugin(mkdocs.Plugin):
    config_scheme = (
        ('setting_1', mkdocs.config_options.Type(mkdocs.utils.string_types)),
        ('setting_2', mkdocs.config_options.Dir(default='foo', exists=True)),
    )

    def on_pre_page(self, **kwargs):
        # implement your `pre_page` event here ...

    def on_post_page(self, **kwargs):
        # implement your `post-page` event here ...

In the event that we include theme support for plugins (a theme can provide its own template -- see this comment), then an attribute on the class could be used to set the dir (relative to the location of the module where the class is defined). The default would be None for no theme support.

class MyPlugin(mkdocs.Plugin):
    theme_dir = 'path/to/theme/dir/'
    ...

Proof of Concept

Following is a (untested) proof of concept of what a complete plugin might look like. This plugin would be a "deploy script" which would replace the existing gh-deploy command. It assumes a (currently nonexistent) library called gh_deploy to keep the example simple. As the deploy event would be fired after a build is complete during the new deploy command, the plugin would look something like this:

setup.py

from setuptools import setup

setup(
    name='mkdocs2ghpages',
    version='1.0',
    description='Deploy MkDocs projects to Github Pages',
    author='John Doe',
    author_email='jdoe@example.com',
    url='https://example.com/mkdocs2ghpages',
    py_modules=['m2ghp'],
    install_requires = ['mkdocs>=1.0', 'gh_deploy'],
    entry_points={
        'mkdocs.plugins': [
            '2ghpages = m2ghp:GhPagesPlugin',
        ]
    }
)

m2ghp.py

import mkdocs
import gh_deploy

class GhPagesPlugin(mkdocs.BasePlugin):
    config_scheme = (
        ('remote_branch', mkdocs.config_options.Type(mkdocs.utils.string_types, default='gh-pages')),
        ('remote_name', mkdocs.config_options.Type(mkdocs.utils.string_types, default='origin')),
    )

    def on_deploy(self, config, **kwargs):
        gh_deploy.gh_deploy(config['site_dir'], self.config['remote_name'], self.config['remote_branch'])

That's it. Naturally the on_deploy method for most situations would actually be more complex that that. The entire (imaginary) gh_deploy function called above could be implemented as the on_deploy method itself.

The Users mkdocs.yml config would look like this:

site_name: Marshmallow Generator
plugins:
    - 2ghpages:
        remote_name: gh-pages
        remote_name: origin

Actually, as that just uses the defaults, it could look like this instead:

site_name: Marshmallow Generator
plugins:
    - 2ghpages

Plugin Template

Developers looking to build a plugin may find the following template helpful, which contains all possible plugin hooks that one just needs to implement: