|
|
|
|
@ -212,6 +212,44 @@ def plugin_attachment(hook_identifier):
|
|
|
|
|
return attachment_decorator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
InitMarker = namedtuple("InitMarker", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def plugin_init(func=None):
|
|
|
|
|
"""
|
|
|
|
|
Method decorator to register a method as a plugin init function, similar
|
|
|
|
|
to passing it to `interface.register_init()`.
|
|
|
|
|
|
|
|
|
|
Can either be used on functions in the root level of the plugin module, or
|
|
|
|
|
on methods within the registered Plugin Class (either with @plugin_class or
|
|
|
|
|
interface.register_plugin_class() )
|
|
|
|
|
"""
|
|
|
|
|
if func is None:
|
|
|
|
|
return plugin_init
|
|
|
|
|
|
|
|
|
|
func._shepherd_load_marker = InitMarker()
|
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RunMarker = namedtuple("RunMarker", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def plugin_run(func=None):
|
|
|
|
|
"""
|
|
|
|
|
Method decorator to register a method as a plugin run function, similar
|
|
|
|
|
to passing it to `interface.register_run()`.
|
|
|
|
|
|
|
|
|
|
Can either be used on functions in the root level of the plugin module, or
|
|
|
|
|
on methods within the registered Plugin Class (either with @plugin_class or
|
|
|
|
|
interface.register_plugin_class() )
|
|
|
|
|
"""
|
|
|
|
|
if func is None:
|
|
|
|
|
return plugin_run
|
|
|
|
|
|
|
|
|
|
func._shepherd_load_marker = RunMarker()
|
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@preserve.preservable(exclude_attrs=('function'))
|
|
|
|
|
class InterfaceCall():
|
|
|
|
|
def __init__(self, plugin_name, function_name, kwargs=None):
|
|
|
|
|
@ -386,6 +424,8 @@ class PluginInterface():
|
|
|
|
|
self._tasks = []
|
|
|
|
|
self._plugin_class = None
|
|
|
|
|
self._plugin_obj = None
|
|
|
|
|
self._init_func = None
|
|
|
|
|
self._run_func = None
|
|
|
|
|
self.config = None
|
|
|
|
|
self.plugins = None
|
|
|
|
|
self.hooks = None
|
|
|
|
|
@ -502,6 +542,54 @@ class PluginInterface():
|
|
|
|
|
self._hooks[name] = PluginHook(name, signature)
|
|
|
|
|
return self._hooks[name]
|
|
|
|
|
|
|
|
|
|
def register_init(self, func):
|
|
|
|
|
"""
|
|
|
|
|
Register a function or method as the init function for the plugin. This will be called
|
|
|
|
|
when the plugin is initialised, after load. This is where the plugin can do any setup
|
|
|
|
|
required before hooks and interface functions may be called by other plugins. Plugin config
|
|
|
|
|
is available during this call.
|
|
|
|
|
|
|
|
|
|
Plugin init is also where any tasks may be added.
|
|
|
|
|
|
|
|
|
|
The plugin init function cannot take any arguments.
|
|
|
|
|
|
|
|
|
|
The plugin init function is analogous to the `__init__` method when using a plugin class is
|
|
|
|
|
registered. If both an init function _and_ a plugin class are registered, both the init
|
|
|
|
|
function and the `__init__` method will be called.
|
|
|
|
|
"""
|
|
|
|
|
self._load_guard()
|
|
|
|
|
|
|
|
|
|
if self._init_func is not None:
|
|
|
|
|
raise PluginLoadError("Plugin can only register one init function")
|
|
|
|
|
if not callable(func):
|
|
|
|
|
raise TypeError("Plugin init function must be a callable.")
|
|
|
|
|
if len(inspect.signature(func).parameters) > 0:
|
|
|
|
|
raise TypeError("Plugin init function cannot take any arguments")
|
|
|
|
|
self._init_func = func
|
|
|
|
|
|
|
|
|
|
def register_run(self, func):
|
|
|
|
|
"""
|
|
|
|
|
Register a function or method to be called in a seperate thread once all plugins are
|
|
|
|
|
initialised. This function is intended to be used for any continuous loop needed by the
|
|
|
|
|
plugin, to avoid blocking other plugins or Shepherd itself. When the "run" function is
|
|
|
|
|
called, all other plugin hooks and interface functions are available to be called.
|
|
|
|
|
|
|
|
|
|
The plugin "run" function cannot take any arguments.
|
|
|
|
|
|
|
|
|
|
If trying to register a method on a plugin class, it is better to use the decorator form
|
|
|
|
|
"@plugin_run", as this will then bind to the actual instane of the class once it is
|
|
|
|
|
instantiated.
|
|
|
|
|
"""
|
|
|
|
|
self._load_guard()
|
|
|
|
|
|
|
|
|
|
if self._run_func is not None:
|
|
|
|
|
raise PluginLoadError("Plugin can only register one run function")
|
|
|
|
|
if not callable(func):
|
|
|
|
|
raise TypeError("Plugin run function must be a callable.")
|
|
|
|
|
if len(inspect.signature(func).parameters) > 0:
|
|
|
|
|
raise TypeError("Plugin run function cannot take any arguments")
|
|
|
|
|
self._run_func = func
|
|
|
|
|
|
|
|
|
|
def add_task(self, trigger, interface_function, kwargs=None):
|
|
|
|
|
"""
|
|
|
|
|
Add a task when creating a new session. Can only be called during init (object or hook).
|
|
|
|
|
@ -713,6 +801,10 @@ def load_plugin_interface(plugin_name, interface, module=None):
|
|
|
|
|
setattr(module, key, newhook)
|
|
|
|
|
elif isinstance(attr._shepherd_load_marker, ClassMarker):
|
|
|
|
|
interface.register_class(attr)
|
|
|
|
|
elif isinstance(attr._shepherd_load_marker, InitMarker):
|
|
|
|
|
interface.register_init(attr)
|
|
|
|
|
elif isinstance(attr._shepherd_load_marker, RunMarker):
|
|
|
|
|
interface.register_run(attr)
|
|
|
|
|
|
|
|
|
|
if interface._plugin_class is not None:
|
|
|
|
|
# Scan plugin class for marked methods
|
|
|
|
|
@ -729,6 +821,10 @@ def load_plugin_interface(plugin_name, interface, module=None):
|
|
|
|
|
# Hooks are a little different in that we replace the attr with the hook
|
|
|
|
|
newhook = interface.register_hook(**attr._shepherd_load_marker._asdict())
|
|
|
|
|
setattr(interface._plugin_class, key, newhook)
|
|
|
|
|
elif isinstance(attr._shepherd_load_marker, InitMarker):
|
|
|
|
|
interface.register_init(UnboundMethod(attr))
|
|
|
|
|
elif isinstance(attr._shepherd_load_marker, RunMarker):
|
|
|
|
|
interface.register_run(UnboundMethod(attr))
|
|
|
|
|
|
|
|
|
|
# Assemble remote interface function specs
|
|
|
|
|
|
|
|
|
|
@ -759,21 +855,18 @@ def init_plugins(plugin_configs, plugin_dir=None):
|
|
|
|
|
plugin_interfaces[plugin_name] = load_plugin(plugin_name, plugin_dir)
|
|
|
|
|
|
|
|
|
|
interface_functions = {}
|
|
|
|
|
interface_functions_proxy = MappingProxyType(interface_functions)
|
|
|
|
|
|
|
|
|
|
# Run plugin init and init hooks
|
|
|
|
|
for plugin_name, interface in plugin_interfaces.items():
|
|
|
|
|
# Though we set `plugins` to the proxy, it's empty until after init
|
|
|
|
|
interface.plugins = interface_functions_proxy
|
|
|
|
|
interface.config = plugin_configs[plugin_name]
|
|
|
|
|
# Collect interface functions from this plugin
|
|
|
|
|
interface_functions[plugin_name] = NamespaceProxy(interface._functions)
|
|
|
|
|
|
|
|
|
|
# TODO This probably should be technically done after all plugins have finished init? Could
|
|
|
|
|
# then go in loop that also does the interface functions proxy
|
|
|
|
|
interface.hooks = NamespaceProxy(interface._hooks)
|
|
|
|
|
# Provide config for plugin init
|
|
|
|
|
interface.config = plugin_configs[plugin_name]
|
|
|
|
|
|
|
|
|
|
# If it has one, instantiate the plugin object and bind methods to it.
|
|
|
|
|
if interface._plugin_class is not None:
|
|
|
|
|
# Special case with the 'shepherd' plugin where it is already instantiated
|
|
|
|
|
# Special case with the 'shepherd' plugin already populates `_plugin_obj`
|
|
|
|
|
if interface._plugin_obj is None:
|
|
|
|
|
interface._plugin_obj = interface._plugin_class()
|
|
|
|
|
|
|
|
|
|
@ -785,10 +878,15 @@ def init_plugins(plugin_configs, plugin_dir=None):
|
|
|
|
|
if isinstance(attachment.func, UnboundMethod):
|
|
|
|
|
attachment.func = attachment.func.bind(interface._plugin_obj)
|
|
|
|
|
|
|
|
|
|
# TODO Probably need special case for looking for `init` attachments, and run init here,
|
|
|
|
|
# before other attachments. Or can we rely on populating `.hooks` later, and that only we
|
|
|
|
|
# have access to attachments at this point? Probably. I mean _other_ plugins will have
|
|
|
|
|
# attached to this plugin's hooks at this point.
|
|
|
|
|
if isinstance(interface._init_func, UnboundMethod):
|
|
|
|
|
interface._init_func = interface._init_func.bind(interface._plugin_obj)
|
|
|
|
|
|
|
|
|
|
if isinstance(interface._run_func, UnboundMethod):
|
|
|
|
|
interface._run_func = interface._run_func.bind(interface._plugin_obj)
|
|
|
|
|
|
|
|
|
|
# Call the plugin init func (we've already done any plugin class instance __init__ above)
|
|
|
|
|
if interface._init_func is not None:
|
|
|
|
|
interface._init_func()
|
|
|
|
|
|
|
|
|
|
# Find hooks attachments are referring to and attach them
|
|
|
|
|
for attachment in interface._attachments:
|
|
|
|
|
@ -806,13 +904,14 @@ def init_plugins(plugin_configs, plugin_dir=None):
|
|
|
|
|
plugin_interfaces[hook_plugin_name]._hooks[hook_name]._attach(attachment.func,
|
|
|
|
|
plugin_name)
|
|
|
|
|
|
|
|
|
|
# TODO We've run the object __init__, but not the init hook, that needs to be done
|
|
|
|
|
interface._initialised = True
|
|
|
|
|
|
|
|
|
|
# Wait until all plugins have run their init before filling in and giving access
|
|
|
|
|
# to all the interface functions.
|
|
|
|
|
# to all the interface functions and hooks
|
|
|
|
|
interface_functions_proxy = MappingProxyType(interface_functions)
|
|
|
|
|
for plugin_name, interface in plugin_interfaces.items():
|
|
|
|
|
# Each plugin has a NamespaceProxy of it's interface functions for read-only attr access
|
|
|
|
|
interface_functions[plugin_name] = NamespaceProxy(interface._functions)
|
|
|
|
|
# Each plugin has a NamespaceProxy of its interface functions for read-only attr access
|
|
|
|
|
interface.plugins = interface_functions_proxy
|
|
|
|
|
interface.hooks = NamespaceProxy(interface._hooks)
|
|
|
|
|
|
|
|
|
|
return plugin_interfaces
|
|
|
|
|
|