# 198. Python Plugins

Building extensible systems using dynamically loadable plugins in Python allows you to create modular applications where functionality can be extended without altering the core codebase. This can be achieved using several approaches like the `importlib` module for dynamic imports, `pkg_resources` for managing entry points, or even implementing a custom plugin system.

Here are some Python code snippets demonstrating how to build extensible systems with dynamically loadable plugins:

***

**1. Basic Plugin System with `importlib`**

This example shows how to load a plugin dynamically using `importlib`. Each plugin is simply a Python module that defines a function.

**Plugin Interface:**

```python
# plugin_interface.py
def plugin_function():
    raise NotImplementedError("Subclasses should implement this function.")
```

**Plugin Implementation:**

```python
# plugin_1.py
from plugin_interface import plugin_function

def plugin_function():
    print("Plugin 1 function executed!")
```

```python
# plugin_2.py
from plugin_interface import plugin_function

def plugin_function():
    print("Plugin 2 function executed!")
```

**Main Application:**

```python
import importlib

def load_plugin(plugin_name):
    plugin = importlib.import_module(plugin_name)
    plugin.plugin_function()

# Dynamically load and run plugins
load_plugin('plugin_1')  # This loads and runs plugin_1.py
load_plugin('plugin_2')  # This loads and runs plugin_2.py
```

Here, `importlib` is used to load the plugin modules by their names as strings.

***

**2. Using `pkg_resources` for Entry Points**

`pkg_resources` provides a way to define and discover plugins in Python, allowing for more complex and flexible plugin systems.

**Define an Entry Point:**

In your package’s `setup.py`, you can define entry points for your plugins.

```python
# setup.py
from setuptools import setup

setup(
    name='plugin_system',
    version='0.1',
    packages=['plugin_system'],
    entry_points={
        'myapp.plugins': [
            'plugin1 = plugin_system.plugins.plugin1',
            'plugin2 = plugin_system.plugins.plugin2',
        ]
    }
)
```

**Plugin Code:**

```python
# plugin_system/plugins/plugin1.py
def execute():
    print("Plugin 1 executed")

# plugin_system/plugins/plugin2.py
def execute():
    print("Plugin 2 executed")
```

**Load Plugins Using `pkg_resources`:**

```python
import pkg_resources

def load_plugins():
    for entry_point in pkg_resources.iter_entry_points(group='myapp.plugins'):
        plugin = entry_point.load()
        plugin.execute()

load_plugins()
```

This method allows for better separation between plugins and the main codebase, and `pkg_resources` handles discovering and loading plugins dynamically.

***

**3. Plugin System with a Plugin Manager Class**

This approach involves using a central plugin manager to register and load plugins.

**Plugin Interface:**

```python
# plugin_interface.py
class Plugin:
    def execute(self):
        pass
```

**Plugin Implementations:**

```python
# plugin_1.py
from plugin_interface import Plugin

class Plugin1(Plugin):
    def execute(self):
        print("Plugin 1 executed")

# plugin_2.py
from plugin_interface import Plugin

class Plugin2(Plugin):
    def execute(self):
        print("Plugin 2 executed")
```

**Plugin Manager:**

```python
import importlib
import os

class PluginManager:
    def __init__(self):
        self.plugins = {}

    def register_plugin(self, plugin_name, plugin_class):
        self.plugins[plugin_name] = plugin_class()

    def load_plugin(self, plugin_name):
        if plugin_name in self.plugins:
            self.plugins[plugin_name].execute()

    def discover_and_load_plugins(self, plugin_folder):
        for filename in os.listdir(plugin_folder):
            if filename.endswith('.py') and filename != 'plugin_interface.py':
                plugin_name = filename[:-3]
                plugin_module = importlib.import_module(f"{plugin_folder}.{plugin_name}")
                plugin_class = getattr(plugin_module, f"{plugin_name.capitalize()}")
                self.register_plugin(plugin_name, plugin_class)

plugin_manager = PluginManager()
plugin_manager.discover_and_load_plugins('plugins')

# Example usage
plugin_manager.load_plugin('plugin1')
plugin_manager.load_plugin('plugin2')
```

**Directory Structure:**

```
- plugin_system/
  - __init__.py
  - plugin_interface.py
  - plugins/
    - __init__.py
    - plugin1.py
    - plugin2.py
```

This approach scans a `plugins` directory for Python files, dynamically loads each one, and registers the plugin class. It allows for a more organized plugin system and easy discovery of available plugins.

***

**4. Using `importlib` and Configuration Files for Dynamic Plugin Loading**

Another approach is to use a configuration file (e.g., JSON, YAML) to specify which plugins to load.

**Plugin Configuration (config.json):**

```json
{
    "plugins": ["plugin1", "plugin2"]
}
```

**Plugin Implementations:**

```python
# plugin1.py
def execute():
    print("Plugin 1 executed")

# plugin2.py
def execute():
    print("Plugin 2 executed")
```

**Main Application:**

```python
import importlib
import json

def load_plugins(config_file):
    with open(config_file, 'r') as f:
        config = json.load(f)
        for plugin_name in config['plugins']:
            plugin = importlib.import_module(plugin_name)
            plugin.execute()

load_plugins('config.json')
```

This allows for dynamic plugin loading based on a configuration file, enabling the user to enable or disable plugins without changing the code.

***

**5. Plugin System with Callbacks**

A plugin system can also be built using callbacks, where the main application calls plugin functions.

**Plugin Interface:**

```python
# plugin_interface.py
class Plugin:
    def run(self, callback):
        raise NotImplementedError
```

**Plugin Implementations:**

```python
# plugin1.py
from plugin_interface import Plugin

class Plugin1(Plugin):
    def run(self, callback):
        print("Plugin 1 is running...")
        callback("Plugin 1 finished")

# plugin2.py
from plugin_interface import Plugin

class Plugin2(Plugin):
    def run(self, callback):
        print("Plugin 2 is running...")
        callback("Plugin 2 finished")
```

**Main Application:**

```python
import importlib

def callback(message):
    print(message)

def load_and_run_plugin(plugin_name):
    plugin = importlib.import_module(plugin_name)
    plugin_instance = plugin.Plugin1()  # Adjust for other plugins as needed
    plugin_instance.run(callback)

load_and_run_plugin('plugin1')
load_and_run_plugin('plugin2')
```

This allows for more flexible plugin systems where the main application can pass custom behavior (via a callback) to the plugins.

***

**6. Managing Plugins with a Singleton Pattern**

A plugin manager that follows the Singleton pattern ensures only one instance of the manager exists.

**Singleton Plugin Manager:**

```python
class PluginManager:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def load_plugin(self, plugin_name):
        print(f"Loading plugin: {plugin_name}")

# Usage
manager1 = PluginManager()
manager2 = PluginManager()

print(manager1 is manager2)  # Should print True
manager1.load_plugin('plugin1')
```

The `PluginManager` is a Singleton, ensuring that all parts of the program share the same instance of the plugin manager.

***

**Conclusion:**

These snippets demonstrate different ways to create a modular plugin system in Python:

1. **Basic Plugin System**: Using `importlib` to dynamically load Python modules.
2. **Entry Points with `pkg_resources`**: Using setuptools entry points to discover and load plugins.
3. **Plugin Manager Class**: Centralized management of plugins using a class.
4. **Dynamic Plugin Loading with Config**: Using configuration files to define which plugins to load.
5. **Callback-Based Plugin System**: Plugins that interact with the application via callbacks.
6. **Singleton Plugin Manager**: Ensuring a single instance of a plugin manager.

You can tailor these approaches to suit different requirements, such as plugin discovery, version management, and dynamic behavior extensions.
