Source code for pluginify.utils.validators

# # # This source code is subject to the license referenced at
# # # https://github.com/NRLMMD-GEOIPS.

"""Pluginify validators module.

Currently only implements a PluginRegistryValidator class.
"""

from importlib import metadata, import_module
import json
from pprint import pformat

import pytest

from pluginify.errors import PluginRegistryError
from pluginify.plugin_registry import PluginRegistry


[docs]class PluginRegistryValidator(PluginRegistry): """Subclass of PluginRegistry which adds functionality for unit testing.""" def __init__(self, namespace, fpaths=None): """Initialize TestPluginRegistry Class.""" self.namespace = namespace super().__init__(self.namespace, _test_registry_files=fpaths)
[docs] def validate_plugin_types_exist(self, reg_dict, reg_path): """Test that all top level plugin types exist in each registry file.""" expected_plugin_types = ["yaml_based", "class_based", "text_based"] for p_type in expected_plugin_types: if p_type not in reg_dict: error_str = f"Expected plugin type '{p_type}' to be in the registry but" error_str += f" wasn't. This was in file '{reg_path}'." raise PluginRegistryError(error_str)
[docs] def validate_all_registries(self): """Validate all registries in the current installation. This should be run during testing, but not at runtime. Ensure we do not fail catastrophically for a single bad plugin at runtime, so test up front to test validity. """ for reg_path in self.registry_files: pkg_plugins = json.load(open(reg_path, "r")) self.validate_registry(pkg_plugins, reg_path)
[docs] def validate_registry(self, current_registry, fpath): """Test all plugins found in registered plugins for their validity.""" try: self.validate_plugin_types_exist(current_registry, fpath) except PluginRegistryError as e: # xfail if this is a test, otherwise just raise PluginRegistryError if self._is_test: pytest.xfail(str(e)) else: raise PluginRegistryError(e) try: self.validate_registry_interfaces(current_registry) except PluginRegistryError as e: # xfail if this is a test, otherwise just raise PluginRegistryError if self._is_test: pytest.xfail(str(e)) else: raise PluginRegistryError(e) for plugin_type in current_registry: for interface in current_registry[plugin_type]: for plugin in current_registry[plugin_type][interface]: try: if interface == "products": for subplg in current_registry[plugin_type][interface][ plugin ]: self.validate_plugin_attrs( plugin_type, interface, (plugin, subplg), current_registry[plugin_type][interface][plugin][ subplg ], ) elif plugin_type != "text_based": self.validate_plugin_attrs( plugin_type, interface, plugin, current_registry[plugin_type][interface][plugin], ) except PluginRegistryError as e: # xfail if this is a test, # otherwise just raise PluginRegistryError if self._is_test: pytest.xfail(str(e)) else: raise PluginRegistryError(e)
[docs] def validate_plugin_attrs(self, plugin_type, interface, name, plugin): """Test non-product plugin for all required attributes.""" missing = [] if plugin_type == "yaml_based" and interface != "products": attrs = [ "docstring", "family", "interface", "package", "plugin_type", "relpath", ] elif plugin_type == "yaml_based": attrs = [ "docstring", "family", "interface", "package", "plugin_type", "product_defaults", "source_names", "relpath", ] else: attrs = [ "docstring", "family", "interface", "package", "plugin_type", "signature", "relpath", ] for attr in attrs: try: plugin[attr] except KeyError: missing.append(attr) if missing: raise PluginRegistryError( f"Plugin '{name}' is missing the following required " f"top-level properties: '{missing}'" )
[docs] def validate_registry_interfaces(self, current_registry): """Test Plugin Registry interfaces validity.""" yaml_interfaces = [] class_interfaces = [] # NOTE: all <pkg>/interfaces/__init__.py files MUST # contain a "class_based_interfaces" list and a # "yaml_based_interfaces" list - this is how we # identify the valid interface names. # We must avoid actually importing the interfaces within # the pluginify repo - or we will end up with a circular import # due to BaseInterface. for pkg in metadata.entry_points(group=self.namespace): try: mod = import_module(f"{pkg.value}.interfaces") except ModuleNotFoundError as resp: if f"No module named '{pkg.value}.interfaces'" in str(resp): continue else: raise ModuleNotFoundError(resp) class_interfaces += mod.class_based_interfaces yaml_interfaces += mod.yaml_based_interfaces bad_interfaces = [] for plugin_type in ["class_based", "yaml_based"]: for interface in current_registry[plugin_type]: if ( interface not in class_interfaces and interface not in yaml_interfaces ): error_str = f"\nPlugin type '{plugin_type}' does not allow " error_str += f"interface '{interface}'.\n" error_str += "\nValid interfaces: " if plugin_type == "class_based": interface_list = class_interfaces else: interface_list = yaml_interfaces error_str += f"\n{interface_list}\n" error_str += "\nPlease update the following plugins " error_str += "to use a valid interface:\n" error_str += pformat(current_registry[plugin_type][interface]) bad_interfaces.append(error_str) if bad_interfaces: error_str = "The following interfaces were not valid:\n" for error in bad_interfaces: error_str += error raise PluginRegistryError(error_str)