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.7.0.tar.gz (6.3 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.7.0-py3-none-any.whl (6.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: puzzlepluginsystem-0.7.0.tar.gz
  • Upload date:
  • Size: 6.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.13.0

File hashes

Hashes for puzzlepluginsystem-0.7.0.tar.gz
Algorithm Hash digest
SHA256 e95c0f28257838a2d0c0fae3730c6857d24f85d35bf1771b8a6c3d54af699c26
MD5 12d39bdb0eb1b737228d40cd11da8465
BLAKE2b-256 937cde7db002251b7b0e4a9eae02e4cf3e30f336267511e1a696ef0fa057b8fd

See more details on using hashes here.

File details

Details for the file PuzzlePluginSystem-0.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for PuzzlePluginSystem-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b2a9041b31e4eac090ba931f7906eaafbf9641b5df2d5f0199ba688a98248fdd
MD5 33446287603f06f48fa82fb94959089b
BLAKE2b-256 dbbf5ab1e2ca3e38bbd14733400acf66537d96cf7f70c45503d59a3e4ae39693

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