Added plugin interface functions + tests

master
Tom Wilson 6 years ago
parent bb3f176d41
commit 08c5bf2302

@ -4,6 +4,8 @@ import inspect
import logging import logging
import sys import sys
import pkgutil import pkgutil
from collections import namedtuple
from types import MappingProxyType
import pkg_resources import pkg_resources
from configspec import ConfigSpecification from configspec import ConfigSpecification
from .. import base_plugins from .. import base_plugins
@ -11,6 +13,12 @@ from .. import base_plugins
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_loaded_plugins = {}
plugin_interfaces = {}
plugin_functions = {}
class PluginLoadError(Exception): class PluginLoadError(Exception):
pass pass
@ -36,6 +44,16 @@ def plugin_attachment(hookname):
return wrapped return wrapped
class InterfaceFunction():
def __init__(self, func):
if not callable(func):
raise TypeError("Argument to InterfaceFunction must be callable")
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class PluginInterface(): class PluginInterface():
@staticmethod @staticmethod
def _load_interface(module, plugin_name) -> "PluginInterface": def _load_interface(module, plugin_name) -> "PluginInterface":
@ -58,6 +76,14 @@ class PluginInterface():
interface._plugin_name = plugin_name interface._plugin_name = plugin_name
return interface return interface
def __init__(self):
self._confspec = None
self._loaded = False
self._functions = {}
self.config = None
self.plugins = None
self._plugin_name = "<not yet loaded>"
def _load_confspec(self, module): def _load_confspec(self, module):
""" """
If not already registered, looks for a ConfigSpecification instance in a plugin, If not already registered, looks for a ConfigSpecification instance in a plugin,
@ -82,11 +108,6 @@ class PluginInterface():
def _load_pluginclass(self, module): def _load_pluginclass(self, module):
pass pass
def __init__(self):
self._confspec = None
self._loaded = False
self._plugin_name = "<not yet loaded>"
def _load_guard(self): def _load_guard(self):
if self._loaded: if self._loaded:
raise PluginLoadError("Cannot call interface register functions once" raise PluginLoadError("Cannot call interface register functions once"
@ -98,6 +119,17 @@ class PluginInterface():
raise PluginLoadError("confspec must be an instance of ConfigSpecification") raise PluginLoadError("confspec must be an instance of ConfigSpecification")
self._confspec = confspec self._confspec = confspec
def register_function(self, func, name=None):
self._load_guard()
if not name:
name = func.__name__
if name in self._functions:
raise PluginLoadError(F"Interface function with name '{name}' already exists")
self._functions[name] = InterfaceFunction(func)
@property @property
def confspec(self): def confspec(self):
return self._confspec return self._confspec
@ -132,9 +164,6 @@ def discover_installed_plugins():
return [entrypoint.name for entrypoint in pkg_resources.iter_entry_points('shepherd.plugins')] return [entrypoint.name for entrypoint in pkg_resources.iter_entry_points('shepherd.plugins')]
loaded_plugins = {}
def load_plugin(plugin_name, plugin_dir=None): def load_plugin(plugin_name, plugin_dir=None):
""" """
Finds a Shepherd plugin, loads it, and returns the resulting PluginInterface object. Finds a Shepherd plugin, loads it, and returns the resulting PluginInterface object.
@ -153,8 +182,8 @@ def load_plugin(plugin_name, plugin_dir=None):
Returns: The PluginInterface for the loaded plugin Returns: The PluginInterface for the loaded plugin
""" """
if plugin_name in loaded_plugins: if plugin_name in _loaded_plugins:
return loaded_plugins[plugin_name] return _loaded_plugins[plugin_name]
# Each of the 3 plugin sources have different import mechanisms. Discovery is broken out to # Each of the 3 plugin sources have different import mechanisms. Discovery is broken out to
# allow them to be listed. Using a try/except block wouldn't be able to tell the difference # allow them to be listed. Using a try/except block wouldn't be able to tell the difference
@ -185,10 +214,29 @@ def load_plugin(plugin_name, plugin_dir=None):
interface._load_confspec(mod) interface._load_confspec(mod)
# TODO Populate plugin interface # TODO Populate plugin interface
interface._loaded = True
loaded_plugins[plugin_name] = interface _loaded_plugins[plugin_name] = interface
return interface return interface
def init_plugins(plugin_configs, core_config): def init_plugins(plugin_configs, core_config):
pass """
Initialise plugins named as keys in plugin_configs. Plugins must already be loaded.
"""
# Run plugin init and init hooks
plugin_names = plugin_configs.keys()
for plugin_name in plugin_names:
plugin_interfaces[plugin_name] = _loaded_plugins[plugin_name]
plugin_functions[plugin_name] = _loaded_plugins[plugin_name]._functions
plugin_functions_tuples = {}
for name, functions in plugin_functions.items():
plugin_functions_tuples[name] = namedtuple(
F'{name}_interface_functions', functions.keys())(**functions)
plugin_functions_view = MappingProxyType(plugin_functions_tuples)
for name, plugin_interface in plugin_interfaces.items():
plugin_interface.plugins = plugin_functions_view

@ -0,0 +1,12 @@
from configspec import *
from shepherd import PluginInterface
interface = PluginInterface()
confspec = ConfigSpecification()
confspec.add_spec("spec1", StringSpec())
confspec2 = ConfigSpecification()
confspec2.add_spec("spec2", StringSpec())
interface.register_confspec(confspec2)

@ -6,7 +6,11 @@ interface = PluginInterface()
confspec = ConfigSpecification() confspec = ConfigSpecification()
confspec.add_spec("spec1", StringSpec()) confspec.add_spec("spec1", StringSpec())
confspec2 = ConfigSpecification() interface.register_confspec(confspec)
confspec2.add_spec("spec2", StringSpec())
interface.register_confspec(confspec2)
def my_interface_function():
return 42
interface.register_function(my_interface_function)

@ -1,11 +1,51 @@
# pylint: disable=redefined-outer-name
import pytest import pytest
from shepherd.agent import plugin from shepherd.agent import plugin
def test_simple_plugin_load(request): @pytest.fixture
def simple_plugin(request):
interface = plugin.load_plugin("simpletestplugin", request.fspath.dirname)
return interface
def test_simple_plugin_load(simple_plugin: plugin.PluginInterface):
# If successful, will load as if it's a custom plugin # If successful, will load as if it's a custom plugin
assert simple_plugin._plugin_name == "simpletestplugin"
def test_simple_interface_function_load(simple_plugin: plugin.PluginInterface):
# Check register_function()
assert "my_interface_function" in simple_plugin._functions
@pytest.fixture
def simple_running_plugin(request):
interface = plugin.load_plugin("simpletestplugin", request.fspath.dirname) interface = plugin.load_plugin("simpletestplugin", request.fspath.dirname)
template_config = interface.confspec.get_template()
plugin.init_plugins({"simpletestplugin": template_config}, {"core_conf": "core_conf_vals"})
return interface
def test_simple_plugin_init(simple_running_plugin):
assert "simpletestplugin" in plugin.plugin_interfaces
def test_simple_interface_functions(simple_running_plugin):
# Check module level function dict
assert plugin.plugin_functions["simpletestplugin"]["my_interface_function"]() == 42
# Check functions handed back to plugin
assert simple_running_plugin.plugins["simpletestplugin"].my_interface_function() == 42
def test_dirty_plugin_load(request):
"""
Corner cases in plugin load
"""
interface = plugin.load_plugin("dirtytestplugin", request.fspath.dirname)
# Should prefer the confspec actually registered # Should prefer the confspec actually registered, even if declared after
assert "spec2" in interface.confspec.spec_dict assert "spec2" in interface.confspec.spec_dict
print(interface)

Loading…
Cancel
Save