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/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