Plugin API

This documents the general plugin API.

Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE. Authors are encouraged to develop plugins and work with the MediaGoblin community to keep them up to date, but this API will be a moving target for a few releases.

Please check the Release Notes for updates!

How are hooks added? Where do I find them?

Much of this document talks about hooks, both as in terms of regular hooks and template hooks. But where do they come from, and how can you find a list of them?

For the moment, the best way to find available hooks is to check the source code itself. (Yes, we should start a more official hook listing with descriptions soon.) But many hooks you may need do not exist yet: what to do then?

The plan at present is that we are adding hooks as people need them, with community discussion. If you find that you need a hook and MediaGoblin at present doesn’t provide it at present, please talk to us! We’ll evaluate what to do from there.

pluginapi Module

This module implements the plugin api bits.

Two things about things in this module:

  1. they should be excessively well documented because we should pull from this file for the docs
  2. they should be well tested

How do plugins work?

Plugins are structured like any Python project. You create a Python package. In that package, you define a high-level __init__.py module that has a hooks dict that maps hooks to callables that implement those hooks.

Additionally, you want a LICENSE file that specifies the license and a setup.py that specifies the metadata for packaging your plugin. A rough file structure could look like this:

myplugin/
 |- setup.py         # plugin project packaging metadata
 |- README           # holds plugin project information
 |- LICENSE          # holds license information
 |- myplugin/        # plugin package directory
    |- __init__.py   # has hooks dict and code

Lifecycle

  1. All the modules listed as subsections of the plugins section in the config file are imported. MediaGoblin registers any hooks in the hooks dict of those modules.
  2. After all plugin modules are imported, the setup hook is called allowing plugins to do any set up they need to do.
mediagoblin.tools.pluginapi.get_config(key)

Retrieves the configuration for a specified plugin by key

Example:

>>> get_config('mediagoblin.plugins.sampleplugin')
{'foo': 'bar'}
>>> get_config('myplugin')
{}
>>> get_config('flatpages')
{'directory': '/srv/mediagoblin/pages', 'nesting': 1}}
mediagoblin.tools.pluginapi.register_routes(routes)

Registers one or more routes

If your plugin handles requests, then you need to call this with the routes your plugin handles.

A “route” is a routes.Route object. See the routes.Route documentation for more details.

Example passing in a single route:

>>> register_routes(('about-view', '/about',
...     'mediagoblin.views:about_view_handler'))

Example passing in a list of routes:

>>> register_routes([
...     ('contact-view', '/contact', 'mediagoblin.views:contact_handler'),
...     ('about-view', '/about', 'mediagoblin.views:about_handler')
... ])

Note

Be careful when designing your route urls. If they clash with core urls, then it could result in DISASTER!

mediagoblin.tools.pluginapi.register_template_path(path)

Registers a path for template loading

If your plugin has templates, then you need to call this with the absolute path of the root of templates directory.

Example:

>>> my_plugin_dir = os.path.dirname(__file__)
>>> template_dir = os.path.join(my_plugin_dir, 'templates')
>>> register_template_path(template_dir)

Note

You can only do this in setup_plugins(). Doing this after that will have no effect on template loading.

mediagoblin.tools.pluginapi.register_template_hooks(template_hooks)

Register a dict of template hooks.

Takes template_hooks as an argument, which is a dictionary of template hook names/keys to the templates they should provide. (The value can either be a single template path or an iterable of paths.)

Example:

{"media_sidebar": "/plugin/sidemess/mess_up_the_side.html",
 "media_descriptionbox": ["/plugin/sidemess/even_more_mess.html",
                          "/plugin/sidemess/so_much_mess.html"]}
mediagoblin.tools.pluginapi.get_hook_templates(hook_name)

Get a list of hook templates for this hook_name.

Note: for the most part, you access this via a template tag, not this method directly, like so:

{% template_hook("media_sidebar") %}

... which will include all templates for you, partly using this method.

However, this method is exposed to templates, and if you wish, you can iterate over templates in a template hook manually like so:

{% for template_path in get_hook_templates("media_sidebar") %}
  <div class="extra_structure">
    {% include template_path %}
  </div>
{% endfor %}
Returns:
A list of strings representing template paths.
mediagoblin.tools.pluginapi.hook_handle(hook_name, *args, **kwargs)

Run through hooks attempting to find one that handle this hook.

All callables called with the same arguments until one handles things and returns a non-None value.

(If you are writing a handler and you don’t have a particularly useful value to return even though you’ve handled this, returning True is a good solution.)

Note that there is a special keyword argument:
if “default_handler” is passed in as a keyword argument, this will be used if no handler is found.
Some examples of using this:
  • You need an interface implemented, but only one fit for it
  • You need to do something, but only one thing needs to do it.
mediagoblin.tools.pluginapi.hook_runall(hook_name, *args, **kwargs)

Run through all callable hooks and pass in arguments.

All non-None results are accrued in a list and returned from this. (Other “false-like” values like False and friends are still accrued, however.)

Some examples of using this:
  • You have an interface call where actually multiple things can and should implement it
  • You need to get a list of things from various plugins that handle them and do something with them
  • You need to do something, and actually multiple plugins need to do it separately
mediagoblin.tools.pluginapi.hook_transform(hook_name, arg)

Run through a bunch of hook callables and transform some input.

Note that unlike the other hook tools, this one only takes ONE argument. This argument is passed to each function, which in turn returns something that becomes the input of the next callable.

Some examples of using this:
  • You have an object, say a form, but you want plugins to each be able to modify it.

Configuration

Your plugin may define its own configuration defaults.

Simply add to the directory of your plugin a config_spec.ini file. An example might look like:

[plugin_spec]
some_string = string(default="blork")
some_int = integer(default=50)

This means that when people enable your plugin in their config you’ll be able to provide defaults as well as type validation.

You can access this via the app_config variables in mg_globals, or you can use a shortcut to get your plugin’s config section:

>>> from mediagoblin.tools import pluginapi
# Replace with the path to your plugin.
# (If an external package,  it won't be part of mediagoblin.plugins)
>>> floobie_config = pluginapi.get_config('mediagoblin.plugins.floobifier')
>>> floobie_dir = floobie_config['floobie_dir']
# This is the same as the above
>>> from mediagoblin import mg_globals
>>> config = mg_globals.global_config['plugins']['mediagoblin.plugins.floobifier']
>>> floobie_dir = floobie_config['floobie_dir']

A tip: you have access to the %(here)s variable in your config, which is the directory that the user’s MediaGoblin config is running out of. So for example, your plugin may need a “floobie” directory to store floobs in. You could give them a reasonable default that makes use of the default user_dev location, but allow users to override it, like so:

[plugin_spec]
floobie_dir = string(default="%(here)s/user_dev/floobs/")

Note, this is relative to the user’s MediaGoblin config directory, not your plugin directory!

Context Hooks

View specific hooks

You can hook up to almost any template called by any specific view fairly easily. As long as the view directly or indirectly uses the method render_to_response you can access the context via a hook that has a key in the format of the tuple:

(view_symbolic_name, view_template_path)

Where the “view symbolic name” is the same parameter used in request.urlgen() to look up the view. So say we’re wanting to add something to the context of the user’s homepage. We look in mediagoblin/user_pages/routing.py and see:

add_route('mediagoblin.user_pages.user_home',
          '/u/<string:user>/',
          'mediagoblin.user_pages.views:user_home')

Aha! That means that the name is mediagoblin.user_pages.user_home. Okay, so then we look at the view at the mediagoblin.user_pages.user_home method:

@uses_pagination
def user_home(request, page):
    # [...] whole bunch of stuff here
    return render_to_response(
        request,
        'mediagoblin/user_pages/user.html',
        {'user': user,
         'user_gallery_url': user_gallery_url,
         'media_entries': media_entries,
         'pagination': pagination})

Nice! So the template appears to be mediagoblin/user_pages/user.html. Cool, that means that the key is:

("mediagoblin.user_pages.user_home",
 "mediagoblin/user_pages/user.html")

The context hook uses hook_transform() so that means that if we’re hooking into it, our hook will both accept one argument, context, and should return that modified object, like so:

def add_to_user_home_context(context):
    context['foo'] = 'bar'
    return context

hooks = {
    ("mediagoblin.user_pages.user_home",
     "mediagoblin/user_pages/user.html"): add_to_user_home_context}

Global context hooks

If you need to add something to the context of every view, it is not hard; there are two hooks hook that also uses hook_transform (like the above) but make available what you are providing to every view.

Note that there is a slight, but critical, difference between the two.

The most general one is the 'template_global_context' hook. This one is run only once, and is read into the global context... all views will get access to what are in this dict.

The slightly more expensive but more powerful one is 'template_context_prerender'. This one is not added to the global context... it is added to the actual context of each individual template render right before it is run! Because of this you also can do some powerful and crazy things, such as checking the request object or other parts of the context before passing them on.

Adding static resources

It’s possible to add static resources for your plugin. Say your plugin needs some special JavaScript and images... how to provide them? Then how to access them? MediaGoblin has a way!

Attaching to the hook

First, you need to register your plugin’s resources with the hook. This is pretty easy actually: you just need to provide a function that passes back a PluginStatic object.

class mediagoblin.tools.staticdirect.PluginStatic(name, file_path)

Pass this into the 'static_setup' hook to register your plugin’s static directory.

This has two mandatory attributes that you must pass in on class init:

  • name: this name will be both used for lookup in “urlgen” for your plugin’s static resources and for the subdirectory that it’ll be “mounted” to for serving via your web browser. It MUST be unique. If writing a plugin bundled with MediaGoblin please use the pattern ‘coreplugin__foo’ where ‘foo’ is your plugin name. All external plugins should use their modulename, so if your plugin is ‘mg_bettertags’ you should also call this name ‘mg_bettertags’.
  • file_path: the directory your plugin’s static resources are located in. It’s recommended that you use pkg_resources.resource_filename() for this.

An example of using this:

from pkg_resources import resource_filename
from mediagoblin.tools.staticdirect import PluginStatic

hooks = {
    'static_setup': lambda: PluginStatic(
        'mg_bettertags',
        resource_filename('mg_bettertags', 'static'))
}

Using staticdirect

Once you have this, you will want to be able to of course link to your assets! MediaGoblin has a “staticdirect” tool; you want to use this like so in your templates:

staticdirect("css/monkeys.css", "mystaticname")

Replace “mystaticname” with the name you passed to PluginStatic. The staticdirect method is, for convenience, attached to the request object, so you can access this in your templates like:

<img alt="A funny bunny"
     src="{{ request.staticdirect('images/funnybunny.png', 'mystaticname') }}" />

Additional hook tips

This section aims to explain some tips in regards to adding hooks to the MediaGoblin repository.

WTForms hooks

We haven’t totally settled on a way to transform wtforms form objects, but here’s one way. In your view:

from mediagoblin.foo.forms import SomeForm

def some_view(request)
    form_class = hook_transform('some_form_transform', SomeForm)
    form = form_class(request.form)

Then to hook into this form, do something in your plugin like:

import wtforms

class SomeFormAdditions(wtforms.Form):
    new_datefield = wtforms.DateField()

def transform_some_form(orig_form):
    class ModifiedForm(orig_form, SomeFormAdditions)
    return ModifiedForm

hooks = {
    'some_form_transform': transform_some_form}

Interfaces

If you want to add a pseudo-interface, it’s not difficult to do so. Just write the interface like so:

class FrobInterface(object):
    """
    Interface for Frobbing.

    Classes implementing this interface should provide defrob and frob.
    They may also implement double_frob, but it is not required; if
    not provided, we will use a general technique.
    """

    def defrob(self, frobbed_obj):
        """
        Take a frobbed_obj and defrob it.  Returns the defrobbed object.
        """
        raise NotImplementedError()

    def frob(self, normal_obj):
        """
        Take a normal object and frob it.  Returns the frobbed object.
        """
        raise NotImplementedError()

    def double_frob(self, normal_obj):
        """
        Frob this object and return it multiplied by two.
        """
        return self.frob(normal_obj) * 2


def some_frob_using_method():
    # something something something
    frobber = hook_handle(FrobInterface)
    frobber.frob(blah)

    # alternately you could have a default
    frobber = hook_handle(FrobInterface) or DefaultFrobber
    frobber.defrob(foo)

It’s fine to use your interface as the key instead of a string if you like. (Usually this is messy, but since interfaces are public and since you need to import them into your plugin anyway, interfaces might as well be keys.)

Then a plugin providing your interface can be like:

from mediagoblin.foo.frobfrogs import FrobInterface
from frogfrobber import utils

class FrogFrobber(FrobInterface):
    """
    Takes a frogputer science approach to frobbing.
    """
    def defrob(self, frobbed_obj):
        return utils.frog_defrob(frobbed_obj)

    def frob(self, normal_obj):
        return utils.frog_frob(normal_obj)

 hooks = {
     FrobInterface: lambda: return FrogFrobber}