cincodex api
Codex
- class cincodex.Codex
A plugin system that maintains the list of available plugins.
A codex is the primarily interface for both registering plugins and retrieving available plugins. Typically the application will create a codex that an extension will import and then register the plugin with, similar to the following:
# app.py from cincodex import Codex, register_codex codex = Codex('app') register_codex(codex) codex.discover_plugins('./plugins') # plugins/foo/bar.py from app import codex @codex.register # register the plugin and metadata @codex.metadata(id='foo.bar') # plugin metadata def foo_bar_plugin(): print('hello world!')
The codex can also be retrieve with the
get_codex()function:# plugins/foo/bar.py from cincodex import get_codex codex = get_codex('app') @codex.register @codex.metadata(id='foo.bar') def foo_bar_plugin(): print('hello world!')
Plugins can be any Python object: a function, a class, or an object instance. Each plugin must have metadata that describe it, which includes at the very least a unique
id. Plugin metadata can be retrieved using thePluginMetadata.get()method:plugin = codex.get('foo.bar') # plugin is the `foo_bar_plugin` function metadata = PluginMetadata.get(plugin) # metadata is the metadata created within the `codex.metadata` decorator # or, optionally, the metadata is stored in the plugin's __plugin_metadata__ attribute metadata = plugin.__plugin_metadata__
- __init__(namespace, metadata=None, finder=None, loader=None)
- Parameters:
namespace (
str) – codex namespacemetadata (
Optional[type[_PluginMetadataT]]) – plugin metadata class orPluginMetadataif not specified (default:None)finder (
Optional[PluginFinder]) – the plugin finder or the defaultPluginPathFinderif not specified (default:None)loader (
Optional[PluginLoader]) – the plugin loader or the defaultPluginPathLoaderif not specified (default:None)
- discover_plugins(dirname, modname=None)
Discover and register all plugins.
This method uses the plugin finder and loader to find all plugins within a directory and then registers them with the codex.
- find(*plugin_filters, **metadata_criteria)
Find plugins that match the given criteria.
Two types of criteria can be supplied to this function:
plugin_filters- callables that accept a plugin and returnTrueif the plugin if the plugin matches the criteria.metadata_criteria- key/value pairs that must all match the plugin metadata.
For example, the following block finds plugins that are subclasses of
AppPluginand are authored byAcme, Inc.:class AppPlugin: pass class AppPluginMetadata(PluginMetadata): def __init__(self, id: str, author: str, version: str): super().__init__(id) self.author = author self.version = version codex = Codex('app', AppPluginMetadata) # ... after registering plugins ... codex.find(lambda plugin: issubclass(plugin, AppPlugin), author='Acme, Inc.')
The
metadata_criteriaalso handled regular expressions. If the value is are.Patterninstance,re.Pattern.match()is called to check if the attribute matches.- Parameters:
plugin_filter – list of callables that filter based on plugins
metadata_criteria – dictionary of key/value pairs that the plugin metadata must have
- Return type:
list[_PluginT]- Returns:
the list of plugins that match all filters
- find_one(*plugin_filters, **metadata_criteria)
Find the first plugin that matches all criteria.
This method accepts the same arguments with the same semantics as
find(). The difference is thatfind_onereturns the first plugin that matches all the criteria rather than returning all plugins that match the criteria. This method is useful when finding a plugin based on some unique combination of criteria other than the plugin metadataid.- Parameters:
plugin_filter – list of callables that filter based on plugins
metadata_criteria – dictionary of key/value pairs that the plugin metadata must have
- Return type:
Optional[_PluginT]- Returns:
the list of plugins that match all filters
- get(id)
Get a plugin by its unique id.
- register(plugin)
Register a plugin.
This is typically used as a decorator for function and class plugins but can be called directly with the plugin to register for object instance plugins. For example:
codex = Codex('app') # Class plugin @codex.register @codex.metadata(id='class_plugin') class Plugin: pass # Function plugin @codex.register @codex.metadata(id='func_plugin') def func_plugin(): pass # Object instance plugin class MyPlugin: pass obj_plugin = MyPlugin() codex.register(codex.metadata(id='object_plugin').bind(obj_plugin))
The plugin being registered must have metadata bound to it via
PluginMetadata.bind()or using thecodex.metadataas a decorator.- Parameters:
plugin (
T) – plugin to register- Return type:
T
- cincodex.register_codex(codex)
Register a new codex. A codex is registered by its
namespace, which must be unique. Registered codexes can be retrieved using theget_codex()method.
Plugin Finders and Loaders
- class cincodex.PluginPathFinder
Recursively scan directories for Python modules that may contain plugins.
Bundle modules can have a configurable stem and extension. By default, with the stem name of
__bundle__and allowed extensions of['.py'], bundle modules must have the filename of__bundle__.py.- __init__(bundle_name='__bundle__', extensions=['.py'], exclude=['__pycache__', '.git'], follow_symlinks=False)
- Parameters:
bundle_name (
str) – the stem portion of the filename for a bundle plugin (default:'__bundle__')extensions (
list[str]) – list of filename extensions that may contain plugins (default:['.py'])exclude (
list[str]) – file and directory names to exclude (default:['__pycache__', '.git'])follow_symlinks (
bool) – recurse into symlink directories and include symlink files (default:False)
- class cincodex.PluginPathLoader
Plugin loader implementation that mirrors the Python import machinery.
- load_module(codex, loc)
Load a module.
- Parameters:
loc (
ModuleLocation) – module location- Return type:
- Returns:
the imported module or
Noneif the module could not be imported
Plugin Metadata
- class cincodex.PluginMetadata
Base class for plugin metadata.
Applications should subclass this with additional metadata fields as necessary. At a minimum, the plugin metadata must have a unique
id.- bind(plugin)
Bind the provided plugin to this metadata.
This method should always set the
plugin.__plugin_metadata__attribute toself.- Parameters:
plugin (
_PluginT) – the plugin to bind to this metadata to- Return type:
_PluginT- Returns:
the plugin
- classmethod get(plugin)
Get the metadata for a plugin.
- Parameters:
plugin (
_PluginT) – the plugin- Return type:
_PluginMetadataT- Returns:
the metadata
Base Classes
These base classes can be subclassed and customized for applications that need to change some of
the default behavior of cincodex and, specifically, the Codex.
- class cincodex.PluginFinder
Abstract base class for plugin finders.
A plugin finder discovers Python modules that may contain potential plugins. The interface is similar to the Python import machinery
Finderinterface with the exception that thePluginFinderaccepts a directory to search for module source files rather than a single module name.- discover(dirname, modname=None)
Discover all Python modules within the root directory.
This method may return a list or can be a generator that yields module locations. This method must produce module locations according to several rules:
Within a single directory, all modules should be returned or yielded prior to recursing into any nested module or directory.
The first module must be the
__init__module, if it exists within the current directory being scanned.When a bundle module is discovered, no further scanning within that directory and all nested directories must stop. Bundle modules must not contain any nested modules.
The directory must have a unique root module name that is part of each returned module location, this is either the
modnameargument or must be generated in a deterministic way ifmodnameis empty.Plugins that are defined across multiple Python source files are bundle modules. Bundle modules have a special filename, which is
__bundle__by default, and, when encountered, must stop all scanning in the current directory and nested directories. A bundle module may be similar to the following:# file: __bundle__.py from app import codex from .lib import do_stuff @codex.register @codex.metadata(id='my.plugin') def my_plugin(): do_stuff()
Subclasses must implement this method.
- Parameters:
dirname (
Path) – root directory to scan for Python modules- Return type:
- Returns:
an iterator over discovered module locations
- class cincodex.PluginLoader
Abstract base class for plugin loaders.
A plugin loader accepts a Python module location and loads it. Loading the module should honor the Python import machinery so that relative and absolute imports work as expected, as if the module were imported using the
importstatement.The default mechanism for loading a plugin is:
Recursively scan a directory for Python modules (
PluginPathFinder)Load each module discovered (
PluginPathLoader)The loaded module calls
Codex.register()which registers the plugin
- load_module(codex, loc)
Load a module, honoring the Python import machinery, which includes:
Import all missing parent modules.
Registering the module in
sys.modules.
For example, if the module
plugins.foo.baris being loaded, the modulespluginsandplugins.foomust first be loaded if they are not loaded already. This is to ensure that relative and absolute imports continue to work for plugin modules.Loaded modules must have their plugins registered with the provided
codex. This can be done automatically by plugins registering themselves on import or can be done manually within this plugin loader by callingCodex.register().- Parameters:
loc (
ModuleLocation) – module location to import- Return type:
- Returns:
the imported module or
Noneif the module could not be imported.