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) 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
824a6a37e83d5151641419d1aff521eb38b710ed1d283a1602805f28d1d84fa6
|
|
| MD5 |
90d7e91ecbfa722b2963fc94c03d0172
|
|
| BLAKE2b-256 |
c6114e27b70eed0cc9871b7c193137eb7745f63e2d658e88d04e77e32d3b9cbf
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
594b5319ef55acd0c818c93ec2f522ffebef44410a2d051343b363fb38ce3106
|
|
| MD5 |
afbaf99a8eae79eaf56faf39bb5052ec
|
|
| BLAKE2b-256 |
9bff573f107431545d0ffda6b57757c626daf4601b4d7fa1d20d4306411c7bd9
|