Skip to main content

Python Plugin Project

Project description

mm-pyplugin

Plugin infrastructure for Python applications with Pydantic configuration validation, lifecycle management, and dependency injection.

Features

  • Structured plugin lifecycle: __init__configure()setup()teardown()
  • Two base styles: ComposablePluginBase (configure-then-setup) and SimplePluginBase (config at construction)
  • Typed config, zero boilerplate: config schema is inferred from the generic parameter — no config_schema() to write
  • Dependency injection: a context container shares services (loggers, databases, etc.) across plugins
  • Fluent API: method chaining for clean, readable setup
  • Declarative composition: nest sub-plugins by declaring typed fields — resolved recursively
  • Singletons: opt-in per class via is_singleton
  • Automatic discovery: entry-point-based plugin registration, with test overrides

Installation

pip install mm-pyplugin

For local development:

cd mm-pyplugin
uv sync

Core Concepts

Generics

PluginBase is generic over two type parameters:

PluginBase[TContext, TConfig]
  • TContext — the type of the dependency-injection container (often dict).
  • TConfig — the plugin's configuration model; must subclass PluginConfig.

The config schema is derived automatically from TConfig by walking the class's generic bases, so you never implement config_schema() by hand (override it only for runtime schema selection).

Configuration models

Config schemas subclass PluginConfig (a Pydantic BaseModel that allows extra fields and carries an optional plugin_class discriminator):

from mm_pyplugin import PluginConfig

class MyConfig(PluginConfig):
    host: str = "localhost"
    port: int = 8080
    ssl_enabled: bool = False

Config can be supplied as a dict, a JSON string, a Path to a JSON file, or a PluginConfig instance.

Quick Start

Composable plugin (configure → setup)

ComposablePluginBase splits construction from configuration. Accessing config before configure() raises.

import logging
from mm_pyplugin import ComposablePluginBase, PluginConfig

class MyConfig(PluginConfig):
    timeout: int = 30
    retries: int = 3

class MyPlugin(ComposablePluginBase[dict, MyConfig]):
    def setup(self) -> "MyPlugin":
        super().setup()                       # call FIRST — guards double-setup
        logger = self.context.get("logger") if self.context else None
        if logger:
            logger.info(f"setup with timeout={self.config.timeout}")
        return self

    def teardown(self) -> None:
        logger = self.context.get("logger") if self.context else None
        if logger:
            logger.info("cleaning up")
        super().teardown()                    # call LAST

# DI container of shared services
context = {"logger": logging.getLogger("app")}

plugin = (MyPlugin(context)
    .configure({"timeout": 60, "retries": 5})
    .setup())

assert plugin.was_setup
plugin.context["logger"].info("ready")
plugin.teardown()

Simple plugin (config at construction)

SimplePluginBase takes config in the constructor, so config is available immediately — there is no configure() step.

from mm_pyplugin import SimplePluginBase

class MyPlugin(SimplePluginBase[dict, MyConfig]):
    def setup(self) -> "MyPlugin":
        super().setup()
        return self

    def teardown(self) -> None:
        super().teardown()

plugin = MyPlugin({"timeout": 60}, context).setup()

Dependency Injection

context is a plain container (typically a dict) of shared services, passed at construction and reachable via the read-only self.context property throughout the lifecycle:

context = {
    "logger": logging.getLogger("app"),
    "database": Database("localhost:5432"),
    "cache": RedisCache("localhost:6379"),
}

# All plugins share the same services
p1 = LogParser(context).configure(cfg1).setup()
p2 = OutputWriter(context).configure(cfg2).setup()

p1.context["database"].query("SELECT 1")

Benefits: one shared connection across plugins; trivial testing by injecting mocks; per-environment wiring (dev/prod) without touching plugin code.

Composition (sub-plugin injection)

Composition is declarative, not manual: a parent declares a sub-plugin as a typed attribute, and its config declares the matching sub-config field.

from mm_pyplugin import ComposablePluginBase, PluginConfig

class ChildConfig(PluginConfig):
    greeting: str = "Hello"

class Child(ComposablePluginBase[dict, ChildConfig]):
    def setup(self): return super().setup()
    def teardown(self): super().teardown()

class ParentConfig(PluginConfig):
    child: ChildConfig = ChildConfig()        # sub-config field

class Parent(ComposablePluginBase[dict, ParentConfig]):
    child: Child                              # matching typed attribute

    def setup(self): return super().setup()
    def teardown(self): super().teardown()

Create the parent with PluginFactory.create_plugin(..., recursive=True) (the parent must be discoverable — see Plugin Discovery). Each config field whose value is a PluginConfig and whose parent attribute is annotated as a PluginBase subclass is instantiated depth-first, set up, and assigned to the parent:

from mm_pyplugin import PluginFactory

cfg = ParentConfig(plugin_class="mypackage.plugins.Parent", child={"greeting": "Hi"})
parent = PluginFactory(context).create_plugin(cfg, recursive=True)

assert isinstance(parent.child, Child)

Pass setup=False to create_plugin to get configured-but-not-set-up plugins.

Plugin Discovery

Register plugins under the mm_pyplugin entry-point group in pyproject.toml:

[project.entry-points.mm_pyplugin]
my_plugin = "mypackage.plugins:MyPlugin"

Then discover and instantiate by class name:

from mm_pyplugin import PluginFactory, PluginConfig

# {"mypackage.plugins.MyPlugin": <class MyPlugin>, ...}
plugins = PluginFactory.find_plugins()

# Instantiate from a PluginConfig that names its plugin_class
cfg = PluginConfig(plugin_class="mypackage.plugins.MyPlugin", timeout=10)
plugin = PluginFactory(context).create_plugin(cfg, recursive=True)

create_plugin(instance_config, context=None, recursive=False, setup=True) looks up instance_config.plugin_class in the registry (raising KeyError if absent), instantiates it, optionally injects sub-plugins, and optionally calls setup().

Singletons

Set is_singleton = True on a plugin class to reuse a single live instance: when one already exists in the registry, PluginFactory returns it instead of constructing a new one.

class Cache(SimplePluginBase[dict, MyConfig]):
    is_singleton = True
    ...

Key Classes

Class Role
PluginBase Abstract, generic base; lifecycle, context, config_schema()
SimplePluginBase Config supplied at construction; config available immediately
ComposablePluginBase Adds a configure() step before setup()
PluginConfig Pydantic base for all config schemas (extra fields allowed)
PluginFactory Discovery (find_plugins) and instantiation (create_plugin)
PluginSetupException Raised on lifecycle-ordering violations

Lifecycle rules

  • setup() — call super().setup() first; raises PluginSetupException if already set up. was_setup becomes True.
  • teardown() — call super().teardown() last; raises PluginSetupException if not set up. was_setup becomes False.

Testing

Inject mocks via the context, and swap real plugins for test doubles with the plugin_overrides context manager (no entry points required):

from mm_pyplugin.testing import plugin_overrides
from mm_pyplugin import PluginFactory, PluginConfig

with plugin_overrides(FakeSource, FakeTransformer):
    cfg = PluginConfig(plugin_class="tests.FakeSource")
    plugin = PluginFactory({"database": MockDatabase()}).create_plugin(cfg)
# overrides cleared automatically on exit

Running tests

uv run python -m pytest

Examples

Runnable examples live in tests/examples/:

  • ExamplePlugin / ExamplePluginConfig — composable plugin with DI
  • SimpleExamplePluginSimplePluginBase variant
  • ParentPlugin / ParentPluginConfig — declarative sub-plugin injection

See USAGE_EXAMPLES.md for additional patterns.

License

MIT

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

mm_pyplugin-2.3.3.tar.gz (173.6 kB view details)

Uploaded Source

Built Distribution

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

mm_pyplugin-2.3.3-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

File details

Details for the file mm_pyplugin-2.3.3.tar.gz.

File metadata

  • Download URL: mm_pyplugin-2.3.3.tar.gz
  • Upload date:
  • Size: 173.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for mm_pyplugin-2.3.3.tar.gz
Algorithm Hash digest
SHA256 824a6a37e83d5151641419d1aff521eb38b710ed1d283a1602805f28d1d84fa6
MD5 90d7e91ecbfa722b2963fc94c03d0172
BLAKE2b-256 c6114e27b70eed0cc9871b7c193137eb7745f63e2d658e88d04e77e32d3b9cbf

See more details on using hashes here.

File details

Details for the file mm_pyplugin-2.3.3-py3-none-any.whl.

File metadata

  • Download URL: mm_pyplugin-2.3.3-py3-none-any.whl
  • Upload date:
  • Size: 12.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for mm_pyplugin-2.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 594b5319ef55acd0c818c93ec2f522ffebef44410a2d051343b363fb38ce3106
MD5 afbaf99a8eae79eaf56faf39bb5052ec
BLAKE2b-256 9bff573f107431545d0ffda6b57757c626daf4601b4d7fa1d20d4306411c7bd9

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