From 9a41df6c576478d451447e7485164d68c0110e07 Mon Sep 17 00:00:00 2001 From: Jan Lerking Date: Fri, 15 Nov 2024 11:42:29 +0100 Subject: [PATCH 1/8] Initial checkin #1 #2 #3. /JL --- setup.py | 0 xtendr/__init__.py | 1 + xtendr/core.py | 0 xtendr/registry.py | 0 xtendr/xtendr.py | 0 5 files changed, 1 insertion(+) create mode 100644 setup.py create mode 100644 xtendr/__init__.py create mode 100644 xtendr/core.py create mode 100644 xtendr/registry.py create mode 100644 xtendr/xtendr.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/xtendr/__init__.py b/xtendr/__init__.py new file mode 100644 index 0000000..dbb9a62 --- /dev/null +++ b/xtendr/__init__.py @@ -0,0 +1 @@ +import xtendr \ No newline at end of file diff --git a/xtendr/core.py b/xtendr/core.py new file mode 100644 index 0000000..e69de29 diff --git a/xtendr/registry.py b/xtendr/registry.py new file mode 100644 index 0000000..e69de29 diff --git a/xtendr/xtendr.py b/xtendr/xtendr.py new file mode 100644 index 0000000..e69de29 From 12fb4e30052918abbafbcecd8527283b9291ac9a Mon Sep 17 00:00:00 2001 From: Lerking Date: Fri, 6 Dec 2024 18:27:11 +0100 Subject: [PATCH 2/8] Update xtendr/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 plugcore.py --- xtendr/core.py | 0 ... \"\"\" Execute the plugcore.py" | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+) delete mode 100644 xtendr/core.py create mode 100644 "xtendr/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 plugcore.py" diff --git a/xtendr/core.py b/xtendr/core.py deleted file mode 100644 index e69de29..0000000 diff --git "a/xtendr/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 plugcore.py" "b/xtendr/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 plugcore.py" new file mode 100644 index 0000000..6a794c2 --- /dev/null +++ "b/xtendr/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 plugcore.py" @@ -0,0 +1,70 @@ +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 From ea45973df2e82af957af93825161f480effede4c Mon Sep 17 00:00:00 2001 From: Lerking Date: Fri, 6 Dec 2024 18:30:03 +0100 Subject: [PATCH 3/8] Add test_plugin/example_plugin.py --- test_plugin/example_plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 test_plugin/example_plugin.py diff --git a/test_plugin/example_plugin.py b/test_plugin/example_plugin.py new file mode 100644 index 0000000..352f00e --- /dev/null +++ b/test_plugin/example_plugin.py @@ -0,0 +1,6 @@ +from plugin_system import PluginInterface + +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 From 1567e8fd35a327c85db1ec6ab3df78fb8211782f Mon Sep 17 00:00:00 2001 From: Lerking Date: Fri, 6 Dec 2024 18:31:46 +0100 Subject: [PATCH 4/8] Update xtendr/extendr_manager.py --- .../extendr_manager.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "xtendr/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 plugcore.py" => xtendr/extendr_manager.py (100%) diff --git "a/xtendr/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 plugcore.py" b/xtendr/extendr_manager.py similarity index 100% rename from "xtendr/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 plugcore.py" rename to xtendr/extendr_manager.py From 18e8ad5e629a0244a71ade3e60e4c50135147a7c Mon Sep 17 00:00:00 2001 From: Lerking Date: Thu, 20 Feb 2025 21:13:17 +0100 Subject: [PATCH 5/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b8672c..5dad75d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# xtendr +# XtendR A python 3 extension system to ease the use of plugins. \ No newline at end of file From a6549544e4c4cb64299ecc5109c43fa8897e72df Mon Sep 17 00:00:00 2001 From: Lerking Date: Thu, 20 Feb 2025 22:10:43 +0100 Subject: [PATCH 6/8] Add xtendr-icon.svg --- xtendr-icon.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 xtendr-icon.svg diff --git a/xtendr-icon.svg b/xtendr-icon.svg new file mode 100644 index 0000000..132a9f4 --- /dev/null +++ b/xtendr-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From 45520fc41265eb802d55969134ac33e0252597a4 Mon Sep 17 00:00:00 2001 From: Lerking Date: Fri, 21 Feb 2025 15:45:28 +0100 Subject: [PATCH 7/8] Update xtendr-icon.svg --- xtendr-icon.svg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xtendr-icon.svg b/xtendr-icon.svg index 132a9f4..61d3496 100644 --- a/xtendr-icon.svg +++ b/xtendr-icon.svg @@ -1,9 +1,9 @@ - - - - - - \ No newline at end of file + + + + + + From ea9ad1702b7a7b2fe1bb98bde196be833ddb5570 Mon Sep 17 00:00:00 2001 From: Jan Lerking Date: Fri, 21 Feb 2025 17:08:46 +0100 Subject: [PATCH 8/8] Updated plugin system. /JL --- example.py | 21 ++++++ setup.py | 19 ++++++ test_plugin/example_plugin.json | 6 ++ test_plugin/example_plugin.py | 21 ++++-- xtendr/extendr_manager.py | 70 -------------------- xtendr/registry.py | 0 xtendr/xtendr.py | 109 ++++++++++++++++++++++++++++++++ 7 files changed, 171 insertions(+), 75 deletions(-) create mode 100644 example.py create mode 100644 test_plugin/example_plugin.json delete mode 100644 xtendr/extendr_manager.py delete mode 100644 xtendr/registry.py 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.")