# cincodex
[](https://gitlab.com/ameily/cincodex/-/pipelines/main/latest)
[](https://gitlab.com/ameily/cincodex/-/pipelines/main/latest)
[](https://cincodex.readthedocs.io/en/latest/)
[](https://gitlab.com/ameily/cincodex/-/releases)
[](https://www.python.org/downloads/release/python-31010/)
[](/ameily/cincodex/-/blob/main/LICENSE)
Cincodex is a simple, flexible, and unopinionated plugin system for Python projects. Cincodex is designed for both applications and libraries that wish to be extensible at runtime. Plugins can be any Python object including a class, a function, or an instance of an object. The following is a very basic example showing that the application creates a Codex, which is the plugin system, then discovers all available plugins within the `./plugins` directory, and the `./plugins/foo/bar.py` source file registers a function with the codex.
```python
# app.py
from cincodex import Codex, register_codex
codex = Codex('my_app')
register_codex(codex)
# discover all plugins
codex.discover_plugins('./plugins')
# after this, the `foo.bar` plugin is available and we can call it.
plugin = codex.get('foo.bar')
plugin()
# 'Hello world!'
# ./plugins/foo/bar.py
from app import codex
@codex.register
@codex.metadata(id='foo.bar')
def foo_bar_plugin():
print('Hello world!')
```
Cincodex has been tested on Linux and Windows systems but should support other operating systems as well. Cincodex supports Python 3.10 and newer.
## Installation
```bash
pip install cincodex
```
## Features
* Cincodex is designed to work with multiple Python packages within a single project. For example, an application can have its own plugin system a multiple dependencies can have their own plugin system and they will not interfere with each other.
* Cincodex automatically discovers and registers all available plugins within one or more root directories.
* The method of finding and loading plugins and registering their metadata are all extensible within cincodex. Easily customize the plugin metadata and change how plugins are loaded.
* The `import` statement and machinery continues to work properly for discovered plugins, supporting both relative and absolute imports within plugins.
* A plugin is anything: a class, a function, or an object instance. Cincodex works seamlessly with all plugin types.
### A more complete example
The following is a cincodex plugin system where:
* A plugin base class that all plugin must implement. Each plugin says "hello" in a different language.
* A custom plugin metadata class
* Loading plugin from multiple directories
**`hello.py`**
This module defines the plugin metadata, plugin base class, and codex.
```python
#
# hello.py
#
from cincodex import Codex, PluginMetadata, register_codex
# First we create a custom plugin metadata class, inheriting from cincodex `PluginMetadata`
class HelloPluginMetadata(PluginMetadata):
def __init__(self, id: str, *, lang: str):
super().__init__(id)
self.lang = lang
# Our plugin system will be class based. So, next we create a base class that all plugins must
# inherit from and implement the `say_hello` method.
class HelloPlugin:
# __plugin_metadata__ is always set by cincodex with the call to `Codex.register`. We include
# the field here so that the IDE / type checking knows that __plugin_metadata__ is a class
# attribute.
__plugin_metadata__: HelloPluginMetadata
def say_hello(self) -> None:
raise NotImplementedError()
# Create and register the codex
codex: Codex[type[HelloPlugin], HelloPluginMetadata] = Codex('hello', HelloPluginMetadata)
register_codex(codex)
```
**`builtin_plugins/english.py`**
Defines a plugin that says "hello" in English.
```python
#
# ./builtin_plugins/english.py
#
from hello import HelloPlugin, codex
@codex.register
@codex.metadata('lang.english', lang='en-us')
class EnglishHello(HelloPlugin):
def say_hello(self):
name = input('What is your name? > ')
print('Hello,', name)
```
**`./contrib_plugins/german.py`**
Defines a plugin that says "hello" in German.
```python
#
# ./contrib_plugins/german.py
#
from hello import HelloPlugin
from cincodex import get_codex
# We registered the codex so we don't technically need to import it
codex = get_codex('hello')
@codex.register
@codex.metadata('lang.german', lang='de')
class GermanLang(HelloPlugin):
def say_hello(self):
name = input('Wie heissen sie? > ')
print('Guten tag,', name)
```
**`app.py`**
Application script that discovers both builtin and contributed plugins and runs each sequentially.
```python
#
# app.py
#
if __name__ == '__main__':
from hello import HelloPluginMetadata, codex
# Discover builtin and contributed plugins
codex.discover_plugins('./builtin_plugins')
codex.discover_plugins('./contrib_plugins')
# iterate over all available plugins
for plugin_cls in codex:
# `plugin_cls` is a type[HelloPlugin]
# get the plugin metadata
metadata = HelloPluginMetadata.get(plugin_cls)
# our codex registers plugin *classes*, so we need to first instantiate an instance of this
# plugin
plugin = plugin_cls()
print(f'Running plugin: {metadata.id} (lang: {metadata.lang})')
plugin.say_hello()
print()
```
In this example, running `python app.py` will output the following:
```
$ python app.py
Running plugin: lang.english (lang: en-us)
What is your name? > Adam
Hello, Adam
Running plugin: lang.german (lang: de)
Wie heissen sie? > Wolfgang
Guten tag, Wolfgang
```
This example is available in the `example_app` directory.
### API Documentation
Cincodex API documentation is generated by Sphinx and hosted on [Read the Docs](https://cincodex.readthedocs.io/en/latest/). The API documentation includes more detailed and complex examples and information on how to extend cincodex.
## License
Cincodex is licensed under the MIT permissive license.
<!--
cspell:ignore heissen Guten
-->