Source code for dob.run_cli

# This file exists within 'dob':
#
#   https://github.com/hotoffthehamster/dob
#
# Copyright © 2018-2020 Landon Bouma,  2015-2016 Eric Goller.  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/>.

"""Defines the main Click Group."""

import sys
from functools import update_wrapper

import click_hotoffthehamster as click

# BREADCRUMB: PROFILING
from nark.helpers.dev.profiling import profile_elapsed, timefunct

from dob_bright.termio import click_echo, echo_exit
from dob_bright.termio.paging import ClickEchoPager

from .clickux import help_strings
from .clickux.aliasable_bunchy_plugin import ClickAliasableBunchyPluginGroup
from .controller import DobController
from .copyright import echo_copyright

__all__ = (
    'pass_controller',
    'pass_controller_context',
    'dob_versions',
    'run',
    # Private:
    #  'CONTEXT_SETTINGS',
)


# Profiling: Controller is made during Command.invoke via click.MultiCommand.invoke.
# Profiling: Controller calls _get_store: ~ 0.173 secs.
pass_controller = click.make_pass_decorator(DobController, ensure=True)


# ***

[docs]def pass_controller_context(func): @pass_controller @click.pass_context def _pass_context(ctx, controller, *args, **kwargs): controller.ctx = ctx func(ctx, controller, *args, **kwargs) return update_wrapper(_pass_context, func)
# *** # *** [VERSION] Version command helper. # ***
[docs]def dob_versions(include_all=False): '''Return CLI version information, either for this package, or all HOTH packages. ''' vers = '' include_head = include_all import importlib # MAYBE/2020-04-01: Add config_decorator and pedantic_timedelta. hothlibs = ['dob'] if include_all: hothlibs += [ 'dob_viewer', 'dob_prompt', 'dob_bright', 'nark', ] minlen = max([len(name) for name in hothlibs]) for hothlib in hothlibs: mod = importlib.import_module(hothlib, package=None) vers += '\n' if vers else '' vers += '{name:{minlen}s} version {vers}'.format( minlen=minlen, name=mod.__package_name__, vers=mod.get_version(include_head=include_head), ) return vers
# *** # *** [CLICK ROOT CONTEXT] Twiddle default Context behavior. # *** CONTEXT_SETTINGS = dict( # Tell Click to plumb the -h and --help options. help_option_names=['-h', '--help'], # But also tell Click not to invoke the help callback, # which it would otherwise do immediately when it sees # the help option. # # By default, Click processes help immediately when spotted. # # This has a few consequences: # # - If --help is seen before the subcommand in args, the run() method # below is not called, because Click handles the --help before invoking # the callback, and the Click help callback prints the help and exits. # As such, color and paging will not be setup properly for root help. # However, for subcommand help, our run() will be called, and then color # and paging will be setup properly for the subcommand help! # # - If the user wants help on a subcommand, Click forces them to specify # the --help option after the command, i.e., `dob --help command` would # normally print the same thing as `dob --help`. But we'd rather print # the help for the subcommand specified. (This also makes such obvious # commands like `dob help command` work as expected as well!) # # So tell Click not to process help upon sight, but to mark a flag. # Later, we'll look for help_option_spotted on the root context. help_option_fallthrough=True, # The --help width, used for one-line summaries, as well as the longer # reformatted help documentation. # MAGIC_NUMBER: Set reasonably wide, but the help strings themselves # will likely limit their widths to 89 or so. max_content_width=101, ) # *** # *** [BASE COMMAND GROUP] One Group to rule them all. # *** # (lb): Use invoke_without_command so `dob -v` works, otherwise Click's # Group (MultiCommand ancestor) does not allow it ('Missing command.'). @click.group( # (lb): Use our Plugin group class , which dynamically loads pluggable # commands from user's, e.g., ~/.config/dob/plugins directory. That class # derives from an Aliasable group class, which empowers us to assign command # name aliases. That class, in turn, derives from Click's base Group class. cls=ClickAliasableBunchyPluginGroup, invoke_without_command=True, help=help_strings.RUN_HELP_OVERVIEW, context_settings=CONTEXT_SETTINGS, ) # (lb): Also include version to avoid RuntimeError, ha! @click.version_option(message=dob_versions(), version='') # (lb): Hide -v: version_option adds help for --version, so don't repeat ourselves. @click.option('-v', is_flag=True, help=help_strings.VERSION_HELP, hidden=True) # (lb): Note that universal --options must com before the sub command. # FIXME: Need universal options in cmd_options? Or can I apply to Groups? # These aren't recognized by other fcns... # OH! These have to come *before* the command?? @click.option('-V', '--verbose', is_flag=True, help=help_strings.GLOBAL_OPT_VERBOSE) @click.option('-VV', '--verboser', is_flag=True, hidden=True, help=help_strings.GLOBAL_OPT_VERBOSER) @click.option('-X', '--color/--no-color', default=None, help=help_strings.GLOBAL_OPT_COLOR_NO_COLOR) @click.option('-P', '--pager/--no-pager', default=None, help=help_strings.GLOBAL_OPT_PAGER_NO_PAGER) @click.option('-C', '--config', multiple=True, metavar='KEY=VALUE', help=help_strings.GLOBAL_OPT_CONFIG) # (lb): We could use `type=click.File('r')` here. Or not. @click.option('-F', '--configfile', metavar='PATH', help=help_strings.GLOBAL_OPT_CONFIGFILE) # Profiling: pass_controller appears to take ~ ¼ seconds. @timefunct('run: create Controller [_get_store]') @pass_controller @click.pass_context # NOTE: @click.group transforms this func. definition into a callback that # we use as a decorator for the top-level commands (see: @run.command). def run(ctx, controller, v, verbose, verboser, color, pager, config, configfile): """General context run right before any of the commands.""" def _run(ctx, controller, show_version): """ Do stuff before running the command. Setup paging, if paging. Enable/disable color, per config. No longer show a banner. Show version and exit, if user specified -v option. Setup up loggers. """ profile_elapsed('To dob: run') controller.ensure_config(ctx, configfile, *config) _setup_tty_options(ctx, controller) _run_handle_banner() _run_handle_version(ctx, show_version) _run_handle_without_command(ctx) controller.setup_logging(verbose, verboser) def _setup_tty_options(ctx, controller): # If piping output, Disable color and paging. # MAYBE: (lb): What about allowing color for outputting to ANSI file? Meh. use_color = color if use_color is None and not sys.stdout.isatty(): controller.config['term.use_pager'] = False controller.config['term.use_color'] = False _setup_tty_paging(controller) _setup_tty_color(ctx, controller) controller.pre_apply_style_conf() def _setup_tty_paging(controller): use_pager = pager if use_pager is None: # None if --pager nor --no-pager specified, # so fallback to what's in the user config. use_pager = controller.config['term.use_pager'] ClickEchoPager.set_paging(use_pager) def _setup_tty_color(ctx, controller): use_color = color controller.setup_tty_color(use_color) # We'll set the Click Context object's color attribute, too, but note # that only Click's echo() method strips color based on that attribute; # the format_help() methods do not scrub ANSI codes, so we still need # to be color-aware ourselves. (So setting ctx.color here is more of a # formality than something that actually does anything.) ctx.color = color def _run_handle_banner(): # (lb): I find the greeting annoying, and somewhat boastful. # It's not that I'm against self-promotion, per se -- and I # recognize that the GPL instructs open source software to # always output a license, on every invocation -- but I like # the clean aesthetics of not showing it. Though I suppose # it's easy enough just to make a config option for it.... if not controller.config['term.show_greeting']: return echo_copyright() click_echo() def _run_handle_version(ctx, show_version): if show_version: echo_exit(ctx, dob_versions(include_all=False)) def _run_handle_without_command(ctx): # Because we set invoke_without_command, we have to check ourselves # if invoked without any command, in which case show the help. # Note this code originally, naively checked `if len(sys.argv) == 1`, # but the context (which is always root when run() is invoked) knows # if there's a subcommand specified, so check that attribute instead. if ctx.invoked_subcommand is None: echo_exit(ctx, ctx.get_help()) # Shim to the private run() functions. _run(ctx, controller, show_version=v)