SSPM 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sspm/__init__.py +5 -0
- sspm/plugin.py +110 -0
- sspm/plugin_base.py +6 -0
- sspm/pluginmanager.py +193 -0
- sspm/version.py +27 -0
- sspm-1.0.0.dist-info/METADATA +159 -0
- sspm-1.0.0.dist-info/RECORD +10 -0
- sspm-1.0.0.dist-info/WHEEL +5 -0
- sspm-1.0.0.dist-info/licenses/LICENSE +75 -0
- sspm-1.0.0.dist-info/top_level.txt +1 -0
sspm/__init__.py
ADDED
sspm/plugin.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from configparser import ConfigParser
|
|
2
|
+
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
|
|
5
|
+
# Defaults used for any [Documentation] field a plugin's info file doesn't specify.
|
|
6
|
+
_DOCUMENTATION_DEFAULTS = {
|
|
7
|
+
"Author": "Unknown",
|
|
8
|
+
"Version": "0.0",
|
|
9
|
+
"Website": "None",
|
|
10
|
+
"Copyright": "Unknown",
|
|
11
|
+
"Description": "",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Plugin:
|
|
16
|
+
"""
|
|
17
|
+
Represents a single plugin: the metadata parsed from its info file (name, version, author,
|
|
18
|
+
copyright, website, description) plus, once PluginManager has instantiated it, the
|
|
19
|
+
plugin_object itself.
|
|
20
|
+
|
|
21
|
+
Metadata is backed by a configparser.ConfigParser (see `details`). Values are read with a
|
|
22
|
+
fallback, so a plugin's info file only needs to specify what it wants to override -- the
|
|
23
|
+
fallback values themselves are never written back into `details`, so `details` reflects
|
|
24
|
+
only what was actually parsed from the info file (plus whatever `name`/`path`/etc. setters
|
|
25
|
+
have explicitly been called with).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, plugin_name, plugin_path):
|
|
29
|
+
# Used as the fallback for name/path if `details` doesn't specify Core/Name or
|
|
30
|
+
# Core/Module -- see __read().
|
|
31
|
+
self.__default_name = plugin_name
|
|
32
|
+
self.__default_path = plugin_path
|
|
33
|
+
self.__details = ConfigParser()
|
|
34
|
+
|
|
35
|
+
# Set by PluginManager once the plugin's module class has been instantiated; None until then.
|
|
36
|
+
self.plugin_object = None
|
|
37
|
+
|
|
38
|
+
def __read(self, section: str, option: str, default: str) -> str:
|
|
39
|
+
return self.__details.get(section, option, fallback=default)
|
|
40
|
+
|
|
41
|
+
def __write(self, section: str, option: str, value: str) -> None:
|
|
42
|
+
if not self.__details.has_section(section):
|
|
43
|
+
self.__details.add_section(section)
|
|
44
|
+
self.__details.set(section, option, value)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def details(self) -> ConfigParser:
|
|
48
|
+
return self.__details
|
|
49
|
+
|
|
50
|
+
@details.setter
|
|
51
|
+
def details(self, config_details: ConfigParser) -> None:
|
|
52
|
+
self.__details = config_details
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def name(self):
|
|
56
|
+
return self.__read("Core", "Name", self.__default_name)
|
|
57
|
+
|
|
58
|
+
@name.setter
|
|
59
|
+
def name(self, name):
|
|
60
|
+
self.__write("Core", "Name", name)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def path(self):
|
|
64
|
+
return self.__read("Core", "Module", self.__default_path)
|
|
65
|
+
|
|
66
|
+
@path.setter
|
|
67
|
+
def path(self, path):
|
|
68
|
+
self.__write("Core", "Module", path)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def version(self):
|
|
72
|
+
return Version(self.__read("Documentation", "Version", _DOCUMENTATION_DEFAULTS["Version"]))
|
|
73
|
+
|
|
74
|
+
@version.setter
|
|
75
|
+
def version(self, ver):
|
|
76
|
+
if isinstance(ver, Version):
|
|
77
|
+
ver = str(ver)
|
|
78
|
+
self.__write("Documentation", "Version", ver)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def author(self):
|
|
82
|
+
return self.__read("Documentation", "Author", _DOCUMENTATION_DEFAULTS["Author"])
|
|
83
|
+
|
|
84
|
+
@author.setter
|
|
85
|
+
def author(self, author):
|
|
86
|
+
self.__write("Documentation", "Author", author)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def copyright(self):
|
|
90
|
+
return self.__read("Documentation", "Copyright", _DOCUMENTATION_DEFAULTS["Copyright"])
|
|
91
|
+
|
|
92
|
+
@copyright.setter
|
|
93
|
+
def copyright(self, copyright):
|
|
94
|
+
self.__write("Documentation", "Copyright", copyright)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def website(self):
|
|
98
|
+
return self.__read("Documentation", "Website", _DOCUMENTATION_DEFAULTS["Website"])
|
|
99
|
+
|
|
100
|
+
@website.setter
|
|
101
|
+
def website(self, website):
|
|
102
|
+
self.__write("Documentation", "Website", website)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def description(self):
|
|
106
|
+
return self.__read("Documentation", "Description", _DOCUMENTATION_DEFAULTS["Description"])
|
|
107
|
+
|
|
108
|
+
@description.setter
|
|
109
|
+
def description(self, description):
|
|
110
|
+
self.__write("Documentation", "Description", description)
|
sspm/plugin_base.py
ADDED
sspm/pluginmanager.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from configparser import ConfigParser
|
|
8
|
+
|
|
9
|
+
from .plugin import Plugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PluginManager:
|
|
13
|
+
"""
|
|
14
|
+
A basic python plugin manager. Plugins live one per subdirectory of a user-specified plugin
|
|
15
|
+
folder, each with a Python module and an info file (see plugin.py) describing it.
|
|
16
|
+
|
|
17
|
+
Safe to use from multiple threads: all reads and mutations of the manager's internal plugin
|
|
18
|
+
state (import_plugins, rescan, remove_plugin, get_active_plugin, active_plugins,
|
|
19
|
+
categorized_plugins) are serialized with an internal lock, and active_plugins/
|
|
20
|
+
categorized_plugins return snapshots rather than live references, so a caller iterating one
|
|
21
|
+
won't be affected by a concurrent import_plugins()/rescan()/remove_plugin() call.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, plugin_folder: str, plugin_info_ext="info", log=logging):
|
|
25
|
+
"""
|
|
26
|
+
This is the initialization method. User must set the plugin folder location. They can also set their own
|
|
27
|
+
logging should they have their own.
|
|
28
|
+
:param plugin_folder: Base dir for plugins.
|
|
29
|
+
:param plugin_info_ext: Allows user to define a custom extension for their plugin info files.
|
|
30
|
+
:param log: Python logging.
|
|
31
|
+
"""
|
|
32
|
+
self.__logging = log
|
|
33
|
+
self.__plugin_folder = pathlib.Path(plugin_folder)
|
|
34
|
+
self.__plugin_config_ext = plugin_info_ext
|
|
35
|
+
self.__imported_plugins = dict()
|
|
36
|
+
self.__categorized_plugins = dict()
|
|
37
|
+
self.__lock = threading.Lock()
|
|
38
|
+
|
|
39
|
+
# Follows the recipe in the Python docs for importing a source file directly:
|
|
40
|
+
# https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
41
|
+
# This is a better and more flexible solution than the python path modification. This also allows for
|
|
42
|
+
# subdirectories within the provided plugin directory.
|
|
43
|
+
def __load_plugin_src(self, name: str, plugin_path: str):
|
|
44
|
+
"""
|
|
45
|
+
Import a Python source file and return the loaded module.
|
|
46
|
+
|
|
47
|
+
:param name: The name for the loaded module. It may contain `.` and even characters
|
|
48
|
+
that would normally not be allowed (e.g., `-`).
|
|
49
|
+
:param plugin_path: The full path to the source file. It may contain characters like
|
|
50
|
+
`.` or `-`.
|
|
51
|
+
:return: The imported module.
|
|
52
|
+
:raises ImportError: If the file cannot be imported (e.g., if it's not a `.py` file or
|
|
53
|
+
if it does not exist).
|
|
54
|
+
:raises Exception: Any exception that is raised while executing the module (e.g., a
|
|
55
|
+
`SyntaxError`). These are errors made by the author of the module!
|
|
56
|
+
"""
|
|
57
|
+
spec = importlib.util.spec_from_file_location(name, plugin_path)
|
|
58
|
+
if spec is None:
|
|
59
|
+
raise ImportError(f"Could not load spec for module '{name}' at: {plugin_path}")
|
|
60
|
+
module = importlib.util.module_from_spec(spec)
|
|
61
|
+
sys.modules[name] = module
|
|
62
|
+
try:
|
|
63
|
+
spec.loader.exec_module(module)
|
|
64
|
+
except FileNotFoundError as e:
|
|
65
|
+
raise ImportError(f"{e.strerror}: {plugin_path}") from e
|
|
66
|
+
return module
|
|
67
|
+
|
|
68
|
+
def __import_plugin_info(self, plugin_info_path: pathlib.Path) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Loads a single plugin from its info file and, if successful, registers it. Shared by
|
|
71
|
+
import_plugins() and rescan(); callers are expected to hold self.__lock.
|
|
72
|
+
"""
|
|
73
|
+
config_parser = ConfigParser()
|
|
74
|
+
config_parser.read(plugin_info_path)
|
|
75
|
+
try:
|
|
76
|
+
if config_parser.get("Core", "Module"):
|
|
77
|
+
module_name = config_parser.get("Core", "Module")
|
|
78
|
+
module_parent_dir = plugin_info_path.parent
|
|
79
|
+
module_path = module_parent_dir.joinpath(f"{module_name}.py").resolve().as_posix()
|
|
80
|
+
importlib.invalidate_caches()
|
|
81
|
+
module = self.__load_plugin_src(module_name, module_path)
|
|
82
|
+
if module:
|
|
83
|
+
if config_parser.get("Core", "Name"):
|
|
84
|
+
plugin = Plugin(config_parser.get("Core", "Name"), module_path)
|
|
85
|
+
plugin.details = config_parser
|
|
86
|
+
|
|
87
|
+
cls_names = [m[0] for m in inspect.getmembers(module, inspect.isclass) if
|
|
88
|
+
m[1].__module__ == module.__name__]
|
|
89
|
+
|
|
90
|
+
cls_name = [cls for cls in cls_names
|
|
91
|
+
if "PluginBase" in [x.__name__ for x in type.mro(getattr(module, cls))]]
|
|
92
|
+
|
|
93
|
+
if cls_name.__len__() != 1:
|
|
94
|
+
self.__logging.error(
|
|
95
|
+
f"Illegal action: expected exactly one plugin class in module. "
|
|
96
|
+
f"The plugin file: {module_name} contains {len(cls_name)} plugin classes.")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
_cls = getattr(module, cls_name[0])
|
|
100
|
+
plugin.plugin_object = _cls()
|
|
101
|
+
|
|
102
|
+
self.__imported_plugins[plugin.name] = plugin
|
|
103
|
+
self.__categorize_plugin(plugin)
|
|
104
|
+
|
|
105
|
+
self.__logging.info(f"{plugin.name} imported successfully.")
|
|
106
|
+
else:
|
|
107
|
+
self.__logging.warning(f"Missing Module for Plugin: {plugin_info_path.absolute().as_posix()}")
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("Plugin Config file is missing necessary parameters.")
|
|
110
|
+
except ModuleNotFoundError:
|
|
111
|
+
self.__logging.warning(f"Missing Module for Plugin: {plugin_info_path.absolute().as_posix()}")
|
|
112
|
+
|
|
113
|
+
def import_plugins(self) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Imports all plugins in the user-defined plugin directory. Each subdirectory containing a matching
|
|
116
|
+
plugin info file is expected to hold exactly one module defining exactly one PluginBase subclass;
|
|
117
|
+
plugins that don't meet that contract, or whose module can't be found, are skipped and logged
|
|
118
|
+
rather than raised.
|
|
119
|
+
|
|
120
|
+
Every matching plugin is (re)imported and its plugin_object (re)instantiated, even ones
|
|
121
|
+
already active from a previous call -- to pick up only newly-added plugins without
|
|
122
|
+
disturbing already-loaded ones, use rescan() instead.
|
|
123
|
+
"""
|
|
124
|
+
with self.__lock:
|
|
125
|
+
for plugin_info_path in pathlib.Path(self.__plugin_folder).glob(f"**/*.{self.__plugin_config_ext}"):
|
|
126
|
+
self.__import_plugin_info(plugin_info_path)
|
|
127
|
+
|
|
128
|
+
def rescan(self) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Scans the plugin directory for plugins that aren't already active and imports those,
|
|
131
|
+
leaving already-active plugins (matched by the Name in their info file) untouched -- their
|
|
132
|
+
existing plugin_object is not re-instantiated or replaced. Use this to pick up plugins
|
|
133
|
+
dropped into the plugin directory after the initial import_plugins() call, while the
|
|
134
|
+
process is still running.
|
|
135
|
+
"""
|
|
136
|
+
with self.__lock:
|
|
137
|
+
for plugin_info_path in pathlib.Path(self.__plugin_folder).glob(f"**/*.{self.__plugin_config_ext}"):
|
|
138
|
+
config_parser = ConfigParser()
|
|
139
|
+
config_parser.read(plugin_info_path)
|
|
140
|
+
name = config_parser.get("Core", "Name", fallback=None)
|
|
141
|
+
if name is not None and name in self.__imported_plugins:
|
|
142
|
+
continue
|
|
143
|
+
self.__import_plugin_info(plugin_info_path)
|
|
144
|
+
|
|
145
|
+
def __categorize_plugin(self, plugin) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Indexes a plugin under the name of each base class its plugin_object directly subclasses, so it can
|
|
148
|
+
later be looked up by category via categorized_plugins. Callers are expected to hold self.__lock.
|
|
149
|
+
:param plugin: The Plugin to index.
|
|
150
|
+
"""
|
|
151
|
+
plugin_types = [x.__name__ for x in type(plugin.plugin_object).__bases__]
|
|
152
|
+
|
|
153
|
+
for plugin_type in plugin_types:
|
|
154
|
+
if plugin_type not in self.__categorized_plugins:
|
|
155
|
+
self.__categorized_plugins[plugin_type] = {plugin.name: plugin}
|
|
156
|
+
else:
|
|
157
|
+
plugins_store = self.__categorized_plugins.get(plugin_type)
|
|
158
|
+
plugins_store[plugin.name] = plugin
|
|
159
|
+
|
|
160
|
+
def get_active_plugin(self, plugin_name: str) -> Plugin:
|
|
161
|
+
"""
|
|
162
|
+
Retrieves a plugin from the active plugins.
|
|
163
|
+
:param plugin_name: User defined name of plugin from plugin info file
|
|
164
|
+
:return: The matching Plugin, or None if no plugin with that name is active.
|
|
165
|
+
"""
|
|
166
|
+
with self.__lock:
|
|
167
|
+
return self.__imported_plugins.get(plugin_name)
|
|
168
|
+
|
|
169
|
+
def remove_plugin(self, plugin_name) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Removes a loaded plugin.
|
|
172
|
+
:param plugin_name: the name of the plugin to be removed
|
|
173
|
+
:return: None
|
|
174
|
+
:raises KeyError: if no plugin with that name is currently active.
|
|
175
|
+
"""
|
|
176
|
+
with self.__lock:
|
|
177
|
+
del self.__imported_plugins[plugin_name]
|
|
178
|
+
self.__logging.info(f"{plugin_name} removed successfully.")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def active_plugins(self):
|
|
182
|
+
"""A snapshot dict of all successfully imported plugins, keyed by plugin name."""
|
|
183
|
+
with self.__lock:
|
|
184
|
+
return dict(self.__imported_plugins)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def categorized_plugins(self):
|
|
188
|
+
"""
|
|
189
|
+
A snapshot dict mapping each PluginBase subclass name to a dict of the plugins (keyed by
|
|
190
|
+
plugin name) whose plugin_object directly subclasses it.
|
|
191
|
+
"""
|
|
192
|
+
with self.__lock:
|
|
193
|
+
return {plugin_type: dict(plugins) for plugin_type, plugins in self.__categorized_plugins.items()}
|
sspm/version.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
|
|
4
|
+
_SSPM_VERSION_CLS = namedtuple("_SSPM_VERSION_CLS", "major minor bugfix pre post dev")
|
|
5
|
+
|
|
6
|
+
version_tuple = _SSPM_VERSION_CLS(1, 0, 0, None, None, None)
|
|
7
|
+
|
|
8
|
+
version = "{0.major:d}.{0.minor:d}.{0.bugfix:d}".format(version_tuple)
|
|
9
|
+
if version_tuple.pre is not None:
|
|
10
|
+
version += version_tuple.pre
|
|
11
|
+
if version_tuple.post is not None:
|
|
12
|
+
version += ".post{0.post:d}".format(version_tuple)
|
|
13
|
+
if version_tuple.dev is not None:
|
|
14
|
+
version += ".dev{0.dev:d}".format(version_tuple)
|
|
15
|
+
|
|
16
|
+
info = """\
|
|
17
|
+
Summary of the SSPM configuration
|
|
18
|
+
---------------------------------
|
|
19
|
+
|
|
20
|
+
SSPM %(sspm)s
|
|
21
|
+
Python %(python)s
|
|
22
|
+
Platform %(platform)s
|
|
23
|
+
""" % {
|
|
24
|
+
'sspm': version,
|
|
25
|
+
'python': sys.version,
|
|
26
|
+
'platform': sys.platform,
|
|
27
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SSPM
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SSPM: a simple, hands-off Python plugin manager
|
|
5
|
+
Author-email: Steven Nix <stevencnix@gmail.com>
|
|
6
|
+
License: PolyForm-Noncommercial-1.0.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/stevencnix/sspm
|
|
8
|
+
Project-URL: Repository, https://github.com/stevencnix/sspm
|
|
9
|
+
Project-URL: Changelog, https://github.com/stevencnix/sspm/blob/develop/CHANGELOG
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: packaging
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Super Simple Plugin Manager - SSPM
|
|
26
|
+
|
|
27
|
+
## About SSPM
|
|
28
|
+
|
|
29
|
+
Super Simple Plugin Manager - SSPM is a lightweight, hands-off Python plugin manager. Drop a
|
|
30
|
+
plugin's code and a small `.info` file describing it into a plugins directory, and SSPM discovers,
|
|
31
|
+
imports, categorizes, and instantiates it for you — no extra registration step needed.
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3.10+
|
|
36
|
+
- [packaging](https://pypi.org/project/packaging/) (installed automatically as a dependency)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
The easiest way to install is to use pip:
|
|
41
|
+
|
|
42
|
+
pip install SSPM
|
|
43
|
+
|
|
44
|
+
or if you have cloned the repo:
|
|
45
|
+
|
|
46
|
+
cd <path to repo>
|
|
47
|
+
pip install .
|
|
48
|
+
|
|
49
|
+
## Writing a Plugin
|
|
50
|
+
|
|
51
|
+
A plugin is a folder containing two files: a Python module with your plugin's code, and an `.info` file
|
|
52
|
+
(a standard `configparser`/INI file) describing it. Each module must define **exactly one** class that
|
|
53
|
+
subclasses `PluginBase` (or a subclass of it) — SSPM uses that to find and categorize your plugin.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
plugins/
|
|
57
|
+
└── add_plugin/
|
|
58
|
+
├── add_plugin.py
|
|
59
|
+
└── add.info
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`add_plugin.py`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from sspm import PluginBase
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AddPlugin(PluginBase):
|
|
69
|
+
|
|
70
|
+
def operation(self, x, y):
|
|
71
|
+
return x + y
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`add.info`:
|
|
75
|
+
|
|
76
|
+
```ini
|
|
77
|
+
[Core]
|
|
78
|
+
Name = Add Plugin
|
|
79
|
+
Module = add_plugin
|
|
80
|
+
|
|
81
|
+
[Documentation]
|
|
82
|
+
Author = Your Name
|
|
83
|
+
Version = 1.0.0
|
|
84
|
+
Website = None
|
|
85
|
+
Description = Plugin performing basic add operation
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`Core.Name` and `Core.Module` are required (`Module` is the module filename, without the `.py`
|
|
89
|
+
extension). Everything under `[Documentation]` is optional — SSPM fills in sensible defaults
|
|
90
|
+
(`Unknown`, `0.0`, etc.) for anything you leave out.
|
|
91
|
+
|
|
92
|
+
You can subclass `PluginBase` further to create your own plugin categories (e.g. a
|
|
93
|
+
`CalculatorPluginBase`) and group related plugins together — see
|
|
94
|
+
[`examples/`](examples) for a full working example with multiple plugin types.
|
|
95
|
+
|
|
96
|
+
## Basic Usage
|
|
97
|
+
|
|
98
|
+
1. Initialize the plugin manager, pointing it at the folder containing your plugins:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sspm import PluginManager
|
|
102
|
+
|
|
103
|
+
plugin_manager = PluginManager(plugin_folder="./plugins")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
2. Import the plugins in the plugins directory:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
plugin_manager.import_plugins()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
3. Get a specific imported plugin by the name from its `.info` file:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
plugin = plugin_manager.get_active_plugin("Add Plugin")
|
|
116
|
+
result = plugin.plugin_object.operation(1, 2)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
or get all active plugins:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
plugins = plugin_manager.active_plugins
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
or get plugins grouped by their `PluginBase` category:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
calculator_plugins = plugin_manager.categorized_plugins.get("CalculatorPluginBase")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A `Plugin` object exposes the metadata from its `.info` file (`name`, `version`, `author`,
|
|
132
|
+
`website`, `copyright`, `description`) as well as `plugin_object`, the instantiated plugin class
|
|
133
|
+
itself.
|
|
134
|
+
|
|
135
|
+
## Runtime Plugins
|
|
136
|
+
|
|
137
|
+
`PluginManager` is meant to be used for plugins that come and go while your process is running,
|
|
138
|
+
not just a one-time scan at startup:
|
|
139
|
+
|
|
140
|
+
- **Adding a plugin after startup**: drop a new plugin's folder into the plugin directory, then
|
|
141
|
+
call `plugin_manager.rescan()`. Unlike `import_plugins()`, `rescan()` only loads plugins that
|
|
142
|
+
aren't already active — it won't re-instantiate or replace a plugin that's already loaded.
|
|
143
|
+
- **Removing a plugin**: `plugin_manager.remove_plugin("Plugin Name")` (raises `KeyError` if no
|
|
144
|
+
plugin with that name is active).
|
|
145
|
+
- **Thread safety**: `PluginManager` is safe to use from multiple threads. All reads and writes go
|
|
146
|
+
through an internal lock, and `active_plugins`/`categorized_plugins` return a snapshot rather
|
|
147
|
+
than a live reference, so code iterating one of them won't be affected by a concurrent
|
|
148
|
+
`import_plugins()`, `rescan()`, or `remove_plugin()` call on another thread.
|
|
149
|
+
|
|
150
|
+
## More Examples
|
|
151
|
+
|
|
152
|
+
See the [`examples/`](examples) directory for a runnable calculator example with `Add` and
|
|
153
|
+
`Subtract` plugins, and [`CHANGELOG`](CHANGELOG) for release history.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
SSPM is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE) — free to use, modify,
|
|
158
|
+
and distribute for any noncommercial purpose. Commercial use requires a separate license from the
|
|
159
|
+
author.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sspm/__init__.py,sha256=UFO8Z4la54nrwuULkcNcbylwfOIfwIAxvhNPatlpiKI,157
|
|
2
|
+
sspm/plugin.py,sha256=_lewbtSt3zFMmswXmADMApdm1Ep3SucJ-zKlawLLqY0,3654
|
|
3
|
+
sspm/plugin_base.py,sha256=5fcXZkhIFm9TLnLsdiD2C_lykeB0B6GvBqrDsQ_tIfQ,235
|
|
4
|
+
sspm/pluginmanager.py,sha256=eQQjVW6Fw4puC0Gur24ggrpkrlWQB-vxoouWIQACHOo,9440
|
|
5
|
+
sspm/version.py,sha256=U0uwb3JvG1jJIXdCHkdc1Ue6wYSbF_obwNVRfdUP-HU,743
|
|
6
|
+
sspm-1.0.0.dist-info/licenses/LICENSE,sha256=YAVWzDDyrpBjlrKa69ZGW6HUyVChzZawY2T7b-6RdVU,4639
|
|
7
|
+
sspm-1.0.0.dist-info/METADATA,sha256=QQV0HkfVBDd63xYNSJ_tO5i8pQmIPPyIB-vDAeLAC0A,5109
|
|
8
|
+
sspm-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
sspm-1.0.0.dist-info/top_level.txt,sha256=x80ciy1FOK_VHkApkgYSVIkkw9jF288FnVL6fDrL4q8,5
|
|
10
|
+
sspm-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
Required Notice: Copyright Steven Nix (https://github.com/stevencnix/sspm)
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sspm
|