397 lines
12 KiB
Python
397 lines
12 KiB
Python
"""
|
|
lml.plugin
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
lml divides the plugins into two category: load-me-later plugins and
|
|
load-me-now ones. load-me-later plugins refer to the plugins were
|
|
loaded when needed due its bulky and/or memory hungry dependencies.
|
|
Those plugins has to use lml and respect lml's design principle.
|
|
|
|
load-me-now plugins refer to the plugins are immediately imported. All
|
|
conventional Python classes are by default immediately imported.
|
|
|
|
:class:`~lml.plugin.PluginManager` should be inherited to form new
|
|
plugin manager class. If you have more than one plugins in your
|
|
architecture, it is advisable to have one class per plugin type.
|
|
|
|
:class:`~lml.plugin.PluginInfoChain` helps the plugin module to
|
|
declare the available plugins in the module.
|
|
|
|
:class:`~lml.plugin.PluginInfo` can be subclassed to describe
|
|
your plugin. Its method :meth:`~lml.plugin.PluginInfo.tags`
|
|
can be overridden to help its matching :class:`~lml.plugin.PluginManager`
|
|
to look itself up.
|
|
|
|
:copyright: (c) 2017-2018 by Onni Software Ltd.
|
|
:license: New BSD License, see LICENSE for more details
|
|
"""
|
|
import logging
|
|
from collections import defaultdict
|
|
|
|
from lml.utils import json_dumps, do_import_class
|
|
|
|
PLUG_IN_MANAGERS = {}
|
|
CACHED_PLUGIN_INFO = defaultdict(list)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class PluginInfo(object):
|
|
"""
|
|
Information about the plugin.
|
|
|
|
It is used together with PluginInfoChain to describe the plugins.
|
|
Meanwhile, it is a class decorator and can be used to register a plugin
|
|
immediately for use, in other words, the PluginInfo decorated plugin
|
|
class is not loaded later.
|
|
|
|
Parameters
|
|
-------------
|
|
name:
|
|
plugin name
|
|
|
|
absolute_import_path:
|
|
absolute import path from your plugin name space for your plugin class
|
|
|
|
tags:
|
|
a list of keywords help the plugin manager to retrieve your plugin
|
|
|
|
keywords:
|
|
Another custom properties.
|
|
|
|
Examples
|
|
-------------
|
|
|
|
For load-me-later plugins:
|
|
|
|
>>> info = PluginInfo("sample",
|
|
... abs_class_path='lml.plugin.PluginInfo', # demonstration only.
|
|
... tags=['load-me-later'],
|
|
... custom_property = 'I am a custom property')
|
|
>>> print(info.module_name)
|
|
lml
|
|
>>> print(info.custom_property)
|
|
I am a custom property
|
|
|
|
For load-me-now plugins:
|
|
|
|
>>> @PluginInfo("sample", tags=['load-me-now'])
|
|
... class TestPlugin:
|
|
... def echo(self, words):
|
|
... print("echoing %s" % words)
|
|
|
|
Now let's retrive the second plugin back:
|
|
|
|
>>> class SamplePluginManager(PluginManager):
|
|
... def __init__(self):
|
|
... PluginManager.__init__(self, "sample")
|
|
>>> sample_manager = SamplePluginManager()
|
|
>>> test_plugin=sample_manager.get_a_plugin("load-me-now")
|
|
>>> test_plugin.echo("hey..")
|
|
echoing hey..
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self, plugin_type, abs_class_path=None, tags=None, **keywords
|
|
):
|
|
self.plugin_type = plugin_type
|
|
self.absolute_import_path = abs_class_path
|
|
self.cls = None
|
|
self.properties = keywords
|
|
self.__tags = tags
|
|
|
|
def __getattr__(self, name):
|
|
if name == "module_name":
|
|
if self.absolute_import_path:
|
|
module_name = self.absolute_import_path.split(".")[0]
|
|
else:
|
|
module_name = self.cls.__module__
|
|
return module_name
|
|
return self.properties.get(name)
|
|
|
|
def tags(self):
|
|
"""
|
|
A list of tags for identifying the plugin class
|
|
|
|
The plugin class is described at the absolute_import_path
|
|
"""
|
|
if self.__tags is None:
|
|
yield self.plugin_type
|
|
else:
|
|
for tag in self.__tags:
|
|
yield tag
|
|
|
|
def __repr__(self):
|
|
rep = {
|
|
"plugin_type": self.plugin_type,
|
|
"path": self.absolute_import_path,
|
|
}
|
|
rep.update(self.properties)
|
|
return json_dumps(rep)
|
|
|
|
def __call__(self, cls):
|
|
self.cls = cls
|
|
_register_a_plugin(self, cls)
|
|
return cls
|
|
|
|
|
|
class PluginInfoChain(object):
|
|
"""
|
|
Pandas style, chained list declaration
|
|
|
|
It is used in the plugin packages to list all plugin classes
|
|
"""
|
|
|
|
def __init__(self, path):
|
|
self._logger = logging.getLogger(
|
|
self.__class__.__module__ + "." + self.__class__.__name__
|
|
)
|
|
self.module_name = path
|
|
|
|
def add_a_plugin(self, plugin_type, submodule=None, **keywords):
|
|
"""
|
|
Add a plain plugin
|
|
|
|
Parameters
|
|
-------------
|
|
|
|
plugin_type:
|
|
plugin manager name
|
|
|
|
submodule:
|
|
the relative import path to your plugin class
|
|
"""
|
|
a_plugin_info = PluginInfo(
|
|
plugin_type, self._get_abs_path(submodule), **keywords
|
|
)
|
|
|
|
self.add_a_plugin_instance(a_plugin_info)
|
|
return self
|
|
|
|
def add_a_plugin_instance(self, plugin_info_instance):
|
|
"""
|
|
Add a plain plugin
|
|
|
|
Parameters
|
|
-------------
|
|
|
|
plugin_info_instance:
|
|
an instance of PluginInfo
|
|
|
|
The developer has to specify the absolute import path
|
|
"""
|
|
self._logger.debug(
|
|
"add %s as '%s' plugin",
|
|
plugin_info_instance.absolute_import_path,
|
|
plugin_info_instance.plugin_type,
|
|
)
|
|
_load_me_later(plugin_info_instance)
|
|
return self
|
|
|
|
def _get_abs_path(self, submodule):
|
|
return "%s.%s" % (self.module_name, submodule)
|
|
|
|
|
|
class PluginManager(object):
|
|
"""
|
|
Load plugin info into in-memory dictionary for later import
|
|
|
|
Parameters
|
|
--------------
|
|
|
|
plugin_type:
|
|
the plugin type. All plugins of this plugin type will be
|
|
registered to it.
|
|
"""
|
|
|
|
def __init__(self, plugin_type):
|
|
self.plugin_name = plugin_type
|
|
self.registry = defaultdict(list)
|
|
self.tag_groups = dict()
|
|
self._logger = logging.getLogger(
|
|
self.__class__.__module__ + "." + self.__class__.__name__
|
|
)
|
|
_register_class(self)
|
|
|
|
def get_a_plugin(self, key, **keywords):
|
|
""" Get a plugin
|
|
|
|
Parameters
|
|
---------------
|
|
|
|
key:
|
|
the key to find the plugins
|
|
|
|
keywords:
|
|
additional parameters for help the retrieval of the plugins
|
|
"""
|
|
self._logger.debug("get a plugin called")
|
|
plugin = self.load_me_now(key)
|
|
return plugin()
|
|
|
|
def raise_exception(self, key):
|
|
"""Raise plugin not found exception
|
|
|
|
Override this method to raise custom exception
|
|
|
|
Parameters
|
|
-----------------
|
|
|
|
key:
|
|
the key to find the plugin
|
|
"""
|
|
self._logger.debug(self.registry.keys())
|
|
raise Exception("No %s is found for %s" % (self.plugin_name, key))
|
|
|
|
def load_me_later(self, plugin_info):
|
|
"""
|
|
Register a plugin info for later loading
|
|
|
|
Parameters
|
|
--------------
|
|
|
|
plugin_info:
|
|
a instance of plugin info
|
|
"""
|
|
self._logger.debug("load %s later", plugin_info.absolute_import_path)
|
|
for key in plugin_info.tags():
|
|
self.registry[key.lower()].append(plugin_info)
|
|
|
|
def load_me_now(self, key, library=None, **keywords):
|
|
"""
|
|
Import a plugin from plugin registry
|
|
|
|
Parameters
|
|
-----------------
|
|
|
|
key:
|
|
the key to find the plugin
|
|
|
|
library:
|
|
to use a specific plugin module
|
|
"""
|
|
if keywords:
|
|
self._logger.debug(keywords)
|
|
__key = key.lower()
|
|
if __key in self.registry:
|
|
for plugin_info in self.registry[__key]:
|
|
cls = self.dynamic_load_library(plugin_info)
|
|
module_name = _get_me_pypi_package_name(cls)
|
|
if library and module_name != library:
|
|
continue
|
|
else:
|
|
break
|
|
else:
|
|
# only library condition coud raise an exception
|
|
raise Exception("%s is not installed" % library)
|
|
self._logger.debug("load %s now for '%s'", cls, key)
|
|
return cls
|
|
else:
|
|
self.raise_exception(key)
|
|
|
|
def dynamic_load_library(self, a_plugin_info):
|
|
"""Dynamically load the plugin info if not loaded
|
|
|
|
|
|
Parameters
|
|
--------------
|
|
|
|
a_plugin_info:
|
|
a instance of plugin info
|
|
"""
|
|
if a_plugin_info.cls is None:
|
|
self._logger.debug("import " + a_plugin_info.absolute_import_path)
|
|
cls = do_import_class(a_plugin_info.absolute_import_path)
|
|
a_plugin_info.cls = cls
|
|
return a_plugin_info.cls
|
|
|
|
def register_a_plugin(self, plugin_cls, plugin_info):
|
|
""" for dynamically loaded plugin during runtime
|
|
|
|
Parameters
|
|
--------------
|
|
|
|
plugin_cls:
|
|
the actual plugin class refered to by the second parameter
|
|
|
|
plugin_info:
|
|
a instance of plugin info
|
|
"""
|
|
self._logger.debug("register %s", _show_me_your_name(plugin_cls))
|
|
primary_tag = None
|
|
for index, key in enumerate(plugin_info.tags()):
|
|
plugin_info.cls = plugin_cls
|
|
self.registry[key.lower()].append(plugin_info)
|
|
if index == 0:
|
|
primary_tag = key.lower()
|
|
self.tag_groups[key.lower()] = primary_tag
|
|
|
|
def get_primary_key(self, key):
|
|
__key = key.lower()
|
|
return self.tag_groups.get(__key, None)
|
|
|
|
|
|
def _register_class(cls):
|
|
"""Reigister a newly created plugin manager"""
|
|
log.debug("declare '%s' plugin manager", cls.plugin_name)
|
|
PLUG_IN_MANAGERS[cls.plugin_name] = cls
|
|
if cls.plugin_name in CACHED_PLUGIN_INFO:
|
|
# check if there is early registrations or not
|
|
for plugin_info in CACHED_PLUGIN_INFO[cls.plugin_name]:
|
|
if plugin_info.absolute_import_path:
|
|
log.debug(
|
|
"load cached plugin info: %s",
|
|
plugin_info.absolute_import_path,
|
|
)
|
|
else:
|
|
log.debug(
|
|
"load cached plugin info: %s",
|
|
_show_me_your_name(plugin_info.cls),
|
|
)
|
|
cls.load_me_later(plugin_info)
|
|
|
|
del CACHED_PLUGIN_INFO[cls.plugin_name]
|
|
|
|
|
|
def _register_a_plugin(plugin_info, plugin_cls):
|
|
"""module level function to register a plugin"""
|
|
manager = PLUG_IN_MANAGERS.get(plugin_info.plugin_type)
|
|
if manager:
|
|
manager.register_a_plugin(plugin_cls, plugin_info)
|
|
else:
|
|
# let's cache it and wait the manager to be registered
|
|
log.debug("caching %s", _show_me_your_name(plugin_cls.__name__))
|
|
CACHED_PLUGIN_INFO[plugin_info.plugin_type].append(plugin_info)
|
|
|
|
|
|
def _load_me_later(plugin_info):
|
|
""" module level function to load a plugin later"""
|
|
manager = PLUG_IN_MANAGERS.get(plugin_info.plugin_type)
|
|
if manager:
|
|
manager.load_me_later(plugin_info)
|
|
else:
|
|
# let's cache it and wait the manager to be registered
|
|
log.debug(
|
|
"caching %s for %s",
|
|
plugin_info.absolute_import_path,
|
|
plugin_info.plugin_type,
|
|
)
|
|
CACHED_PLUGIN_INFO[plugin_info.plugin_type].append(plugin_info)
|
|
|
|
|
|
def _get_me_pypi_package_name(module):
|
|
try:
|
|
module_name = module.__module__
|
|
root_module_name = module_name.split(".")[0]
|
|
return root_module_name.replace("_", "-")
|
|
except AttributeError:
|
|
return None
|
|
|
|
|
|
def _show_me_your_name(cls_func_or_data_type):
|
|
try:
|
|
return cls_func_or_data_type.__name__
|
|
except AttributeError:
|
|
return str(type(cls_func_or_data_type))
|