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