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) andSimplePluginBase(config at construction) - Typed config, zero boilerplate: config schema is inferred from the generic parameter — no
config_schema()to write - Dependency injection: a
contextcontainer 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 (oftendict).TConfig— the plugin's configuration model; must subclassPluginConfig.
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()— callsuper().setup()first; raisesPluginSetupExceptionif already set up.was_setupbecomesTrue.teardown()— callsuper().teardown()last; raisesPluginSetupExceptionif not set up.was_setupbecomesFalse.
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 DISimpleExamplePlugin—SimplePluginBasevariantParentPlugin/ParentPluginConfig— declarative sub-plugin injection
See USAGE_EXAMPLES.md for additional patterns.
License
MIT
Release files for mm-pyplugin 2.3.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mm_pyplugin-2.3.4.tar.gz | 173.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mm_pyplugin-2.3.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 185.7 kB
Release files / mm_pyplugin-2.3.4.tar.gz
| Download URL | mm_pyplugin-2.3.4.tar.gz |
|---|---|
| Size | 173.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ee9f5b0de056acb0c10a7129fcd60aa28bf131a0972b7364961f82da4c7351a2
|
|
BLAKE2b-256 checksum How to use checksums |
170f198a64d3eefac12ae7ec14ae756f96e2b3d89de3b0e742c985b22490276a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mm_pyplugin-2.3.4-py3-none-any.whl
| Download URL | mm_pyplugin-2.3.4-py3-none-any.whl |
|---|---|
| Size | 12.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d3474701311333abda2e56c01dd78bff3041de7bf4bced2363bd070fad71f1aa
|
|
BLAKE2b-256 checksum How to use checksums |
3757e16c324f36375ce53806cfadd6136367f4e2e5cca5d58e139c2b387c523d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|