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 the PluginMetadata.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:
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.

Parameters:

dirname (str | Path) – root directory to scan for plugins

Return type:

None

find(*plugin_filters, **metadata_criteria)

Find plugins that match the given criteria.

Two types of criteria can be supplied to this function:

  1. plugin_filters - callables that accept a plugin and return True if the plugin if the plugin matches the criteria.

  2. metadata_criteria - key/value pairs that must all match the plugin metadata.

For example, the following block finds plugins that are subclasses of AppPlugin and are authored by Acme, 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_criteria also handled regular expressions. If the value is a re.Pattern instance, 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 that find_one returns 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 metadata id.

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.

Parameters:

id (str) – plugin metadata id

Return type:

Optional[_PluginT]

Returns:

the registered plugin or None if the plugin does not exist

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 the codex.metadata as 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 the get_codex() method.

Parameters:

codex (Codex) – the codex to register

Return type:

None

cincodex.get_codex(namespace)

Get a registered codex by its namespace.

Parameters:

namespace (str) – codex namespace

Return type:

Optional[Codex]

Returns:

the registered codex if it exists or None if it doesn’t

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)

discover(dirname, modname=None)

Recursively discover all Python modules within the directory.

Parameters:

dirname (Path) – directory to scan

Return type:

Iterator[ModuleLocation]

Returns:

a generator that yields module locations

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:

Optional[ModuleType]

Returns:

the imported module or None if 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.

__init__(id)
Parameters:

id (str) – unique plugin id.

bind(plugin)

Bind the provided plugin to this metadata.

This method should always set the plugin.__plugin_metadata__ attribute to self.

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 Finder interface with the exception that the PluginFinder accepts 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:

  1. Within a single directory, all modules should be returned or yielded prior to recursing into any nested module or directory.

  2. The first module must be the __init__ module, if it exists within the current directory being scanned.

  3. 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 modname argument or must be generated in a deterministic way if modname is 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:

Iterator[ModuleLocation]

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 import statement.

The default mechanism for loading a plugin is:

  1. Recursively scan a directory for Python modules (PluginPathFinder)

  2. Load each module discovered (PluginPathLoader)

  3. The loaded module calls Codex.register() which registers the plugin

load_module(codex, loc)

Load a module, honoring the Python import machinery, which includes:

  1. Import all missing parent modules.

  2. Registering the module in sys.modules.

For example, if the module plugins.foo.bar is being loaded, the modules plugins and plugins.foo must 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 calling Codex.register().

Parameters:

loc (ModuleLocation) – module location to import

Return type:

Optional[ModuleType]

Returns:

the imported module or None if the module could not be imported.

class cincodex.ModuleLocation

A location of Python module on disk.

__init__(root, filename, modname, is_bundle=False)
filename: Path

The Python module filename, relative to root

is_bundle: bool = False

The Python module is a bundle

modname: str

The Python module name (__name__)

root: Path

The root search directory where the module was discovered.