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.4.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.4-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: mm_pyplugin-2.3.4.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.4.tar.gz
Algorithm Hash digest
SHA256 ee9f5b0de056acb0c10a7129fcd60aa28bf131a0972b7364961f82da4c7351a2
MD5 174917ce0ddfe75ff89ef3509bc12520
BLAKE2b-256 170f198a64d3eefac12ae7ec14ae756f96e2b3d89de3b0e742c985b22490276a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: mm_pyplugin-2.3.4-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.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d3474701311333abda2e56c01dd78bff3041de7bf4bced2363bd070fad71f1aa
MD5 b4437b3bc2d577f6fe139957e9f01ae2
BLAKE2b-256 3757e16c324f36375ce53806cfadd6136367f4e2e5cca5d58e139c2b387c523d

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