Source code for dob.clickux.plugin_group

# This file exists within 'dob':
#
#   https://github.com/hotoffthehamster/dob
#
# Copyright © 2018-2020 Landon Bouma. All rights reserved.
#
# 'dob' is free software: you can redistribute it and/or modify it under the terms
# of the GNU General Public License  as  published by the Free Software Foundation,
# either version 3  of the License,  or  (at your option)  any   later    version.
#
# 'dob' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY  or  FITNESS FOR A PARTICULAR
# PURPOSE.  See  the  GNU General Public License  for  more details.
#
# You can find the GNU General Public License reprinted in the file titled 'LICENSE',
# or visit <http://www.gnu.org/licenses/>.

"""Click Group wrapper adds plugin support."""

from functools import update_wrapper
import glob
import os

from gettext import gettext as _

import click_hotoffthehamster as click

from dob_bright.config.app_dirs import AppDirs
from dob_bright.termio import dob_in_user_warning

from ..helpers.path import compile_and_eval_source

__all__ = (
    'ensure_plugged_in',
    'ClickPluginGroup',
    'PLUGINS_DIRNAME',
)


PLUGINS_DIRNAME = 'plugins'


[docs]class ClickPluginGroup(click.Group): def __init__(self, *args, **kwargs): super(ClickPluginGroup, self).__init__(*args, **kwargs) self.plugins_basepath = os.path.join( AppDirs.user_config_dir, PLUGINS_DIRNAME, ) self.has_loaded = False @property def plugin_paths(self): py_paths = glob.glob(os.path.join(self.plugins_basepath, '*.py')) return py_paths
[docs] def list_commands(self, ctx): """Return list of commands.""" set_names = set() for cmd in self.get_commands_from_plugins(ctx, name=None): set_names.add(cmd.name) cmd_names = super(ClickPluginGroup, self).list_commands(ctx) return cmd_names
[docs] def get_command(self, ctx, name): # Aha!: # assert ctx.command is self # So True. # NOTE: get_command is called via self.resolve_command, from # click.MultiCommand.invoke. Just FYI. -(lb) # Call the get-commands func., which really just sources the plugins, so they # can tie into Click; then we can just call the base class implementation. cmd = super(ClickPluginGroup, self).get_command(ctx, name) if cmd is None: # (lb): Profiling: Loading plugins [2018-07-15: I have 3]: 0.139 secs. # So only call if necessary. self.get_commands_from_plugins(ctx, name) cmd = super(ClickPluginGroup, self).get_command(ctx, name) return cmd
[docs] def ensure_plugged_in(self, controller): if self.has_loaded: return self.get_commands_from_plugins(controller.ctx, name=None) # Redo the config now that the plugins are loaded (because when we # first read the config, the plugins had not added their definitions # yet, so any user plugin config was previously ignored). controller.replay_config()
[docs] def get_commands_from_plugins(self, ctx, name): cmds = set() for py_path in self.plugin_paths: try: files_cmds = self.open_source_eval_and_poke_around(py_path, name) if files_cmds: # (lb): We use a set, because if different plugins all import # the same object, e.g., `from dob.run_cli import run`, we do # not want to return multiple matches. cmds |= files_cmds except Exception as err: msg = _( 'ERROR: Could not open plugins file "{}": {}' ).format(py_path, str(err)) dob_in_user_warning(msg) self.has_loaded = True return list(cmds)
[docs] def open_source_eval_and_poke_around(self, py_path, name): # NOTE: The code that's eval()ed might append to self._aliases! # (Or anything else!) # NOTE: This source *should* be trusted -- the user had to run # `dob plugin install` to wire it. At least I think so. -lb. eval_globals = compile_and_eval_source(py_path) cmds = self.probe_source_for_commands(eval_globals, name) return cmds
[docs] def probe_source_for_commands(self, eval_globals, name): # Check for alias now, after having sourced the plugin. cmd_name = None if name: cmd_name = self.resolve_alias(name) cmds = set() for lname, obj in eval_globals.items(): if not isinstance(obj, click.Command): continue if self is obj: # This is the 'run' object created in the run_cli module. continue if not cmd_name or obj.name == cmd_name: cmds.add(obj) return cmds
# ***
[docs]def ensure_plugged_in(func): """ """ def wrapper(ctx, controller, *args, **kwargs): # Ensure plugins are loaded for Click functions decorated like: # @pass_controller # @click.pass_context # @ensure_plugged_in # - ctx.parent is a click_hotoffthehamster.core.Context, and # ctx.parent.command is <ClickAliasableBunchyPluginGroup run>. ctx.parent.command.ensure_plugged_in(controller) func(ctx, controller, *args, **kwargs) return update_wrapper(wrapper, func)