pluginify 0.0.0.post1.dev0__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.
- pluginify/__init__.py +11 -0
- pluginify/_version.py +7 -0
- pluginify/commandline_typer.py +258 -0
- pluginify/config.py +144 -0
- pluginify/create_plugin_registries.py +1037 -0
- pluginify/errors.py +16 -0
- pluginify/interfaces/__init__.py +9 -0
- pluginify/interfaces/base.py +665 -0
- pluginify/interfaces/class_based/data_modifiers.py +38 -0
- pluginify/interfaces/class_based_plugin.py +210 -0
- pluginify/interfaces/yaml_based/configs.py +27 -0
- pluginify/plugin_registry.py +925 -0
- pluginify/plugins/classes/data_modifiers/cuboid.py +41 -0
- pluginify/plugins/yaml/configs/stucco.yaml +16 -0
- pluginify/pydantic_models/v1/configs.py +58 -0
- pluginify/utils/__init__.py +62 -0
- pluginify/utils/context_managers.py +32 -0
- pluginify-0.0.0.post1.dev0.dist-info/METADATA +66 -0
- pluginify-0.0.0.post1.dev0.dist-info/RECORD +22 -0
- pluginify-0.0.0.post1.dev0.dist-info/WHEEL +4 -0
- pluginify-0.0.0.post1.dev0.dist-info/entry_points.txt +6 -0
- pluginify-0.0.0.post1.dev0.dist-info/licenses/LICENSE +128 -0
pluginify/errors.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# # # This source code is subject to the license referenced at
|
|
2
|
+
# # # https://github.com/NRLMMD-GEOIPS.
|
|
3
|
+
|
|
4
|
+
"""Pluginify error module."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PluginError(Exception):
|
|
8
|
+
"""Exception to be raised when there is an error in a plugin."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PluginRegistryError(Exception):
|
|
14
|
+
"""Exception to be raised when there is an error in a plugin registry."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Initialization module for pluginify interfaces."""
|
|
2
|
+
|
|
3
|
+
from pluginify.interfaces.class_based.data_modifiers import data_modifiers
|
|
4
|
+
from pluginify.interfaces.yaml_based.configs import configs
|
|
5
|
+
|
|
6
|
+
class_based_interfaces = ["data_modifiers"]
|
|
7
|
+
yaml_based_interfaces = ["configs"]
|
|
8
|
+
|
|
9
|
+
__all__ = class_based_interfaces + yaml_based_interfaces
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
# # # This source code is subject to the license referenced at
|
|
2
|
+
# # # https://github.com/NRLMMD-GEOIPS.
|
|
3
|
+
|
|
4
|
+
"""Base classes for interfaces, plugins, and plugin validation machinery."""
|
|
5
|
+
|
|
6
|
+
import abc
|
|
7
|
+
from glob import glob
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
from os.path import basename
|
|
12
|
+
|
|
13
|
+
from jsonschema.exceptions import ValidationError
|
|
14
|
+
|
|
15
|
+
from pluginify.config import REBUILD_REGISTRIES
|
|
16
|
+
from pluginify.errors import PluginError
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseInterface(abc.ABC):
|
|
22
|
+
"""Base class for plugin interfaces.
|
|
23
|
+
|
|
24
|
+
This class should not be instantiated directly. Instead, a package should implement
|
|
25
|
+
custom interfaces that inherit from the children of this class, such as
|
|
26
|
+
`BaseYamlInterface` or `BaseClassInterface`. Those custom interfaces would then be
|
|
27
|
+
accessed by importing them from ``<your_package>.interfaces``. For example:
|
|
28
|
+
```
|
|
29
|
+
from <your_package>.interfaces import algorithms
|
|
30
|
+
```
|
|
31
|
+
will retrieve an instance of ``AlgorithmsInterface`` which will provide access to
|
|
32
|
+
<your_package> algorithm plugins.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import pluginify.plugin_registry as plugin_registry_module
|
|
36
|
+
|
|
37
|
+
name = "BaseInterface"
|
|
38
|
+
interface_type = None # This is set by child classes
|
|
39
|
+
rebuild_registries = REBUILD_REGISTRIES
|
|
40
|
+
# Setting this attribute at the top level so it can be used by all methods.
|
|
41
|
+
# This can be overridden by setting them in child interface classes
|
|
42
|
+
apiVersion = "pluginify/v1"
|
|
43
|
+
|
|
44
|
+
def __new__(cls):
|
|
45
|
+
"""Plugin interface new method."""
|
|
46
|
+
if not hasattr(cls, "name") or not cls.name:
|
|
47
|
+
raise AttributeError(
|
|
48
|
+
f"Error creating {cls.name} class. SubClasses of ``BaseInterface`` "
|
|
49
|
+
"must have the class attribute 'name'."
|
|
50
|
+
)
|
|
51
|
+
cls.__doc__ = f"Base interface class for {cls.name} plugins."
|
|
52
|
+
# cls.__doc__ += interface_attrs_doc causes duplication warnings
|
|
53
|
+
|
|
54
|
+
return super(BaseInterface, cls).__new__(cls)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def namespace(self):
|
|
58
|
+
"""Default namespace used for the plugin registry associated with this class.
|
|
59
|
+
|
|
60
|
+
By default, we use 'pluginify.plugin_packages' as the namespace for interface
|
|
61
|
+
classes. However, if a user has developed interfaces in a separate namespace
|
|
62
|
+
from pluginify, they can override this in their own classes by setting the
|
|
63
|
+
namespace to search in.
|
|
64
|
+
"""
|
|
65
|
+
if not hasattr(self, "_namespace"):
|
|
66
|
+
# By default all pluginify interfaces will have
|
|
67
|
+
# self.apiVersion = 'pluginify/v1' If that attribute is not already set.
|
|
68
|
+
self._namespace = f"{self.apiVersion.split('/')[0]}.plugin_packages"
|
|
69
|
+
return self._namespace
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def plugin_registry(self):
|
|
73
|
+
"""The plugin registry associated with this interface.
|
|
74
|
+
|
|
75
|
+
By default, we use 'pluginify.plugin_packages' as the namespace for interface
|
|
76
|
+
classes. However, if a user has developed interfaces in a separate namespace
|
|
77
|
+
from pluginify, they can override this in their own classes by setting the
|
|
78
|
+
namespace to search in.
|
|
79
|
+
"""
|
|
80
|
+
if not hasattr(self, "_plugin_registry"):
|
|
81
|
+
self._plugin_registry = self.plugin_registry_module.PluginRegistry(
|
|
82
|
+
self.namespace
|
|
83
|
+
)
|
|
84
|
+
return self._plugin_registry
|
|
85
|
+
|
|
86
|
+
@abc.abstractmethod
|
|
87
|
+
def get_plugin(self, name, rebuild_registries=rebuild_registries):
|
|
88
|
+
"""Abstract function for retrieving a plugin under a certain interface.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
name: str or tuple(str)
|
|
93
|
+
- The name of the yaml-based plugin. Either a single string or a tuple of
|
|
94
|
+
strings for product plugins.
|
|
95
|
+
rebuild_registries: bool (default=True)
|
|
96
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
97
|
+
true and get_plugin fails, rebuild the plugin registry, call then call
|
|
98
|
+
get_plugin once more with rebuild_registries toggled off, so it only gets
|
|
99
|
+
rebuilt once.
|
|
100
|
+
- By default, the value of rebuild_registries is set to True if not
|
|
101
|
+
explicitly set to False as an configuration variable under the name
|
|
102
|
+
`REBUILD_REGISTRIES` in `~/.config/pluginify/config.yaml`.
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
@abc.abstractmethod
|
|
107
|
+
def get_plugins(self):
|
|
108
|
+
"""Abstract function for retrieving all plugins under a certain interface."""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def get_plugin_metadata(self, name):
|
|
112
|
+
"""Retrieve a plugin's metadata.
|
|
113
|
+
|
|
114
|
+
Where the metadata of the plugin matches the plugin's corresponding entry in the
|
|
115
|
+
plugin registry.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
name: str or tuple(str)
|
|
120
|
+
- The name of the plugin whose metadata we want.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
metadata: dict
|
|
125
|
+
- A dictionary of metadata for the requested plugin.
|
|
126
|
+
"""
|
|
127
|
+
return self.plugin_registry.get_plugin_metadata(self, name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class BaseYamlPlugin(dict):
|
|
131
|
+
"""Base class for YAML plugins."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, *args, **kwargs):
|
|
134
|
+
"""Class BaseYamlPlugin init method."""
|
|
135
|
+
super().__init__(*args, **kwargs)
|
|
136
|
+
|
|
137
|
+
def __repr__(self):
|
|
138
|
+
"""Class BaseYamlPlugin repr method."""
|
|
139
|
+
val = super().__repr__()
|
|
140
|
+
return f"{self.__class__.__name__}({val})"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class BaseYamlInterface(BaseInterface):
|
|
144
|
+
"""Base class for yaml-based plugin interfaces.
|
|
145
|
+
|
|
146
|
+
This class should not be instantiated directly. Instead, a package should implement
|
|
147
|
+
custom interfaces that inherit from the children of this class. Those custom
|
|
148
|
+
interfaces would then be accessed by importing them from
|
|
149
|
+
``<your_package>.interfaces``. For example:
|
|
150
|
+
```
|
|
151
|
+
from <your_package>.interfaces import products
|
|
152
|
+
```
|
|
153
|
+
will retrieve an instance of ``ProductsInterface`` which will provide access to
|
|
154
|
+
the <your_package> products plugins.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# This defaults to the json-schema-based validator but can be overridden
|
|
158
|
+
# by the interface class to use a different validator. We are making use of this as
|
|
159
|
+
# we switch to the new pydantic-based validators.
|
|
160
|
+
interface_type = "yaml_based"
|
|
161
|
+
name = "BaseYamlInterface"
|
|
162
|
+
use_pydantic = False
|
|
163
|
+
|
|
164
|
+
def __new__(cls):
|
|
165
|
+
"""YAML plugin interface new method."""
|
|
166
|
+
cls = super(BaseInterface, cls).__new__(cls)
|
|
167
|
+
|
|
168
|
+
return cls
|
|
169
|
+
|
|
170
|
+
def __init__(self):
|
|
171
|
+
"""YAML plugin interface init method."""
|
|
172
|
+
try:
|
|
173
|
+
self.supported_families = [
|
|
174
|
+
basename(fname).split(".")[0]
|
|
175
|
+
for fname in sorted(
|
|
176
|
+
glob(str(files("geoips") / f"schema/{self.name}/*.yaml"))
|
|
177
|
+
)
|
|
178
|
+
]
|
|
179
|
+
except Exception:
|
|
180
|
+
# Catching in case the file paths above don't exist. This is a stop gap
|
|
181
|
+
# fix and will be removed entirely once families are removed.
|
|
182
|
+
self.supported_families = ["standard"]
|
|
183
|
+
|
|
184
|
+
def _create_registered_plugin_names(self, yaml_plugin):
|
|
185
|
+
"""Create a plugin name for plugin registry.
|
|
186
|
+
|
|
187
|
+
Some interfaces need to override this (e.g. products) because they
|
|
188
|
+
need a more complex name for retrieval.
|
|
189
|
+
"""
|
|
190
|
+
return [yaml_plugin["name"]]
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
@abc.abstractmethod
|
|
194
|
+
def validator(self):
|
|
195
|
+
"""The validator for plugin types that fall under this interface.
|
|
196
|
+
|
|
197
|
+
Abstract. This must be implemented by the final child node which inherits from
|
|
198
|
+
this class.
|
|
199
|
+
"""
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def _plugin_yaml_to_obj(cls, name, yaml_plugin, obj_attrs={}):
|
|
204
|
+
"""Convert a yaml plugin to an object.
|
|
205
|
+
|
|
206
|
+
Convert the passed YAML plugin into an object and return it. The returned
|
|
207
|
+
object will be derived from a class named ``<interface>Plugin`` where
|
|
208
|
+
interface is the interface specified by the plugin. This class is derived
|
|
209
|
+
from ``BasePlugin``.
|
|
210
|
+
|
|
211
|
+
This function is used instead of predefined classes to allow setting ``__doc__``
|
|
212
|
+
on a plugin-by-plugin basis. This allows collecting ``__doc__`` and
|
|
213
|
+
from the plugin and using them in the objects.
|
|
214
|
+
|
|
215
|
+
For a yaml plugin to be converted into an object it must meet the following
|
|
216
|
+
requirements:
|
|
217
|
+
|
|
218
|
+
- Must match the jsonschema spec provided for its interface.
|
|
219
|
+
- The plugin must have the following non-empty top-level attributes.
|
|
220
|
+
- interface: The name of the interface that the plugin belongs to.
|
|
221
|
+
- family: The family of plugins this plugin belongs to within the interface.
|
|
222
|
+
- name: The name of the plugin which must be unique within the interface.
|
|
223
|
+
- docstring: A string to be used as the object's docstring.
|
|
224
|
+
"""
|
|
225
|
+
obj_attrs["id"] = name
|
|
226
|
+
obj_attrs["yaml"] = yaml_plugin
|
|
227
|
+
|
|
228
|
+
missing = []
|
|
229
|
+
|
|
230
|
+
for attr in [
|
|
231
|
+
"package",
|
|
232
|
+
"relpath",
|
|
233
|
+
"interface",
|
|
234
|
+
"family",
|
|
235
|
+
"name",
|
|
236
|
+
"docstring",
|
|
237
|
+
]:
|
|
238
|
+
try:
|
|
239
|
+
obj_attrs[attr] = yaml_plugin[attr]
|
|
240
|
+
# This should be removed once we fully switch to pydantic models
|
|
241
|
+
except TypeError:
|
|
242
|
+
yaml_plugin = yaml_plugin.model_dump()
|
|
243
|
+
obj_attrs[attr] = yaml_plugin[attr]
|
|
244
|
+
except KeyError:
|
|
245
|
+
missing.append(attr)
|
|
246
|
+
if missing:
|
|
247
|
+
raise PluginError(
|
|
248
|
+
f"Plugin '{yaml_plugin['name']}' is missing the following required "
|
|
249
|
+
f"top-level properties: '{missing}'"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
obj_attrs["__doc__"] = obj_attrs["docstring"]
|
|
253
|
+
|
|
254
|
+
plugin_interface_name = obj_attrs["interface"].title().replace("_", "")
|
|
255
|
+
plugin_type = f"{plugin_interface_name}Plugin"
|
|
256
|
+
|
|
257
|
+
plugin_base_class = BaseYamlPlugin
|
|
258
|
+
if hasattr(cls, "plugin_class") and cls.plugin_class:
|
|
259
|
+
plugin_base_class = cls.plugin_class
|
|
260
|
+
return type(plugin_type, (plugin_base_class,), obj_attrs)(yaml_plugin)
|
|
261
|
+
|
|
262
|
+
def __repr__(self):
|
|
263
|
+
"""Plugin interface repr method."""
|
|
264
|
+
return f"{self.__class__.__name__}()"
|
|
265
|
+
|
|
266
|
+
def get_plugin(self, name, rebuild_registries=None):
|
|
267
|
+
"""Get a plugin by its name.
|
|
268
|
+
|
|
269
|
+
This default method can be overridden to provide different search
|
|
270
|
+
functionality for an interface. An example of this is in the
|
|
271
|
+
ProductsInterface which uses a tuple containing 'source_name' and
|
|
272
|
+
'name'.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
name: str or tuple(str)
|
|
277
|
+
- The name of the yaml-based plugin. Either a single string or a tuple of
|
|
278
|
+
strings for product plugins.
|
|
279
|
+
rebuild_registries: bool (default=None)
|
|
280
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
281
|
+
None, default to the value of
|
|
282
|
+
`~/.config/pluginify/config.yaml:REBUILD_REGISTRIES`. If that
|
|
283
|
+
configuration variable is not set, we default to True.
|
|
284
|
+
If specified, use the input value of rebuild_registries, which should be a
|
|
285
|
+
boolean value. If rebuild registries is true and get_plugin fails, rebuild
|
|
286
|
+
the plugin registry, call then call get_plugin once more with
|
|
287
|
+
rebuild_registries toggled off, so it only gets rebuilt once.
|
|
288
|
+
"""
|
|
289
|
+
return self.plugin_registry.get_yaml_plugin(self, name, rebuild_registries)
|
|
290
|
+
|
|
291
|
+
def get_plugins(self):
|
|
292
|
+
"""Retrieve all yaml plugin objects."""
|
|
293
|
+
return self.plugin_registry.get_yaml_plugins(self)
|
|
294
|
+
|
|
295
|
+
def plugin_is_valid(self, name):
|
|
296
|
+
"""Plugin is valid method."""
|
|
297
|
+
try:
|
|
298
|
+
self.get_plugin(name)
|
|
299
|
+
return True
|
|
300
|
+
except ValidationError:
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def plugins_all_valid(self):
|
|
304
|
+
"""Plugins all valid method."""
|
|
305
|
+
try:
|
|
306
|
+
self.get_plugins()
|
|
307
|
+
return True
|
|
308
|
+
except ValidationError:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def test_interface(self):
|
|
312
|
+
"""Test interface method.
|
|
313
|
+
|
|
314
|
+
Note this is currently only called via the
|
|
315
|
+
tests/utils/test_interfaces.py script (which is
|
|
316
|
+
not a valid pytest script, but called directly
|
|
317
|
+
via the command line.
|
|
318
|
+
"""
|
|
319
|
+
plugins = self.get_plugins()
|
|
320
|
+
all_valid = self.plugins_all_valid()
|
|
321
|
+
family_list = []
|
|
322
|
+
plugin_ids = {}
|
|
323
|
+
for plugin in plugins:
|
|
324
|
+
# TODO: Refactor to remove this `if` block after full migration from JSON
|
|
325
|
+
# to Pydantic schemas.
|
|
326
|
+
if hasattr(plugin, "model_dump"):
|
|
327
|
+
pdata = plugin.model_dump()
|
|
328
|
+
else:
|
|
329
|
+
pdata = plugin if isinstance(plugin, dict) else plugin.__dict__
|
|
330
|
+
|
|
331
|
+
plugin_family = pdata.get("family")
|
|
332
|
+
plugin_name = pdata.get("name")
|
|
333
|
+
|
|
334
|
+
if not plugin_family or not plugin_name:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"Missing required plugin fields. "
|
|
337
|
+
f" Expected 'family' and 'name' got: family={plugin_family}, "
|
|
338
|
+
f"name={plugin_name}. Full plugin data: {pdata}"
|
|
339
|
+
)
|
|
340
|
+
if plugin_family not in family_list:
|
|
341
|
+
family_list.append(plugin_family)
|
|
342
|
+
plugin_ids[plugin_family] = []
|
|
343
|
+
plugin_ids[plugin_family].append(plugin_name)
|
|
344
|
+
|
|
345
|
+
output = {
|
|
346
|
+
"all_valid": all_valid,
|
|
347
|
+
"by_family": plugin_ids,
|
|
348
|
+
"validity_check": {},
|
|
349
|
+
"family": {},
|
|
350
|
+
"func": {},
|
|
351
|
+
"docstring": {},
|
|
352
|
+
}
|
|
353
|
+
for curr_family in plugin_ids:
|
|
354
|
+
for curr_id in plugin_ids[curr_family]:
|
|
355
|
+
output["validity_check"][curr_id] = self.plugin_is_valid(curr_id)
|
|
356
|
+
output["func"][curr_id] = self.get_plugin(curr_id)
|
|
357
|
+
output["family"][curr_id] = curr_family
|
|
358
|
+
output["docstring"][curr_id] = output["func"][curr_id].docstring
|
|
359
|
+
return output
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class BaseClassInterface(BaseInterface):
|
|
363
|
+
"""Base class for class-based interfaces.
|
|
364
|
+
|
|
365
|
+
This class should not be instantiated directly. Instead, a package should implement
|
|
366
|
+
custom interfaces that inherit from the children of this class. Those custom
|
|
367
|
+
interfaces would then be accessed by importing them from
|
|
368
|
+
``<your_package>.interfaces``. For example:
|
|
369
|
+
```
|
|
370
|
+
from <your_package>.interfaces import algorithms
|
|
371
|
+
```
|
|
372
|
+
will retrieve an instance of ``AlgorithmsInterface`` which will provide access to
|
|
373
|
+
the <your_package> algorithm plugins.
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
interface_type = "class_based"
|
|
377
|
+
name = "BaseClassInterface"
|
|
378
|
+
required_args = {}
|
|
379
|
+
|
|
380
|
+
def __repr__(self):
|
|
381
|
+
"""Plugin interface repr method."""
|
|
382
|
+
return f"{self.__class__.__name__}()"
|
|
383
|
+
|
|
384
|
+
def __init__(self):
|
|
385
|
+
"""Initialize module plugin interface."""
|
|
386
|
+
self.supported_families = list(self.required_args.keys())
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def _plugin_module_to_obj(cls, name, module, obj_attrs={}):
|
|
390
|
+
"""Convert a module plugin to an object.
|
|
391
|
+
|
|
392
|
+
Convert the passed module plugin into an object and return it. The returned
|
|
393
|
+
object will be derrived from a class named ``<interface>Plugin`` where
|
|
394
|
+
interface is the interface specified by the plugin. This class is derrived
|
|
395
|
+
from ``BasePlugin``.
|
|
396
|
+
|
|
397
|
+
This function is used instead of predefined classes to allow setting ``__doc__``
|
|
398
|
+
and ``call`` on a plugin-by-plugin basis. This allows collecting ``__doc__``
|
|
399
|
+
and ``call`` from the plugin modules and using them in the objects.
|
|
400
|
+
|
|
401
|
+
For a module to be converted into an object it must meet the following
|
|
402
|
+
requirements:
|
|
403
|
+
|
|
404
|
+
- The module must define a docstring. This will be used as the docstring for the
|
|
405
|
+
plugin class as well as the docstring for the plugin when requested on the
|
|
406
|
+
command line. The first line will be used as a "short" description, and the
|
|
407
|
+
full docstring will be used as a more detailed discussion of the plugin.
|
|
408
|
+
- The following global attributes must be defined in the module:
|
|
409
|
+
- interface: The name of the interface that the plugin belongs to.
|
|
410
|
+
- family: The family of plugins that the plugin belongs to within the interface.
|
|
411
|
+
- name: The name of the plugin which must be unique within the interface.
|
|
412
|
+
- A callable named `call` that will be called when the plugin is used.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
module : module
|
|
417
|
+
The imported plugin module.
|
|
418
|
+
obj_attrs : dict, optional
|
|
419
|
+
Additional attributes to be assigned to the plugin object.
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
An object of type ``<interface>InterfacePlugin`` where ``<interface>`` is the
|
|
424
|
+
name of the interface that the desired plugin belongs to.
|
|
425
|
+
"""
|
|
426
|
+
obj_attrs["id"] = name
|
|
427
|
+
|
|
428
|
+
missing = []
|
|
429
|
+
for attr in ["interface", "family", "name"]:
|
|
430
|
+
try:
|
|
431
|
+
obj_attrs[attr] = getattr(module, attr)
|
|
432
|
+
except AttributeError:
|
|
433
|
+
missing.append(attr)
|
|
434
|
+
|
|
435
|
+
if missing:
|
|
436
|
+
raise PluginError(
|
|
437
|
+
f"Plugin '{module.__name__}' from '{module.__file__}' is missing the "
|
|
438
|
+
f"following required global attributes: '{missing}'."
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if module.__doc__:
|
|
442
|
+
obj_attrs["__doc__"] = module.__doc__
|
|
443
|
+
else:
|
|
444
|
+
raise PluginError(
|
|
445
|
+
f"Plugin modules must have a docstring. "
|
|
446
|
+
f"No docstring found in '{module.__name__}'."
|
|
447
|
+
)
|
|
448
|
+
obj_attrs["docstring"] = module.__doc__
|
|
449
|
+
|
|
450
|
+
# Collect the callable and assign to call
|
|
451
|
+
try:
|
|
452
|
+
obj_attrs["call"] = staticmethod(getattr(module, "call"))
|
|
453
|
+
except AttributeError as err:
|
|
454
|
+
raise PluginError(
|
|
455
|
+
f"Plugin modules must contain a callable name 'call'. This is missing "
|
|
456
|
+
f"in plugin module '{module.__name__}'"
|
|
457
|
+
) from err
|
|
458
|
+
|
|
459
|
+
plugin_interface_name = obj_attrs["interface"].title().replace("_", "")
|
|
460
|
+
plugin_type = f"{plugin_interface_name}Plugin"
|
|
461
|
+
|
|
462
|
+
# Always require 'plugin_class' from each class-based interface
|
|
463
|
+
# This is enforced in the 'test_interfaces' unit test.
|
|
464
|
+
if not hasattr(cls, "plugin_class") or cls.plugin_class is None:
|
|
465
|
+
raise PluginError(
|
|
466
|
+
f"Error: interface '{obj_attrs['interface']}' is missing required "
|
|
467
|
+
"attribute 'plugin_class'. Please create a base class plugin for this "
|
|
468
|
+
"interface and assign that object to the 'plugin_class' attribute of "
|
|
469
|
+
"this interface before continuing."
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
plugin_base_class = cls.plugin_class
|
|
473
|
+
|
|
474
|
+
# Create an object of type ``plugin_type`` with attributes from ``obj_attrs``
|
|
475
|
+
return type(plugin_type, (plugin_base_class,), obj_attrs)(module)
|
|
476
|
+
|
|
477
|
+
def get_plugin(self, name, rebuild_registries=None):
|
|
478
|
+
"""Retrieve a plugin from this interface by name.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
name : str
|
|
483
|
+
- The name the desired plugin.
|
|
484
|
+
rebuild_registries: bool (default=None)
|
|
485
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
486
|
+
None, default to the value of
|
|
487
|
+
`~/.config/pluginify/config.yaml:REBUILD_REGISTRIES`. If that
|
|
488
|
+
configuration variable is not set, we default to True.
|
|
489
|
+
If specified, use the input value of rebuild_registries, which should be a
|
|
490
|
+
boolean value. If rebuild registries is true and get_plugin fails, rebuild
|
|
491
|
+
the plugin registry, call then call get_plugin once more with
|
|
492
|
+
rebuild_registries toggled off, so it only gets rebuilt once.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
An object of type ``<interface>Plugin`` where ``<interface>`` is the name of
|
|
497
|
+
this interface.
|
|
498
|
+
|
|
499
|
+
Raises
|
|
500
|
+
------
|
|
501
|
+
PluginError
|
|
502
|
+
If the specified plugin isn't found within the interface.
|
|
503
|
+
"""
|
|
504
|
+
return self.plugin_registry.get_class_plugin(self, name, rebuild_registries)
|
|
505
|
+
|
|
506
|
+
def get_plugins(self):
|
|
507
|
+
"""Retrieve all module plugins for this interface."""
|
|
508
|
+
return self.plugin_registry.get_class_plugins(self)
|
|
509
|
+
|
|
510
|
+
def plugin_is_valid(self, plugin):
|
|
511
|
+
"""Check that an interface is valid.
|
|
512
|
+
|
|
513
|
+
Check that the requested interface function has the correct call signature.
|
|
514
|
+
Return values should be as specified below, but are not programmatically
|
|
515
|
+
verified.
|
|
516
|
+
|
|
517
|
+
Note this is currently only called via the
|
|
518
|
+
tests/utils/test_interfaces.py script (which is
|
|
519
|
+
not a valid pytest script, but called directly
|
|
520
|
+
via the command line), via the test_interface
|
|
521
|
+
method within this base interface.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
plugin : PluginObject
|
|
526
|
+
A plugin object coming from _plugin_module_to_obj that needs to be validated.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
bool
|
|
531
|
+
True if valid, False if invalid
|
|
532
|
+
"""
|
|
533
|
+
if plugin.family not in self.required_args:
|
|
534
|
+
raise PluginError(
|
|
535
|
+
f"'{plugin.family}' must be added to required args list"
|
|
536
|
+
f"\nfor '{self.name}' interface,"
|
|
537
|
+
f"\nfound in '{plugin.name}' plugin,"
|
|
538
|
+
f"\nin '{plugin.module_name}' module"
|
|
539
|
+
f"\nat '{plugin.module_path}'\n"
|
|
540
|
+
)
|
|
541
|
+
if plugin.family not in self.required_kwargs:
|
|
542
|
+
raise PluginError(
|
|
543
|
+
f"'{plugin.family}' must be added to required kwargs list"
|
|
544
|
+
f"\nfor '{self.name}' interface,"
|
|
545
|
+
f"\nfound in '{plugin.name}' plugin,"
|
|
546
|
+
f"\nin '{plugin.module_name}' module"
|
|
547
|
+
f"\nat '{plugin.module_path}'\n"
|
|
548
|
+
)
|
|
549
|
+
expected_args = self.required_args[plugin.family]
|
|
550
|
+
expected_kwargs = self.required_kwargs[plugin.family]
|
|
551
|
+
|
|
552
|
+
sig = inspect.signature(plugin.call)
|
|
553
|
+
arg_list = []
|
|
554
|
+
kwarg_list = []
|
|
555
|
+
kwarg_defaults_list = []
|
|
556
|
+
for param_key, param_value in sig.parameters.items():
|
|
557
|
+
# kwargs are identified by a default value - parameter will include "="
|
|
558
|
+
if "=" in str(param_value):
|
|
559
|
+
# This is kwarg = default, splitting on = and taking the second
|
|
560
|
+
# value only.
|
|
561
|
+
default_value = str(param_value).split("=")[-1]
|
|
562
|
+
kwarg_list += [str(param_key)]
|
|
563
|
+
kwarg_defaults_list += [default_value]
|
|
564
|
+
# If there is no "=", then it is a positional parameter
|
|
565
|
+
else:
|
|
566
|
+
arg_list += [str(param_key)]
|
|
567
|
+
|
|
568
|
+
for expected_arg in expected_args:
|
|
569
|
+
if expected_arg not in arg_list:
|
|
570
|
+
raise PluginError(
|
|
571
|
+
f"MISSING expected arg '{expected_arg}' in '{plugin.name}'"
|
|
572
|
+
f"\nfor '{self.name}' interface,"
|
|
573
|
+
f"\nfound in '{plugin.name}' plugin,"
|
|
574
|
+
f"\nin '{plugin.module_name}' module"
|
|
575
|
+
f"\nat '{plugin.module_path}'\n"
|
|
576
|
+
)
|
|
577
|
+
for expected_kwarg in expected_kwargs:
|
|
578
|
+
# If expected_kwarg is a tuple, first item is kwarg, second default value
|
|
579
|
+
if isinstance(expected_kwarg, tuple):
|
|
580
|
+
if expected_kwarg[0] not in kwarg_list:
|
|
581
|
+
raise PluginError(
|
|
582
|
+
f"MISSING expected kwarg '{expected_kwarg}' in '{plugin.name}'"
|
|
583
|
+
f"\nfor '{self.name}' interface,"
|
|
584
|
+
f"\nfound in '{plugin.name}' plugin,"
|
|
585
|
+
f"\nin '{plugin.module_name}' module"
|
|
586
|
+
f"\nat '{plugin.module_path}'\n"
|
|
587
|
+
)
|
|
588
|
+
elif expected_kwarg not in kwarg_list:
|
|
589
|
+
raise PluginError(
|
|
590
|
+
f"MISSING expected kwarg '{expected_kwarg}' in '{plugin.name}'"
|
|
591
|
+
f"\nfor '{self.name}' interface,"
|
|
592
|
+
f"\nfound in '{plugin.name}' plugin,"
|
|
593
|
+
f"\nin '{plugin.module_name}' module"
|
|
594
|
+
f"\nat '{plugin.module_path}'\n"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
def plugins_all_valid(self):
|
|
600
|
+
"""Test the current interface by validating every Plugin.
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
True if all plugins are valid, False if any plugin is invalid.
|
|
605
|
+
"""
|
|
606
|
+
plugins = self.get_plugins()
|
|
607
|
+
for plugin in plugins:
|
|
608
|
+
if not self.plugin_is_valid(plugin):
|
|
609
|
+
return False
|
|
610
|
+
return True
|
|
611
|
+
|
|
612
|
+
def test_interface(self):
|
|
613
|
+
"""Test the current interface by validating each Plugin and testing each method.
|
|
614
|
+
|
|
615
|
+
Test this interface by opening every Plugin available to the interface,
|
|
616
|
+
and validating each plugin by calling `plugin_is_valid` for each.
|
|
617
|
+
Additionally, ensure all methods of this interface work as expected:
|
|
618
|
+
|
|
619
|
+
* get_plugins
|
|
620
|
+
* get_plugin
|
|
621
|
+
* plugin_is_valid
|
|
622
|
+
* plugins_all_valid
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
A dictionary containing three keys:
|
|
627
|
+
'by_family', 'validity_check', 'func', and 'family'. The value for each
|
|
628
|
+
of these keys is a dictionary whose keys are the names of the Plugins.
|
|
629
|
+
|
|
630
|
+
- 'by_family' contains a dictionary of plugin names sorted by family.
|
|
631
|
+
- 'validity_check' contains a dict whose keys are plugin names and whose
|
|
632
|
+
values are bools where `True` indicates that the Plugin's function is
|
|
633
|
+
valid according to `plugin_is_valid`.
|
|
634
|
+
- 'func' contains a dict whose keys are plugin names and whose values are
|
|
635
|
+
the function for each Plugin.
|
|
636
|
+
- 'family' contains a dict whose keys are plugin names and whose values
|
|
637
|
+
are the contents of the 'family' attribute for each Plugin.
|
|
638
|
+
"""
|
|
639
|
+
# plugin_names = self.get_plugins(sort_by="family")
|
|
640
|
+
plugins = self.get_plugins()
|
|
641
|
+
all_valid = self.plugins_all_valid()
|
|
642
|
+
family_list = []
|
|
643
|
+
plugin_ids = {}
|
|
644
|
+
for plugin in plugins:
|
|
645
|
+
if plugin.family not in family_list:
|
|
646
|
+
family_list.append(plugin.family)
|
|
647
|
+
plugin_ids[plugin.family] = []
|
|
648
|
+
plugin_ids[plugin.family].append(plugin.id)
|
|
649
|
+
|
|
650
|
+
output = {
|
|
651
|
+
"all_valid": all_valid,
|
|
652
|
+
"by_family": plugin_ids,
|
|
653
|
+
"validity_check": {},
|
|
654
|
+
"family": {},
|
|
655
|
+
"func": {},
|
|
656
|
+
"docstring": {},
|
|
657
|
+
}
|
|
658
|
+
for curr_family in plugin_ids:
|
|
659
|
+
for curr_id in plugin_ids[curr_family]:
|
|
660
|
+
plugin = self.get_plugin(curr_id)
|
|
661
|
+
output["validity_check"][curr_id] = self.plugin_is_valid(plugin)
|
|
662
|
+
output["func"][curr_id] = plugin
|
|
663
|
+
output["family"][curr_id] = curr_family
|
|
664
|
+
output["docstring"][curr_id] = output["func"][curr_id].docstring
|
|
665
|
+
return output
|