'''
.. console - Comprehensive utility library for ANSI terminals.
.. © 2020, Mike Miller - Released under the LGPL, version 3+.
Convenience command-line interface script for select utility functions
and methods that don't have common implementations,
such as tput.
(Optional arguments may be shortened and are recognized when unique.)
'''
import sys, os
import logging
from importlib import import_module
from inspect import signature
from . import fg, fx
from .meta import __version__
log = logging.getLogger(__name__)
actions = dict(
# function module or function
_print_ascii_chart = 'console.ascii4', # hide
ascii = ['_print_ascii_chart'], # alias
beep = 'console.beep',
_init = 'console.detection', # hide
detect = ['_init'], # alias
get_theme = 'console.detection',
_detect_unicode_support = 'console.detection', # hide
detect_unicode = ['_detect_unicode_support'], # alias
progress = 'console.progress',
clear_lines = 'console.utils',
flash = 'console.utils',
get_clipboard = 'console.utils',
#~ len_stripped = 'console.utils',
sized = ['make_sized'], # alias
line = ['make_line'], # alias
link = ['make_hyperlink'], # alias
make_sized = 'console.utils',
make_hyperlink = 'console.utils',
make_line = 'console.utils',
measure = 'console.utils',
_notify_message = 'console.utils',
notify_msg = ['_notify_message'],
pause = 'console.utils',
set_clipboard = 'console.utils',
set_title = 'console.utils',
strip_ansi = 'console.utils',
wait_key = 'console.utils',
view = 'console.viewers',
_hrender = 'console.viewers', # hide
echo = ['_hrender'], # alias
)
if os.name == 'nt': # :-/
from .windows import add_os_sysexits
add_os_sysexits()
def _add_sub_args(parameters, sub_parser, allow_kwargs, verbose):
''' Given function signature parameters, add to parser. '''
for name, param in parameters.items():
if name.startswith('_'): # skip these
continue
prefix = '--' # defaults
type_ = str
if param.annotation is not param.empty: # allow override
type_ = param.annotation
elif param.kind.name == 'VAR_KEYWORD': # **kwargs
allow_kwargs = True
continue
elif param.default is None:
pass
elif param.default is param.empty:
pass
else:
type_ = type(param.default)
# figure params to the sub_parser argument:
if param.default is param.empty:
default_text = ''
else:
default_text = f', defaults to {param.default!r}'
sub_args = dict(
default=param.default,
help=f'{type_.__name__}{default_text}',
)
if type_ is bool:
if param.default: # default True, negate boolean flag
sub_args['action'] = 'store_false'
sub_args['dest'] = name
sub_args['help'] = f'{type_.__name__}, negates default'
name = 'no-' + name
else:
sub_args['action'] = 'store_true'
else:
if param.default is param.empty:
prefix = ''
else:
sub_args['metavar'] = type_.__name__[0].upper()
sub_args['type'] = type_
if verbose:
log.debug('param: %s', (fg.green + fx.bold)(name))
log.debug(' default: %r' % param.default)
log.debug(' annotat: %r', param.annotation)
log.debug(' kind %r:', param.kind.name)
log.debug(' type %r:', type_)
log.debug(' sub add_argument: %r', sub_args)
name = name.replace('_', '-')
sub_parser.add_argument(prefix + name, **sub_args)
return allow_kwargs
def _get_action_help(choices):
''' Build the action list help string. '''
from textwrap import fill
ac_list = [ fg.green(action) for action in choices ]
return fill('{%s}' % ', '.join(ac_list), width=90) # wider due to escapes
def _parse_extras(parser, extras):
''' Given a list of '--key', 'value' strings, return a dictionary. '''
new = {}
keys = []
for arg in extras:
if arg.startswith('--'):
suffix = arg[2:]
key, _, val = suffix.partition('=') # --name=value form?
if val:
new[key] = val
else:
keys.append(suffix)
elif keys:
new[keys.pop()] = arg
else:
parser.error('no extra positional arguments allowed: %r' % arg)
return new
[docs]def setup():
''' Parse command line, validate, initialize logging, etc. '''
from argparse import ArgumentParser, RawTextHelpFormatter as RawFormatter
# text styles
op, n = fx.dim, fx.end # options, normal
opv = fx.italic + op # variable/abstract in italic
ac = fg.lightgreen # actions
acv = fx.italic + ac
_action = acv('action')
action_choices = sorted(set(f for f in actions if not f.startswith('_')))
action_help = _get_action_help(action_choices)
# build top-level parser
parser = ArgumentParser(
add_help=False, description=__doc__, formatter_class=RawFormatter,
usage=f'%(prog)s {op}-v{n} {op}--version{n} {_action} {opv}options…{n}',
)
parser.add_argument(
'action', nargs='?', choices=action_choices,
metavar=_action + ' ', # clear clumsy action help, better fmt-ing:
help=' one of ' + action_help +
f'\n(use {_action} {op}-h{n} for specific help)',
)
parser.add_argument(
'-n', action='store_const', dest='newline', default='\n', const='',
help='do not output the trailing newline',
)
parser.add_argument(
'-v', '--verbose', action='store_const', dest='loglvl',
default=logging.INFO, const=logging.DEBUG,
help='print additional information',
)
parser.add_argument(
'--version', action='version', version='%(prog)s ' + __version__
)
# parse and validate
args, extras = parser.parse_known_args() # don't complain about extras
verbose = args.loglvl == logging.DEBUG
newline = args.newline
# start logging
logging.basicConfig(
level=args.loglvl,
stream=sys.stdout,
format=' %(levelname)-8.8s %(message)s',
)
logging.captureWarnings(True)
log.debug('console cli, version: %s', __version__)
log.debug('args: %s', args)
if extras: log.debug('extr: %s', extras)
if args.action:
# Build a sub parser with a new parser, so we don't have to build a
# sub parser for every conceivable command at start up only to
# pick one, also simplifies a few things:
sub_parser = ArgumentParser(
formatter_class=RawFormatter,
usage=f'%(prog)s {ac(args.action)} {opv}args…{n} {opv}options…{n}',
)
allow_kwargs = False
# find module and function
funcname = args.action
value = actions[funcname]
if type(value) is list: # for aliases, value is function name
funcname = value[0]
modname = actions[funcname] # try again
funcname = funcname.lstrip('_') # in case it was hidden
else:
modname = value
# load, store, and inspect signature
mod = import_module(modname)
funk = getattr(mod, funcname)
if '-h' in sys.argv: # avoid extra work when subparser not ready
from textwrap import dedent, indent
sub_parser.description = indent(
dedent(' ' + funk.__doc__), ' '
)
else:
sub_parser.set_defaults(_funk=funk)
# configure sub parser
allow_kwargs = _add_sub_args(
signature(funk).parameters, sub_parser, allow_kwargs, verbose
)
# finally, parse and validate subcmd args
if allow_kwargs:
args, extras = sub_parser.parse_known_args(extras)
kwargs = _parse_extras(sub_parser, extras)
else:
args, kwargs = sub_parser.parse_args(extras), {}
args._newline = newline # copy from original
else: # no action, quit
parser.print_help()
sys.exit(os.EX_USAGE)
return args, kwargs
[docs]def main(args, kwargs):
''' Tear the roof off the sucker… '''
status = os.EX_OK
print_err = lambda err: print(f'{err.__class__.__name__}: {err}')
try: # Ow, we want the funk…
options = vars(args)
funk = options.pop('_funk', None) # Give up the funk
nl = options.pop('_newline', None)
if funk:
log.debug('running: %s %s', funk, options)
log.debug('-' * 60)
result = funk(**options, **kwargs)
if result:
print(result, end=nl)
log.debug('result was: %r', result)
except FileNotFoundError as err:
print_err(err)
status = os.EX_NOINPUT
except IOError as err:
print_err(err)
status = os.EX_IOERR
except Exception as err:
if log.isEnabledFor(logging.DEBUG):
log.exception('Unexpected error occurred:')
else:
print_err(err)
status = os.EX_SOFTWARE
# You've got a real type of thing going down, gettin' down
# There's a whole lot of rhythm going round…
log.debug('done, with status: %r', status)
return status
[docs]def setuptools_entry_point():
''' This is the new way to get an .exe on Windows. :-/ '''
return main(*setup())
if __name__ == '__main__':
sys.exit(main(*setup()))