Source code for girder.plugin

"""
This module defines functions for registering, loading, and querying girder plugins.
"""

import distutils.dist
from functools import wraps
import io
import json
import os
from pkg_resources import iter_entry_points, resource_filename

from girder import logprint
from girder.exceptions import GirderException


_NAMESPACE = 'girder.plugin'
_pluginRegistry = None
_pluginLoadOrder = []
_pluginWebroots = {}


def getPluginWebroots():
    global _pluginWebroots
    return _pluginWebroots


[docs]def registerPluginWebroot(webroot, name): """ Adds a webroot to the global registry for plugins based on the plugin name. """ global _pluginWebroots _pluginWebroots[name] = webroot
class _PluginMeta(type): """ This is a metaclass applied to the ``GirderPlugin`` descriptor class. It exists to automatically wrap subclass load methods. """ def __new__(cls, classname, bases, classdict): if 'load' in classdict: classdict['load'] = _PluginMeta._wrapPluginLoad(classdict['load']) return type.__new__(cls, classname, bases, classdict) @staticmethod def _wrapPluginLoad(func): """Wrap a plugin load method to provide logging and ensure it is loaded only once.""" @wraps(func) def wrapper(self, *args, **kwargs): if not getattr(self, '_loaded', False): # This block is executed on the first call to the function. # The return value of the call is saved an attribute on the wrapper # for future invocations. self._return = func(self, *args, **kwargs) self._loaded = True _pluginLoadOrder.append(self.name) logprint.success('Loaded plugin "%s"' % self.name) return self._return return wrapper
[docs]class GirderPlugin(metaclass=_PluginMeta): """ This is a base class for describing a girder plugin. A plugin is registered by adding an entrypoint under the namespace ``girder.plugin``. This entrypoint should return a class derived from this class. Example :: class Cats(GirderPlugin): def load(self, info): # load dependent plugins girder.plugin.getPlugin('pets').load(info) import rest # register new rest endpoints, etc. """ #: This is the named displayed to users on the plugin page. Unlike the entrypoint name #: used internally, this name can be an arbitrary string. DISPLAY_NAME = None #: The path of the plugin's web client source code. This path is given relative to the python #: package. This property is used to link the web client source into the staging area while #: building in development mode. When this value is `None` it indicates there is no web client #: component. CLIENT_SOURCE_PATH = None def __init__(self, entrypoint): self._name = entrypoint.name self._loaded = False self._dist = entrypoint.dist self._metadata = _readPackageMetadata(self._dist)
[docs] def npmPackages(self): """Return a dictionary of npm packages -> versions for building the plugin client. By default, this dictionary will be assembled from the CLIENT_SOURCE_PATH property by inspecting the package.json file in the indicated directory. Plugins can override this method customize the behavior for advanced usage. """ if self.CLIENT_SOURCE_PATH is None: return {} packageJsonFile = resource_filename( self.__module__, os.path.join(self.CLIENT_SOURCE_PATH, 'package.json') ) if not os.path.isfile(packageJsonFile): raise Exception('Invalid web client path provided: %s' % packageJsonFile) with open(packageJsonFile) as f: packageJson = json.load(f) packageName = packageJson['name'] return {packageName: 'file:%s' % os.path.dirname(packageJsonFile)}
@property def name(self): """Return the plugin name defaulting to the entrypoint name.""" return self._name @property def displayName(self): """Return a user-friendly plugin name (defaults to the entrypoint name).""" return self.DISPLAY_NAME or self._name @property def description(self): """Return the plugin description defaulting to the classes docstring.""" return self._metadata.description @property def url(self): """Return a url reference to the plugin (usually a readthedocs page).""" return self._metadata.url @property def version(self): """Return the version of the plugin automatically determined from setup.py.""" return self._metadata.version @property def loaded(self): """Return true if this plugin has been loaded.""" return getattr(self, '_loaded', False) def load(self, info): raise NotImplementedError('Plugins must define a load method')
def _readPackageMetadata(distribution): """Get a metadata object associated with a python package.""" metadata_string = distribution.get_metadata(distribution.PKG_INFO) metadata = distutils.dist.DistributionMetadata() metadata.read_pkg_file(io.StringIO(metadata_string)) return metadata def _getPluginRegistry(): """Return a dictionary containing all detected plugins. This function will discover plugins registered via entrypoints and return a mapping of plugin name -> plugin definition. The result is memoized because iteration through entrypoints is a slow operation. """ global _pluginRegistry if _pluginRegistry is not None: return _pluginRegistry _pluginRegistry = {} for entryPoint in iter_entry_points(_NAMESPACE): pluginClass = entryPoint.load() plugin = pluginClass(entryPoint) _pluginRegistry[plugin.name] = plugin return _pluginRegistry
[docs]def getPlugin(name): """Return a plugin configuration object or None if the plugin is not found.""" registry = _getPluginRegistry() return registry.get(name)
def _loadPlugins(info, names=None): """Load plugins with the given app info object. If `names` is None, all installed plugins will be loaded. If `names` is a list, then only those plugins in the provided list will be loaded. """ if names is None: names = allPlugins() for name in names: pluginObject = getPlugin(name) if pluginObject is None: raise GirderException('Plugin %s is not installed' % name) pluginObject.load(info)
[docs]def allPlugins(): """Return a list of all detected plugins.""" return list(_getPluginRegistry().keys())
[docs]def loadedPlugins(): """Return a list of successfully loaded plugins.""" return _pluginLoadOrder[:]