# 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)