tool v0.5.0 documentation

Plugins

«  Signals   ::   Contents   ::   Extensions  »

Plugins

Tool provides a simple API for plugins. In fact, even the basic functionality (such as standard commands, templating and so on) is extracted to plugins. This makes Tool extremely flexible. You can swap and drop in almost any component.

Moreover, Tool provides a very simple way to ensure that all plugins are loaded in correct order and all dependencies are configured. This makes plugins indeed “pluggable” as opposed to the Django applications.

Warning

This document is a draft; for now see Extensions (outdated)

A Tool extension is a class (or a factory) that conforms to a minimal API. It is initialized with two arguments: the Application instance and the extension configuration dictionary. It can whatever it needs to do then. The normal workflow is implemented in BasePlugin. You can subclass it and override any attribute you need to tune the behaviour and specify the contribution of the extension to the application.

Interaction between extensions

Extensions can interact with each other. It is strongly adviced that they only access each other’s instances through tool.application.Application.get_extension() or tool.application.Application.get_feature() and only use the extensions’ explicitly published and documented API instead of internal states.

For example, if we have this extension:

class Foo(BasePlugin):
    def make_env(self, bar=123):
        return {'bar': bar + 1}

    def get_bar(self):
        return self.env['bar']

...then another extension, which is dependent on it, should access the bar variable this way:

class Quux(BasePlugin):
    requires = ['foo_ext.Foo']
    def make_env(self):
        foo = self.app.get_extension('foo_ext.Foo')
        foobar = foo.get_bar()
        return {'foobar': foobar}

Of course this is also possible:

foo = app.get_extension('foo_ext.Foo')
foobar = foo.env['bar']

...but extension environment is not public API. It may change and these changes will break all dependent extensions. So the extensions should properly expose special methods and gradually remove them (by deprecating) if they are no more in use.

Features

TODO

Best practices

To keep the extensions truly reusable, it is a good idea to follow these principles:

  • Simple is better than complex
  • Explicit is better than implicit
  • Separation of concerns

And probably the most important thing to keep in mind: You never know how the application will be configured.

Here are some more concrete suggestions:

  • Separate interfaces. If your extension provides both commands and views (i.e. supports both command-line interface and WSGI via routing), separate these interfaces. Expose two extension classes: a CLI-only version and an extended version that supports routing. This allows configuring truly CLI-only applications for which fast cold start is crucial. Using purely CLI extension classes ensures that no routing- or templating-related stuff is loaded.

  • Don’t import. Try not to import anything directly from other extensions. Use their class API instead. Remember that all dependencies will be reliably configured before make_env is called. Write your own extensions so that importing your extension module will not trigger an extra imports. For example, this is not CLI-safe:

    # this imports the templating stuff even if you only need FooCLI
    from tool.ext.templating import register_templates
    
    class FooCLI(BasePlugin):
        commands = ...
    
    class FooWeb(FooCLI):
        requires = ['templating']
        def make_env(self):
            register_templates(__name__)
    

    This version is safe because the importing depends on the configuration:

    class FooCLI(BasePlugin):
        commands = ...
    
    class FooWeb(FooCLI):
        requires = ['templating']
        def make_env(self):
            templating = self.app.plugins['templating']
            templating.register_templates(__name__)
    

API reference

class tool.plugins.BasePlugin(app, conf)

Abstract plugin class. Represents a plugin with configuration logic. Must be not be used directly. Concrete module should either subclass the BasePlugin verbosely or use the make_plugin() factory. Usage:

class MyPlugin(BasePlugin):
    pass

That’s enough for a plugin that requires some initialization logic but does not support external configuration. If you need to pass some settings to the plugin, do this:

class SQLitePlugin(BasePlugin):
    def make_env(self, db_path='test.sqlite'):
        conn = sqlite3.connect(db_path)
        return {'connection': conn}

Here’s how the configuration (in YAML) for this plugin could look like (given that the class is located in the module “sqlite_plugin”):

plugins:
    sqlite_plugin.SQLitePlugin:
        db_path: "databases/orders.db"

And if you don’t need to change the defaults or the plugin doesn’t expect any configuration at all, just do this:

plugins:
    sqlite_plugin.SQLitePlugin: null

This tells the application to load and configure the plugin with default settings.

get_middleware()

Returns either None or a tuple of middleware class and the settings dictionary. Called by the application manager after the plugin environment is initialized. A simplified real-life example:

class RepozeWhoPlugin(BasePlugin):

    def make_env(self, **settings):
        return {'middleware_config': settings['config']}

    def get_middleware(self):
        AuthenticationMiddleware, self.env['middleware_config']
make_env(**settings)
Processes the plugin-related configuration and returns a dictionary representing the plugin state. For example, if the plugin configuration specified database connection settings, then the returned dictionary would contain the actual connection to the database.
tool.plugins.get_feature(name)
Returns the extension class instance
tool.plugins.features(feature)

Adds attribute features to the setup function. Usage:

@features('database')
def setup(app, conf):
    conn = sqlite3.connect(conf.get('uri'))
    return {'db': conn}
tool.plugins.requires(*specs)

Adds attribute requires to the setup function. Usage:

@requires('{database}', '{templating}', 'foo.bar.web_setup')
def setup(app, conf):
    assert not conf
    return {}

«  Signals   ::   Contents   ::   Extensions  »