code documentation update
This commit is contained in:
parent
820f7e5382
commit
1fb6f9c83a
|
@ -1,10 +1,10 @@
|
|||
**{{name}}** seamlessly finds the lml based plugins from your current python
|
||||
environment but loads your plugins on demand. It is designed to support
|
||||
plugins that have external dependencies, especially when they are bulky. {{name}}
|
||||
provides the plugin management system only and the plugin interface is on your
|
||||
shoulder, my dear developer.
|
||||
plugins that have external dependencies, especially when they are bulky and/or
|
||||
memory hungry. {{name}} provides the plugin management system only and the
|
||||
plugin interface is on your shoulder, my dear developer.
|
||||
|
||||
**{{name}}** enabled applications helps your developer in two ways:
|
||||
**{{name}}** enabled applications helps your customers [#f1]_ in two ways:
|
||||
|
||||
#. Your developer could cherry-pick the plugins from pypi.
|
||||
#. Only the plugins used at runtime enter into computer memory.
|
||||
|
@ -15,3 +15,5 @@ distributing the similar functionalities across
|
|||
its plugins. However, you as the developer need to do the code refactoring by
|
||||
yourself and lml would lend you a hand.
|
||||
|
||||
.. [#f1] the end developers who uses your library and packages achieve their
|
||||
objectives.
|
3
Makefile
3
Makefile
|
@ -4,8 +4,11 @@ test:
|
|||
bash test.sh
|
||||
|
||||
document:
|
||||
python setup.py install
|
||||
sphinx-build -b html docs/source/ docs/build
|
||||
|
||||
spelling:
|
||||
sphinx-build -b spelling docs/source/ docs/build/spelling
|
||||
|
||||
uml:
|
||||
plantuml -o ../_static/images/ docs/source/uml/*.uml
|
||||
|
|
11
README.rst
11
README.rst
|
@ -13,11 +13,11 @@ lml - Load me later. A lazy loading plugin management system.
|
|||
|
||||
**lml** seamlessly finds the lml based plugins from your current python
|
||||
environment but loads your plugins on demand. It is designed to support
|
||||
plugins that have external dependencies, especially when they are bulky. lml
|
||||
provides the plugin management system only and the plugin interface is on your
|
||||
shoulder, my dear developer.
|
||||
plugins that have external dependencies, especially when they are bulky and/or
|
||||
memory hungry. lml provides the plugin management system only and the
|
||||
plugin interface is on your shoulder, my dear developer.
|
||||
|
||||
**lml** enabled applications helps your developer in two ways:
|
||||
**lml** enabled applications helps your customer [#f1]_ in two ways:
|
||||
|
||||
#. Your developer could cherry-pick the plugins from pypi.
|
||||
#. Only the plugins used at runtime enter into computer memory.
|
||||
|
@ -28,7 +28,8 @@ distributing the similar functionalities across
|
|||
its plugins. However, you as the developer need to do the code refactoring by
|
||||
yourself and lml would lend you a hand.
|
||||
|
||||
|
||||
.. [#f1] the end developers who uses your library and packages achieve their
|
||||
objectives.
|
||||
Installation
|
||||
================================================================================
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
sphinxcontrib-spelling
|
||||
sphinxcontrib-plantuml
|
||||
|
||||
|
|
|
@ -39,6 +39,12 @@ If no chef was found, it prints the default string: I do not know.
|
|||
|
||||
Now let us move to plugin.py. Some of non-relevant lines were omitted here.
|
||||
|
||||
.. uml::
|
||||
|
||||
Chef <|-- Boost
|
||||
Chef <|-- Fry
|
||||
Chef <|-- Bake
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
API documentation
|
||||
====================
|
||||
|
||||
.. automodule:: lml.loader
|
||||
|
||||
.. autofunction:: scan_plugins
|
||||
|
||||
.. automodule:: lml.plugin
|
||||
|
||||
.. autoclass:: PluginInfo
|
||||
|
||||
.. autoclass:: PluginInfoChain
|
||||
|
||||
.. autoclass:: PluginManager
|
||||
:members:
|
||||
|
||||
.. autoclass:: Plugin
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
DESCRIPTION = (
|
||||
'Load me later. A lazy loading plugin management system.' +
|
||||
''
|
||||
|
@ -8,13 +9,17 @@ extensions = [
|
|||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinxcontrib.plantuml',
|
||||
'sphinxcontrib.spelling'
|
||||
]
|
||||
|
||||
tool = os.path.join(os.path.dirname(__file__), "tools/plantuml.jar")
|
||||
plantuml = 'java -Djava.awt.headless=true -jar %s' % tool
|
||||
plantuml_output_format = 'svg'
|
||||
templates_path = ['_templates']
|
||||
source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
imgpath = '_static'
|
||||
project = u'lml'
|
||||
copyright = u'2017 Onni Software Ltd.'
|
||||
version = '0.0.1'
|
||||
|
|
|
@ -69,6 +69,7 @@ With lml, as long as your third party developer respect the plugin name prefix,
|
|||
they could publish their plugins as they do to any normal pypi packages. And the end
|
||||
developer of yours would only need to do pip install.
|
||||
|
||||
|
||||
.. [#f1] https://packaging.python.org/namespace_packages/
|
||||
.. [#f2] http://yapsy.sourceforge.net/
|
||||
.. [#f3] https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld
|
||||
|
|
|
@ -21,11 +21,11 @@ Introduction
|
|||
|
||||
**lml** seamlessly finds the lml based plugins from your current python
|
||||
environment but loads your plugins on demand. It is designed to support
|
||||
plugins that have external dependencies, especially when they are bulky. lml
|
||||
provides the plugin management system only and the plugin interface is on your
|
||||
shoulder, my dear developer.
|
||||
plugins that have external dependencies, especially when they are bulky and/or
|
||||
memory hungry. lml provides the plugin management system only and the
|
||||
plugin interface is on your shoulder, my dear developer.
|
||||
|
||||
**lml** enabled applications helps your developer in two ways:
|
||||
**lml** enabled applications helps your customer [#f1]_ in two ways:
|
||||
|
||||
#. Your developer could cherry-pick the plugins from pypi.
|
||||
#. Only the plugins used at runtime enter into computer memory.
|
||||
|
@ -36,7 +36,8 @@ distributing the similar functionalities across
|
|||
its plugins. However, you as the developer need to do the code refactoring by
|
||||
yourself and lml would lend you a hand.
|
||||
|
||||
|
||||
.. [#f1] the end developers who uses your library and packages achieve their
|
||||
objectives.
|
||||
|
||||
Documentation
|
||||
----------------
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from lml.plugin import PluginInfoList
|
||||
from lml.plugin import PluginInfoChain
|
||||
|
||||
|
||||
PluginInfoList(__name__).add_a_plugin(
|
||||
PluginInfoChain(__name__).add_a_plugin(
|
||||
'cuisine',
|
||||
'electricity.Boost',
|
||||
tags=['Portable Battery']
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
lml.loader
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Auto discover avaiable plugins
|
||||
Auto discover available plugins
|
||||
|
||||
:copyright: (c) 2017 by Onni Software Ltd.
|
||||
:license: New BSD License, see LICENSE for more details
|
||||
|
@ -18,7 +18,36 @@ log = logging.getLogger(__name__)
|
|||
|
||||
def scan_plugins(prefix, path, black_list=None, white_list=None):
|
||||
"""
|
||||
Discover plugins via pkgutil and pyinstaller path
|
||||
Implicitly discover plugins via pkgutil and pyinstaller path
|
||||
|
||||
Parameters
|
||||
-----------------
|
||||
|
||||
prefix:string
|
||||
module prefix. This prefix should become the prefix of the module name
|
||||
of all plugins.
|
||||
|
||||
In the tutorial, robotchef-britishcuisine is a plugin package
|
||||
of robotchef and its module name is 'robotchef_britishcuisine'. When
|
||||
robotchef call scan_plugins to load its cuisine plugins, it specifies
|
||||
its prefix as "robotchef_". All modules that starts with 'robotchef_'
|
||||
will be auto-loaded: robotchef_britishcuisine, robotchef_chinesecuisine,
|
||||
etc.
|
||||
|
||||
path:string
|
||||
used in pyinstaller only. When your end developer would package
|
||||
your main library and its plugins using pyinstaller, this path
|
||||
helps pyinstaller to find the plugins.
|
||||
|
||||
black_list:list
|
||||
a list of module names that should be skipped.
|
||||
|
||||
white_list:list
|
||||
a list of modules that comes with your main module. If you have a
|
||||
built-in module, the module name should be inserted into the list.
|
||||
|
||||
For example, robot_cuisine is a built-in module inside robotchef. It
|
||||
is listed in white_list.
|
||||
"""
|
||||
if black_list is None:
|
||||
black_list = []
|
||||
|
|
111
lml/plugin.py
111
lml/plugin.py
|
@ -4,6 +4,18 @@
|
|||
|
||||
Plugin management system
|
||||
|
||||
:class:`~lml.plugin.PluginManager` should be inherited to form new
|
||||
plugin manager class. If you have more than one plugins in your
|
||||
architcture, it is advisable to have one class per plugin type.
|
||||
|
||||
:class:`~lml.plugin.PluginInfoChain` helps the plugin module to
|
||||
declare the avialable 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 by Onni Software Ltd.
|
||||
:license: New BSD License, see LICENSE for more details
|
||||
"""
|
||||
|
@ -20,29 +32,6 @@ CACHED_PLUGIN_INFO = defaultdict(list)
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Plugin(type):
|
||||
"""
|
||||
For ad-hoc plugin classes
|
||||
|
||||
In a situation where the intention is not to redistribute
|
||||
a plugin package, a dynamically written class is written
|
||||
as one off attempt to extend the main package.
|
||||
"""
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(Plugin, cls).__init__(
|
||||
name, bases, nmspc)
|
||||
_register_a_plugin(cls)
|
||||
|
||||
|
||||
def _register_a_plugin(cls):
|
||||
"""module level function to register a plugin"""
|
||||
manager = PLUG_IN_MANAGERS.get(cls.plugin_name)
|
||||
if manager:
|
||||
manager.register_a_plugin(cls, cls)
|
||||
else:
|
||||
raise Exception("%s has no registry" % cls.plugin_name)
|
||||
|
||||
|
||||
class PluginInfo(object):
|
||||
"""
|
||||
Information about the plugin
|
||||
|
@ -58,12 +47,13 @@ class PluginInfo(object):
|
|||
tags:
|
||||
a list of keywords help the plugin manager to retrieve your plugin
|
||||
"""
|
||||
def __init__(self, name, absolute_import_path, tags=None, **keywords):
|
||||
self.name = name
|
||||
def __init__(self, plugin_type, absolute_import_path,
|
||||
tags=None, **keywords):
|
||||
self.name = plugin_type
|
||||
self.absolute_import_path = absolute_import_path
|
||||
self.cls = None
|
||||
self.properties = keywords
|
||||
self.tags = tags
|
||||
self.__tags = tags
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == 'module_name':
|
||||
|
@ -71,16 +61,16 @@ class PluginInfo(object):
|
|||
return module_name
|
||||
return self.properties.get(name)
|
||||
|
||||
def keywords(self):
|
||||
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:
|
||||
if self.__tags is None:
|
||||
yield self.name
|
||||
else:
|
||||
for tag in self.tags:
|
||||
for tag in self.__tags:
|
||||
yield tag
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -89,7 +79,7 @@ class PluginInfo(object):
|
|||
return json_dumps(rep)
|
||||
|
||||
|
||||
class PluginInfoList(object):
|
||||
class PluginInfoChain(object):
|
||||
"""
|
||||
Pandas style, chained list declaration
|
||||
|
||||
|
@ -98,7 +88,7 @@ class PluginInfoList(object):
|
|||
def __init__(self, path):
|
||||
self.module_name = path
|
||||
|
||||
def add_a_plugin(self, name, submodule=None,
|
||||
def add_a_plugin(self, plugin_type, submodule=None,
|
||||
**keywords):
|
||||
"""
|
||||
Add a plain plugin
|
||||
|
@ -106,14 +96,14 @@ class PluginInfoList(object):
|
|||
Parameters
|
||||
-------------
|
||||
|
||||
name:
|
||||
plugin_type:
|
||||
plugin manager name
|
||||
|
||||
submodule:
|
||||
the relative import path to your plugin class
|
||||
"""
|
||||
a_plugin_info = PluginInfo(
|
||||
name,
|
||||
plugin_type,
|
||||
self._get_abs_path(submodule),
|
||||
**keywords)
|
||||
|
||||
|
@ -144,20 +134,32 @@ class PluginManager(object):
|
|||
"""
|
||||
Load plugin info into in-memory dictionary for later import
|
||||
"""
|
||||
def __init__(self, plugin_name):
|
||||
self.plugin_name = plugin_name
|
||||
def __init__(self, plugin_type):
|
||||
self.plugin_name = plugin_type
|
||||
self.registry = defaultdict(list)
|
||||
self._logger = logging.getLogger(
|
||||
self.__class__.__module__ + '.' + self.__class__.__name__)
|
||||
_register_class(self)
|
||||
|
||||
def get_a_plugin(self, key, **keywords):
|
||||
""" Get a plugin """
|
||||
self._logger.debug("get a plugin")
|
||||
plugin = self.load_me_now(key)
|
||||
return plugin()
|
||||
|
||||
def raise_exception(self, key):
|
||||
"""Raise plugin not found exception"""
|
||||
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
|
||||
"""
|
||||
self._logger.debug('load me later: ' + plugin_info.module_name)
|
||||
self._logger.debug(plugin_info)
|
||||
for key in plugin_info.keywords():
|
||||
for key in plugin_info.tags():
|
||||
self.registry[key.lower()].append(plugin_info)
|
||||
|
||||
def load_me_now(self, key, library=None, **keywords):
|
||||
|
@ -183,12 +185,6 @@ class PluginManager(object):
|
|||
else:
|
||||
self.raise_exception(key)
|
||||
|
||||
def raise_exception(self, key):
|
||||
"""Raise plugin not found exception"""
|
||||
self._logger.debug(self.registry.keys())
|
||||
raise Exception(
|
||||
"No %s is found for %s" % (self.plugin_name, key))
|
||||
|
||||
def dynamic_load_library(self, a_plugin_info):
|
||||
"""Dynamically load the plugin info if not loaded"""
|
||||
self._logger.debug("import " + a_plugin_info.absolute_import_path)
|
||||
|
@ -200,16 +196,10 @@ class PluginManager(object):
|
|||
def register_a_plugin(self, cls, plugin_info):
|
||||
""" for dynamically loaded plugin during runtime"""
|
||||
self._logger.debug("register " + cls.__name__)
|
||||
for key in plugin_info.keywords():
|
||||
for key in plugin_info.tags():
|
||||
plugin_info.cls = cls
|
||||
self.registry[key.lower()].append(plugin_info)
|
||||
|
||||
def get_a_plugin(self, key, **keywords):
|
||||
""" Get a plugin """
|
||||
self._logger.debug("get a plugin")
|
||||
plugin = self.load_me_now(key)
|
||||
return plugin()
|
||||
|
||||
|
||||
def _register_class(cls):
|
||||
"""Reigister a newly created plugin manager"""
|
||||
|
@ -224,6 +214,29 @@ def _register_class(cls):
|
|||
del CACHED_PLUGIN_INFO[cls.plugin_name]
|
||||
|
||||
|
||||
class Plugin(type):
|
||||
"""
|
||||
For ad-hoc plugin classes
|
||||
|
||||
In a situation where the intention is not to redistribute
|
||||
a plugin package, a dynamically written class is written
|
||||
as one off attempt to extend the main package.
|
||||
"""
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(Plugin, cls).__init__(
|
||||
name, bases, nmspc)
|
||||
_register_a_plugin(cls)
|
||||
|
||||
|
||||
def _register_a_plugin(cls):
|
||||
"""module level function to register a plugin"""
|
||||
manager = PLUG_IN_MANAGERS.get(cls.plugin_name)
|
||||
if manager:
|
||||
manager.register_a_plugin(cls, cls)
|
||||
else:
|
||||
raise Exception("%s has no registry" % cls.plugin_name)
|
||||
|
||||
|
||||
def _load_me_later(plugin_info):
|
||||
""" module level function to load a plugin later"""
|
||||
log.debug("load me later")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from lml.registry import PluginInfoList
|
||||
from lml.registry import PluginInfoChain
|
||||
|
||||
|
||||
__test_plugins__ = PluginInfoList(__name__).add_a_plugin(
|
||||
__test_plugins__ = PluginInfoChain(__name__).add_a_plugin(
|
||||
'test_io2', 'reader')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from lml.plugin import PluginInfoList
|
||||
from lml.plugin import PluginInfoChain
|
||||
|
||||
|
||||
__test_plugins__ = PluginInfoList(__name__).add_a_plugin(
|
||||
__test_plugins__ = PluginInfoChain(__name__).add_a_plugin(
|
||||
'test_io', 'x')
|
||||
|
|
|
@ -6,7 +6,7 @@ from nose.tools import eq_
|
|||
def test_plugin_info():
|
||||
info = PluginInfo('renderer', 'abs import path', custom='property')
|
||||
assert info.custom == 'property'
|
||||
keys = list(info.keywords())
|
||||
keys = list(info.tags())
|
||||
assert len(keys) == 1
|
||||
assert keys[0] == 'renderer'
|
||||
expected = {"path": "abs import path",
|
||||
|
|
|
@ -135,7 +135,7 @@ def test_register_a_plugin_function_1():
|
|||
plugin_name = 'test plugin'
|
||||
|
||||
@classmethod
|
||||
def keywords(self):
|
||||
def tags(self):
|
||||
yield 'akey'
|
||||
|
||||
MyPlugin()
|
||||
|
@ -148,7 +148,7 @@ def test_register_a_plugin_function_2():
|
|||
plugin_name = 'I have no plugin manager'
|
||||
|
||||
@classmethod
|
||||
def keywords(self):
|
||||
def tags(self):
|
||||
yield 'akey'
|
||||
|
||||
MyPlugin()
|
||||
|
|
Loading…
Reference in New Issue