code documentation update

This commit is contained in:
chfw 2017-05-20 12:45:30 +01:00
parent 820f7e5382
commit 1fb6f9c83a
16 changed files with 152 additions and 76 deletions

View File

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

View File

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

View File

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

View File

@ -1 +1,3 @@
sphinxcontrib-spelling
sphinxcontrib-plantuml

View File

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

View File

@ -1,5 +1,18 @@
API documentation
====================
.. automodule:: lml.loader
.. autofunction:: scan_plugins
.. automodule:: lml.plugin
.. autoclass:: PluginInfo
.. autoclass:: PluginInfoChain
.. autoclass:: PluginManager
:members:
.. autoclass:: Plugin

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View File

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