diff --git a/example.py b/example.py new file mode 100644 index 0000000..883885f --- /dev/null +++ b/example.py @@ -0,0 +1,21 @@ +from xtendr.xtendr import XtendR + +if __name__ == "__main__": + """Example usage of the PluginSystem. + + Example: + >>> system = XtendR() + >>> system.attach("example_plugin") + Attached plugin 'example_plugin'. + >>> system.run("example_plugin") + ExamplePlugin is running! + >>> system.stop("example_plugin") + ExamplePlugin has stopped! + >>> system.detach("example_plugin") + Detached plugin 'example_plugin'. + """ + system = XtendR() + system.attach("example_plugin") # Assuming 'example_plugin/plugin_info.json' exists + system.run("example_plugin") + system.stop("example_plugin") + system.detach("example_plugin") \ No newline at end of file diff --git a/setup.py b/setup.py index e69de29..43ff109 100644 --- a/setup.py +++ b/setup.py @@ -0,0 +1,19 @@ +if __name__ == "__main__": + from setuptools import setup, find_packages + + setup( + name="XtendR", + version="0.0.1", + packages=find_packages(), + install_requires=[], + author="Jan Lerking", + author_email="", + description="A modular plugin system for Python.", + url="", + classifiers=[ + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.12', + ) \ No newline at end of file diff --git a/test_plugin/example_plugin.json b/test_plugin/example_plugin.json new file mode 100644 index 0000000..cb07c03 --- /dev/null +++ b/test_plugin/example_plugin.json @@ -0,0 +1,6 @@ +{ + "name": "Example Plugin", + "version": "1.0", + "module": "example_plugin", + "description": "An example plugin that prints a message." +} diff --git a/test_plugin/example_plugin.py b/test_plugin/example_plugin.py index 352f00e..69da42c 100644 --- a/test_plugin/example_plugin.py +++ b/test_plugin/example_plugin.py @@ -1,6 +1,17 @@ -from plugin_system import PluginInterface +from xtendr import XtendRBase -class ExamplePlugin(PluginInterface): - def execute(self, *args, **kwargs): - print("ExamplePlugin executed with args:", args, "and kwargs:", kwargs) - return "Execution complete!" \ No newline at end of file +class ExamplePlugin(XtendRBase): + """Example plugin implementation. + + Example: + >>> plugin = ExamplePlugin() + >>> plugin.run() + ExamplePlugin is running! + >>> plugin.stop() + ExamplePlugin has stopped! + """ + def run(self): + print("ExamplePlugin is running!") + + def stop(self): + print("ExamplePlugin has stopped!") diff --git a/xtendr/extendr_manager.py b/xtendr/extendr_manager.py deleted file mode 100644 index 6a794c2..0000000 --- a/xtendr/extendr_manager.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import importlib.util -from abc import ABC, abstractmethod - -# Define the interface that all plugins must implement -class PluginInterface(ABC): - @abstractmethod - def execute(self, *args, **kwargs): - """ - Execute the plugin's functionality. - """ - pass - - -class PluginManager: - def __init__(self, plugins_dir="plugins"): - """ - Initialize the plugin manager. - - :param plugins_dir: Directory containing plugin modules. - """ - self.plugins_dir = plugins_dir - self.plugins = {} - - def load_plugins(self): - """ - Load all plugins from the plugins directory. - """ - if not os.path.exists(self.plugins_dir): - os.makedirs(self.plugins_dir) - print(f"Created plugins directory at {self.plugins_dir}.") - return - - for filename in os.listdir(self.plugins_dir): - if filename.endswith(".py"): - plugin_name = os.path.splitext(filename)[0] - plugin_path = os.path.join(self.plugins_dir, filename) - - spec = importlib.util.spec_from_file_location(plugin_name, plugin_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Check if the module contains a valid plugin - for attr_name in dir(module): - attr = getattr(module, attr_name) - if isinstance(attr, type) and issubclass(attr, PluginInterface) and attr is not PluginInterface: - self.plugins[plugin_name] = attr() - print(f"Loaded plugin: {plugin_name}") - - def execute_plugin(self, plugin_name, *args, **kwargs): - """ - Execute a plugin by name. - - :param plugin_name: Name of the plugin to execute. - """ - plugin = self.plugins.get(plugin_name) - if plugin: - return plugin.execute(*args, **kwargs) - else: - print(f"Plugin '{plugin_name}' not found.") - - -# Example of how the PluginManager would be used -if __name__ == "__main__": - manager = PluginManager() - manager.load_plugins() - - # Execute a plugin (replace 'example_plugin' with your actual plugin name) - result = manager.execute_plugin('example_plugin', param1="Hello", param2="World") - print("Result:", result) \ No newline at end of file diff --git a/xtendr/registry.py b/xtendr/registry.py deleted file mode 100644 index e69de29..0000000 diff --git a/xtendr/xtendr.py b/xtendr/xtendr.py index e69de29..96f6843 100644 --- a/xtendr/xtendr.py +++ b/xtendr/xtendr.py @@ -0,0 +1,109 @@ +import importlib +import sys +import os +import json +from abc import ABC, abstractmethod + +class XtendRBase(ABC): + """Abstract base class for all plugins. + + Example: + >>> class TestPlugin(XtendRBase): + ... def run(self): + ... print("Running TestPlugin") + ... def stop(self): + ... print("Stopping TestPlugin") + + >>> plugin = TestPlugin() + >>> plugin.run() + Running TestPlugin + >>> plugin.stop() + Stopping TestPlugin + """ + @abstractmethod + def run(self): + pass + + @abstractmethod + def stop(self): + pass + +class XtendR: + """Plugin system to manage plugins. + + Example: + >>> system = XtendRSystem() + >>> system.attach("example_plugin") # Assuming 'example_plugin/plugin_info.json' exists + >>> system.run("example_plugin") + ExamplePlugin is running! + >>> system.stop("example_plugin") + ExamplePlugin has stopped! + >>> system.detach("example_plugin") + Detached plugin 'example_plugin'. + """ + def __init__(self): + self.plugins = {} + + def attach(self, name: str) -> None: + """Dynamically load a plugin from its folder.""" + if name in self.plugins: + print(f"Plugin '{name}' is already attached.") + return + + plugin_path = os.path.join(os.getcwd(), name) + info_path = os.path.join(plugin_path, "plugin_info.json") + + if not os.path.isdir(plugin_path) or not os.path.isfile(info_path): + print(f"Failed to attach plugin '{name}', folder or info file not found.") + return + + try: + with open(info_path, "r", encoding="utf-8") as f: + plugin_info = json.load(f) + module_name = plugin_info.get("module") + class_name = plugin_info.get("class") + if not module_name or not class_name: + print(f"Plugin '{name}' info file is missing 'module' or 'class' key.") + return + + sys.path.insert(0, plugin_path) + module = importlib.import_module(module_name) + plugin_class = getattr(module, class_name) + instance = plugin_class() + + if not isinstance(instance, XtendRBase): + print(f"Plugin '{name}' does not inherit from PluginBase.") + return + + self.plugins[name] = { + 'instance': instance, + 'running': False, + 'info': plugin_info + } + print(f"Attached plugin '{name}'.") + except (ModuleNotFoundError, json.JSONDecodeError, AttributeError) as e: + print(f"Failed to attach plugin '{name}': {e}") + + def run(self, name: str, *args, **kwargs): + """Run the plugin's 'run' method if available.""" + if name in self.plugins: + self.plugins[name]['running'] = True + return self.plugins[name]['instance'].run(*args, **kwargs) + print(f"Plugin '{name}' not found or has no 'run' method.") + + def stop(self, name: str) -> None: + """Stop the plugin if it's running.""" + if name in self.plugins and self.plugins[name]['running']: + self.plugins[name]['running'] = False + self.plugins[name]['instance'].stop() + else: + print(f"Plugin '{name}' is not running.") + + def detach(self, name: str) -> None: + """Unload a plugin.""" + if name in self.plugins: + del self.plugins[name] + sys.modules.pop(name, None) + print(f"Detached plugin '{name}'.") + else: + print(f"Plugin '{name}' is not attached.")