Zero-boilerplate metaclass-driven plugin registry system with lazy discovery and caching
- Zero Boilerplate: No custom metaclasses, no manual registry creation, just class attributes
- Lazy Discovery: Plugins discovered automatically on first access
- Registry Inheritance: Child classes inherit parent's registry for clean interface hierarchies
- Secondary Registries: Auto-populate related registries from primary registry
- Persistent Caching: Cache discovery results across process restarts
- Auto-Configuration: Automatic inference of discovery packages and recursive settings
- Nominal Registry Families: Stable key-axis declarations with
RegistryFamilyandRegistryKeyAttribute - Registered Enums:
RegisteredEnumMetafor enum members backed by a nominal registry - Type-Safe: Full type hints and mypy support
from metaclass_registry import AutoRegisterMeta, RegistryFamily
# Define the nominal family and its semantic key axis.
class PluginBase(metaclass=AutoRegisterMeta):
__registry_family__ = RegistryFamily(
"plugin_name",
registry_name="plugin",
)
__registry__ = {} # Local script: no package discovery required.
plugin_name = None
# Access the auto-created registry
PLUGINS = PluginBase.__registry__
# Define plugins - they auto-register!
class MyPlugin(PluginBase):
plugin_name = 'my_plugin'
def run(self):
return "Hello from my plugin!"
# Use the registry
print(list(PLUGINS.keys())) # ['my_plugin']
plugin = PLUGINS['my_plugin']()
print(plugin.run()) # "Hello from my plugin!"RegistryFamily is the preferred declaration for an ordinary nominal root.
It installs the compatibility attributes used by AutoRegisterMeta while
keeping the key axis in one object. Consumers should iterate or query
PluginBase.__registry__ rather than maintain a parallel plugin mapping.
The explicit plain registry is appropriate for this one-file example. A root
declared in an importable package can omit it to use lazy package discovery.
Use RegistryConfig when a family needs a supplied registry, a key extractor,
lazy discovery, secondary registries, or custom logging:
from metaclass_registry import (
AutoRegisterMeta,
RegistryConfig,
make_suffix_extractor,
)
HANDLERS = {}
HANDLER_CONFIG = RegistryConfig(
registry_dict=HANDLERS,
key_attribute="handler_type",
key_extractor=make_suffix_extractor("Handler"),
skip_if_no_key=True,
registry_name="handler",
)
class HandlerMeta(AutoRegisterMeta):
def __new__(mcls, name, bases, namespace):
return super().__new__(
mcls,
name,
bases,
namespace,
registry_config=HANDLER_CONFIG,
)
class Handler(metaclass=HandlerMeta):
handler_type = None
class FileHandler(Handler):
pass
assert HANDLERS["file"] is FileHandlerpip install metaclass-registryMost plugin systems require boilerplate code:
Before (Traditional approach):
# Custom metaclass per registry
class PluginMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if hasattr(cls, 'plugin_name') and cls.plugin_name:
PLUGINS[cls.plugin_name] = cls
return cls
# Manual registry creation
PLUGINS = {}
# Base class with custom metaclass
class PluginBase(metaclass=PluginMeta):
plugin_name = NoneAfter (metaclass-registry):
# One nominal family declaration owns the registry protocol.
class PluginBase(metaclass=AutoRegisterMeta):
__registry_family__ = RegistryFamily("plugin_name")
plugin_name = None
# Access auto-created registry
PLUGINS = PluginBase.__registry__class BackendBase(metaclass=AutoRegisterMeta):
__registry_family__ = RegistryFamily("backend_type")
backend_type = None
class StorageBackend(BackendBase):
pass # Inherits BackendBase.__registry__
class ReadOnlyBackend(BackendBase):
pass # Also inherits BackendBase.__registry__
# All share the SAME registry!
assert StorageBackend.__registry__ is BackendBase.__registry__Pass SecondaryRegistry declarations through the family's authoritative
RegistryConfig. Secondary mappings are derived indexes; populate them only
through registration, never through a second manual registration path.
Set RegistryConfig.key_extractor when names must be derived. The extractor
receives (class_name, cls) and should return the semantic key. Prefer an
explicit family key attribute when the declaration can own the value directly.
Full documentation available at metaclass-registry.readthedocs.io
MIT License - see LICENSE file for details
Contributions are welcome through the repository and its issue tracker.
Developed by Tristan Simas as part of the OpenHCS project.