# clize - automatically generate command-line interfaces from callables
# clize -- A command-line argument parser for Python
# Copyright (C) 2011-2015 by Yann Kaiser <kaiser.yann@gmail.com>
# See COPYING for details.
from __future__ import unicode_literals
import itertools
import inspect
import re
import six
from sigtools.modifiers import annotate, kwoargs
from sigtools.wrappers import wrappers
from clize import runner, parser, util
def lines_to_paragraphs(L):
return list(itertools.chain.from_iterable((x, '') for x in L))
p_delim = re.compile(r'\n\s*\n')
[docs]class Help(object):
def __init__(self, subject, owner):
self.subject = subject
self.owner = owner
self.prepared = False
[docs] def prepare(self):
"""Override for stuff to be done once per subject"""
self.prepared = True
[docs] def prepare_once(self):
if not self.prepared:
self.prepare()
@runner.Clize(pass_name=True, hide_help=True)
@kwoargs('usage')
@annotate(args=parser.Parameter.UNDOCUMENTED)
[docs] def cli(self, name, usage=False, *args):
"""Show the help
usage: Only show the full usage
"""
self.prepare_once()
name = name.rpartition(' ')[0]
f = util.Formatter()
if usage:
f.extend(self.show_full_usage(name))
else:
f.extend(self.show(name))
return six.text_type(f)
def split_docstring(s):
if not s:
return
code_coming = False
code = False
for p in p_delim.split(s):
if (code_coming or code) and p.startswith(' '):
yield p
code_coming = False
code = True
else:
item = ' '.join(p.split())
if item.endswith(':'):
code_coming = True
if item == ':':
continue
code = False
yield item
def pname(p):
return getattr(p, 'argument_name', p.display_name)
def filter_undocumented(params):
for param in params:
if not param.undocumented:
yield param
LABEL_POS = "Arguments:"
LABEL_OPT = "Options:"
LABEL_ALT = "Other actions:"
[docs]class ClizeHelp(Help):
@property
def signature(self):
return self.subject.signature
@classmethod
[docs] def get_param_type(cls, param):
try:
param.aliases
except AttributeError:
return LABEL_POS
if getattr(param, 'func', None) is None:
return LABEL_OPT
return LABEL_ALT
[docs] def prepare(self):
super(ClizeHelp, self).prepare()
s = self.sections = util.OrderedDict((
(LABEL_POS, util.OrderedDict()),
(LABEL_OPT, util.OrderedDict()),
(LABEL_ALT, util.OrderedDict()),
))
self.after = {}
for p in filter_undocumented(self.signature.positional):
s[LABEL_POS][pname(p)] = p, ''
for p in sorted(
filter_undocumented(self.signature.named), key=pname):
s[LABEL_OPT][pname(p)] = p, ''
for p in sorted(
filter_undocumented(self.signature.alternate), key=pname):
s[LABEL_ALT][pname(p)] = p, ''
self._parse_help()
s[LABEL_ALT] = s.pop(LABEL_ALT)
def _parse_func_help(self, obj):
return self._parse_docstring(inspect.getdoc(obj))
argdoc_re = re.compile('^([a-zA-Z_]+): ?(.+)$')
def _parse_docstring(self, s):
free_text = []
header = []
label = None
last_argname = None
for p in split_docstring(s):
argdoc = self.argdoc_re.match(p)
if argdoc:
argname, text = argdoc.groups()
if free_text:
if free_text[-1].endswith(':'):
label = free_text.pop()
if last_argname:
self.after[last_argname] = free_text
else:
header.extend(free_text)
free_text = []
last_argname = argname
try:
default_label = self.get_param_type(
self.signature.parameters[argname])
except KeyError:
continue
if default_label != LABEL_POS:
try:
param, _ = self.sections[default_label].pop(argname)
except KeyError:
continue
label_ = label or default_label
if label_ not in self.sections:
self.sections[label_] = util.OrderedDict()
else:
try:
param, _ = self.sections[default_label][argname]
except KeyError:
continue
label_ = default_label
self.sections[label_][argname] = param, text
else:
free_text.append(p)
if not last_argname:
header = free_text
footer = []
else:
footer = free_text
return lines_to_paragraphs(header), lines_to_paragraphs(footer)
def _parse_help(self):
self.header, self.footer = self._parse_func_help(self.subject.func)
for wrapper in wrappers(self.subject.func):
self._parse_func_help(wrapper)
@property
def description(self):
self.prepare_once()
try:
return self.header[0]
except IndexError:
return ''
[docs] def show_usage(self, name):
return 'Usage: {name}{options}{space}{positional}'.format(
name=name,
options=' [OPTIONS]' if self.signature.named else '',
space=' ' if self.signature.positional else '',
positional=' '.join(
str(arg)
for arg in filter_undocumented(self.signature.positional))
),
[docs] def alternates_with_helper(self):
for param in self.signature.alternate:
if param.undocumented:
continue
try:
helper = param.func.helper
except AttributeError:
pass
else:
yield param, param.display_name, helper
[docs] def usages(self, name):
yield name, str(self.signature)
for param, subname, helper in self.alternates_with_helper():
for usage in helper.usages(' '.join((name, subname))):
yield usage
[docs] def show_full_usage(self, name):
for name, usage in self.usages(name):
yield ' '.join((name, usage))
[docs] def show_arguments(self):
f = util.Formatter()
with f.columns() as cols:
for label, section in self.sections.items():
if not section: continue
f.new_paragraph()
f.append(label)
with f.indent():
for argname, (param, text) in section.items():
self.show_argument(
param,
text, self.after.get(argname, ()),
f, cols)
return f
[docs] def show_argument(self, param, desc, after, f, cols):
ret = param.show_help(desc, after, f, cols)
if ret is not None:
cols.append(*ret)
if after:
f.new_paragraph()
f.extend(after)
f.new_paragraph()
[docs] def show(self, name):
f = util.Formatter()
for iterable in (self.show_usage(name), self.header,
self.show_arguments(), self.footer):
f.extend(iterable)
f.new_paragraph()
return f
[docs]class DispatcherHelper(Help):
[docs] def show_commands(self):
f = util.Formatter()
f.append('Commands:')
with f.indent():
with f.columns() as cols:
for names, command in self.owner.cmds.items():
cols.append(', '.join(names), command.helper.description)
return f
[docs] def prepare_notes(self, doc):
if doc is None:
return ()
else:
return lines_to_paragraphs(split_docstring(inspect.cleandoc(doc)))
[docs] def prepare(self):
super(DispatcherHelper, self).prepare()
self.header = self.prepare_notes(self.owner.description)
self.footer = self.prepare_notes(self.owner.footnotes)
[docs] def show(self, name):
f = util.Formatter()
for text in (self.show_usage(name), self.header,
self.show_commands(), self.footer):
f.extend(text)
f.new_paragraph()
return f
[docs] def show_usage(self, name):
yield 'Usage: {0} command [args...]'.format(name)
[docs] def subcommands_with_helper(self):
for names, subcommand in self.owner.cmds.items():
try:
helper = subcommand.helper
except AttributeError:
pass
else:
yield names, subcommand, helper
[docs] def usages(self, name):
if self.subject.help_aliases:
help_name = ' '.join((name, self.subject.help_aliases[0]))
yield help_name, str(self.cli.signature)
for names, subcommand, helper in self.subcommands_with_helper():
for usage in helper.usages(' '.join((name, names[0]))):
yield usage
[docs] def show_full_usage(self, name):
for name, usage in self.usages(name):
yield ' '.join((name, usage))