Skip to main content

generic plugin system based on setuptools and blinker

Project description

Puzzle Plugin System

This repo helps implementing a plugin system for Python applications. It uses setuptools entry points for plugin discovery and blinker signals to propagate events between loosely connected components. Both aspects are strictly separated so you can use the setuptools part without blinker signalling (or the other way around).

$ pip install PuzzlePluginSystem

Events and Signals (blinker)

Plugins can "connect" (register) to specific blinker signals. When the main application triggers a signal a subscribed plugin handler is called.

Example application:

# "myapp.plugins" module
from schwarz.puzzle_plugins import SignalRegistry

registry = SignalRegistry()

class AppSignal:
    foo = 'myapp:foo'

# need to trigger plugin registration here (see "Plugin Discovery" section below)

# trigger a signal to call some plugin code and get the returned value
result = registry.call_plugin(AppSignal.foo, signal_kwargs={'a': 137})

This is plugin code (e.g. myplugin.py):

from schwarz.puzzle_plugins import connect_signals, disconnect_signals
from myapp.plugins import AppSignal

class MyPlugin:
    def __init__(self, registry):
        self._connected_signals = None
        self._registry = registry

    def signal_map(self):
        return {
            AppSignal.foo: handle_foo,
        }


# The main application must call this function on startup.
# See "Plugin Discovery" section on how to do this.
def initialize(context, registry):
    plugin = MyPlugin(registry)
    plugin._connected_signals = connect_signals(plugin.signal_map(), registry)
    context['plugin'] = plugin

def terminate(context):
    plugin = context['plugin']
    disconnect_signals(plugin._connected_signals, plugin._registry)
    plugin._registry = None
    plugin._connected_signals = None


# --- actual plugin functionality -----------------------------------------
def handle_foo(sender, a=21):
    return a * 2

SignalRegistry: triggering plugin functionality

registry.call_plugins(signal_name, signal_kwargs={…}, expect_single_result=False) is convenience method to get return values from all plugins registered for the specified signal.

If you pass expect_single_result=True this means you still get only a single (scalar) result value. If multiple plugins return a non-None value ValueError is raised.

Plugin Discovery (setuptools)

The blinker signalling above requires that plugins subscribe to specific signals before the main application triggers a signal. When all plugins are known while writing the main application you can just insert the right calls in the startup routine and everything will be fine.

However I believe the more common scenario (and usually main motivation to introduce a plugin system) is to separate plugins from the main application. For example several customers could use the same base software but different plugins which add customer-specific functionality. In this situation the main application must be able to discover and activate available plugins. This is done with the help of setuptools' entry points.

Example:

Create a separate setuptools-project for your plugin. Add your code (for example as shown in the "Events and Signals" section above).

# file: setup.cfg
[options.entry_points]
myapp.plugins =
    MyPlugin = my_plugin

The my_plugin module must contain two functions which are called by the main application:

  • initialize(context, registry)
  • terminate(context)

context is just a dict where the plugin can store arbitrary state. The main application will keep the context and pass the same instance to terminate().

The main application needs to initialize the plugins at startup. If you use blinker-based signalling you must keep the plugin_loader instance during the whole lifetime of the application. When the instance is garbage collected all blinker signal connections will be lost.

from schwarz.puzzle_plugins import parse_list_str, PluginLoader
from myapp.plugins import registry

def initialize_plugins():
    # This string is usually stored in the application config.
    # Use "*" to enable all plugins.
    plugin_config_str = 'MyPlugin, OtherPlugin'
    enabled_plugins = parse_list_str(plugin_config_str)
    plugin_loader = PluginLoader('myapp.plugins', enabled_plugins=enabled_plugins)
    plugin_loader.initialize_plugins(registry)
    return plugin_loader

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

puzzlepluginsystem-0.8.0.tar.gz (8.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

puzzlepluginsystem-0.8.0-py3-none-any.whl (6.9 kB view details)

Uploaded Python 3

File details

Details for the file puzzlepluginsystem-0.8.0.tar.gz.

File metadata

  • Download URL: puzzlepluginsystem-0.8.0.tar.gz
  • Upload date:
  • Size: 8.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.3

File hashes

Hashes for puzzlepluginsystem-0.8.0.tar.gz
Algorithm Hash digest
SHA256 07d0d3abcafb9eb7f0bed0b4cdbfd8917171f880671cef1640b36e0a01a3825e
MD5 ccba768929192e6df3040fe344b80e53
BLAKE2b-256 ffc6c63aa1e85c10eeca611b5c9985fd9f39b2153871d3c98a9b6b0f6253ed84

See more details on using hashes here.

File details

Details for the file puzzlepluginsystem-0.8.0-py3-none-any.whl.

File metadata

File hashes

Hashes for puzzlepluginsystem-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f0ab6a92cbb9b3d5ab48e98a4e4aafa1d989ec8255e1e8040b440e3a20a256b4
MD5 20de6a8d5677d6c6266e0c7bd3eedd88
BLAKE2b-256 1c3b49b0aed13e0cb961294aee53e652e72a24edf7bf53ba20a529087d1b62bf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page