Source code for girder.plugin

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

import hashlib
import importlib.metadata
import importlib.resources
import logging
from collections import OrderedDict
from collections import OrderedDict as OrderedDictType
from dataclasses import dataclass
from functools import wraps
from pathlib import Path

from girder import __version__
from girder.exceptions import GirderException

logger = logging.getLogger(__name__)


[docs] @dataclass class PluginStaticContent: css: list[str] js: list[str]
_NAMESPACE = 'girder.plugin' _pluginRegistry = None _pluginLoadOrder = [] _pluginStaticContent: OrderedDictType[str, PluginStaticContent] = OrderedDict() def getPluginStaticContent(): return _pluginStaticContent def registerPluginStaticContent(plugin: str, css: list[str], js: list[str], staticDir: Path, tree): from girder.utility.server import _errorDefault if plugin not in _pluginStaticContent: tree.mount( None, f'/plugin_static/{plugin}', { '/': { 'tools.staticdir.on': True, 'tools.staticdir.dir': str(staticDir), 'request.show_tracebacks': False, 'response.headers.server': f'Girder {__version__}', 'error_page.default': _errorDefault, } }, ) def cache_bust_url(filename: str) -> str: filename = filename.lstrip('/') if '?' in filename: # For now, we assume this means the plugin is managing its own cache busting return f'plugin_static/{plugin}/{filename}' hash_md5 = hashlib.md5() with (staticDir / filename).open('rb') as f: for chunk in iter(lambda: f.read(4096), b''): hash_md5.update(chunk) return f'plugin_static/{plugin}/{filename}?h={hash_md5.hexdigest()[:10]}' _pluginStaticContent[plugin] = PluginStaticContent( css=[cache_bust_url(f) for f in css], js=[cache_bust_url(f) for f in js], ) 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) logger.info('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 def __init__(self, entrypoint): self._name = entrypoint.name self._loaded = False self._dist = ( entrypoint.dist if hasattr(entrypoint, 'dist') else importlib.metadata.distribution(entrypoint.value.split(':')[0].split('.')[0])) self._metadata = _readPackageMetadata(self._dist) @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.get('Summary', self._metadata.get('description', '')) @property def url(self): """Return a url reference to the plugin (usually a readthedocs page).""" return self._metadata.get('url', self._metadata.get('Home-page', '')) @property def version(self): """Return the version of the plugin automatically determined from setup.py.""" return self._dist.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.""" return distribution.metadata def _listPluginEntryPoints(): if hasattr(importlib.metadata.entry_points(), 'select'): return importlib.metadata.entry_points().select(group=_NAMESPACE) return importlib.metadata.entry_points().get(_NAMESPACE, []) 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 _listPluginEntryPoints(): 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[:]