import os
import re
import sys
import getpass
import keyword
from boltons.strutils import pluralize, strip_ansi
from boltons.iterutils import split, unique
from boltons.typeutils import make_sentinel
import face
try:
unicode
except NameError:
unicode = str
raw_input = input
ERROR = make_sentinel('ERROR') # used for parse_as=ERROR
# keep it just to subset of valid ASCII python identifiers for now
VALID_FLAG_RE = re.compile(r"^[A-z][-_A-z0-9]*\Z")
FRIENDLY_TYPE_NAMES = {int: 'integer',
float: 'decimal'}
def process_command_name(name):
"""Validate and canonicalize a Command's name, generally on
construction or at subcommand addition. Like
``flag_to_identifier()``, only letters, numbers, '-', and/or
'_'. Must begin with a letter, and no trailing underscores or
dashes.
Python keywords are allowed, as subcommands are never used as
attributes or variables in injection.
"""
if not name or not isinstance(name, (str, unicode)):
raise ValueError('expected non-zero length string for subcommand name, not: %r' % name)
if name.endswith('-') or name.endswith('_'):
raise ValueError('expected subcommand name without trailing dashes'
' or underscores, not: %r' % name)
name_match = VALID_FLAG_RE.match(name)
if not name_match:
raise ValueError('valid subcommand name must begin with a letter, and'
' consist only of letters, digits, underscores, and'
' dashes, not: %r' % name)
subcmd_name = normalize_flag_name(name)
return subcmd_name
def normalize_flag_name(flag):
ret = flag.lstrip('-')
if (len(flag) - len(ret)) > 1:
# only single-character flags are considered case-sensitive (like an initial)
ret = ret.lower()
ret = ret.replace('-', '_')
return ret
def flag_to_identifier(flag):
"""Validate and canonicalize a flag name to a valid Python identifier
(variable name).
Valid input strings include only letters, numbers, '-', and/or
'_'. Only single/double leading dash allowed (-/--). No trailing
dashes or underscores. Must not be a Python keyword.
Input case doesn't matter, output case will always be lower.
"""
orig_flag = flag
if not flag or not isinstance(flag, (str, unicode)):
raise ValueError('expected non-zero length string for flag, not: %r' % flag)
if flag.endswith('-') or flag.endswith('_'):
raise ValueError('expected flag without trailing dashes'
' or underscores, not: %r' % orig_flag)
if flag[:2] == '--':
flag = flag[2:]
flag_match = VALID_FLAG_RE.match(flag)
if not flag_match:
raise ValueError('valid flag names must begin with a letter, optionally'
' prefixed by two dashes, and consist only of letters,'
' digits, underscores, and dashes, not: %r' % orig_flag)
flag_name = normalize_flag_name(flag)
if keyword.iskeyword(flag_name):
raise ValueError('valid flag names must not be Python keywords: %r'
% orig_flag)
return flag_name
def identifier_to_flag(identifier):
"""
Turn an identifier back into its flag format (e.g., "Flag" -> --flag).
"""
if identifier.startswith('-'):
raise ValueError('expected identifier, not flag name: %r' % identifier)
ret = identifier.lower().replace('_', '-')
return '--' + ret
def format_flag_label(flag):
"The default flag label formatter, used in help and error formatting"
if flag.display.label is not None:
return flag.display.label
parts = [identifier_to_flag(flag.name)]
if flag.char:
parts.append('-' + flag.char)
ret = ' / '.join(parts)
if flag.display.value_name:
ret += ' ' + flag.display.value_name
return ret
def format_posargs_label(posargspec):
"The default positional argument label formatter, used in help formatting"
if posargspec.display.label:
return posargspec.display.label
if not posargspec.accepts_args:
return ''
return get_cardinalized_args_label(posargspec.display.name, posargspec.min_count, posargspec.max_count)
def get_cardinalized_args_label(name, min_count, max_count):
'''
Examples for parameter values: (min_count, max_count): output for name=arg:
1, 1: arg
0, 1: [arg]
0, None: [args ...]
1, 3: args ...
'''
if min_count == max_count:
return ' '.join([name] * min_count)
if min_count == 1:
return name + ' ' + get_cardinalized_args_label(name,
min_count=0,
max_count=max_count - 1 if max_count is not None else None)
tmpl = '[%s]' if min_count == 0 else '%s'
if max_count == 1:
return tmpl % name
return tmpl % (pluralize(name) + ' ...')
def format_flag_post_doc(flag):
"The default positional argument label formatter, used in help formatting"
if flag.display.post_doc is not None:
return flag.display.post_doc
if flag.missing is face.ERROR:
return '(required)'
if flag.missing is None or repr(flag.missing) == object.__repr__(flag.missing):
# avoid displaying unhelpful defaults
return ''
return '(defaults to %r)' % (flag.missing,)
def get_type_desc(parse_as):
"Kind of a hacky way to improve message readability around argument types"
if not callable(parse_as):
raise TypeError('expected parse_as to be callable, not %r' % parse_as)
try:
return 'as', FRIENDLY_TYPE_NAMES[parse_as]
except KeyError:
pass
try:
# return the type name if it looks like a type
return 'as', parse_as.__name__
except AttributeError:
pass
try:
# return the func name if it looks like a function
return 'with', parse_as.func_name
except AttributeError:
pass
# if all else fails
return 'with', repr(parse_as)
def unwrap_text(text):
"""Turn wrapped text into flowing paragraphs, ready for rewrapping by
the console, browser, or textwrap.
"""
all_grafs = []
cur_graf = []
for line in text.splitlines():
line = line.strip()
if line:
cur_graf.append(line)
else:
all_grafs.append(' '.join(cur_graf))
cur_graf = []
if cur_graf:
all_grafs.append(' '.join(cur_graf))
return '\n\n'.join(all_grafs)
def get_rdep_map(dep_map):
"""
expects and returns a dict of {item: set([deps])}
item can be a string or any other hashable object.
"""
# TODO: the way this is used, this function doesn't receive
# information about what functions take what args. this ends up
# just being args depending on args, with no mediating middleware
# names. this can make circular dependencies harder to debug.
ret = {}
for key in dep_map:
to_proc, rdeps, cur_chain = [key], set(), []
while to_proc:
cur = to_proc.pop()
cur_chain.append(cur)
cur_rdeps = dep_map.get(cur, [])
if key in cur_rdeps:
raise ValueError('dependency cycle: %r recursively depends'
' on itself. full dep chain: %r' % (cur, cur_chain))
to_proc.extend([c for c in cur_rdeps if c not in to_proc])
rdeps.update(cur_rdeps)
ret[key] = rdeps
return ret
def get_minimal_executable(executable=None, path=None, environ=None):
"""Get the shortest form of a path to an executable,
based on the state of the process environment.
Args:
executable (str): Name or path of an executable
path (list): List of directories on the "PATH", or ':'-separated
path list, similar to the $PATH env var. Defaults to ``environ['PATH']``.
environ (dict): Mapping of environment variables, will be used
to retrieve *path* if it is None. Ignored if *path* is
set. Defaults to ``os.environ``.
Used by face's default help renderer for a more readable usage string.
"""
executable = sys.executable if executable is None else executable
environ = os.environ if environ is None else environ
path = environ.get('PATH', '') if path is None else path
if isinstance(path, (str, unicode)):
path = path.split(':')
executable_basename = os.path.basename(executable)
for p in path:
if os.path.relpath(executable, p) == executable_basename:
return executable_basename
# TODO: support "../python" as a return?
return executable
# prompt and echo owe a decent amount of design to click (and
# pocket_protector)
def isatty(stream):
"Returns True if *stream* is a tty"
try:
return stream.isatty()
except Exception:
return False
def should_strip_ansi(stream):
"Returns True when ANSI color codes should be stripped from output to *stream*."
return not isatty(stream)
[docs]def echo(msg, **kw):
"""A better-behaved :func:`print()` function for command-line applications.
Writes text or bytes to a file or stream and flushes. Seamlessly
handles stripping ANSI color codes when the output file is not a
TTY.
>>> echo('test')
test
Args:
msg (str): A text or byte string to echo.
err (bool): Set the default output file to ``sys.stderr``
file (file): Stream or other file-like object to output
to. Defaults to ``sys.stdout``, or ``sys.stderr`` if *err* is
True.
nl (bool): If ``True``, sets *end* to ``'\\n'``, the newline character.
end (str): Explicitly set the line-ending character. Setting this overrides *nl*.
color (bool): Set to ``True``/``False`` to always/never echo ANSI color
codes. Defaults to inspecting whether *file* is a TTY.
"""
msg = msg or ''
if not isinstance(msg, (unicode, bytes)):
msg = unicode(msg)
is_err = kw.pop('err', False)
_file = kw.pop('file', sys.stdout if not is_err else sys.stderr)
end = kw.pop('end', None)
enable_color = kw.pop('color', None)
if enable_color is None:
enable_color = not should_strip_ansi(_file)
if end is None:
if kw.pop('nl', True):
end = u'\n' if isinstance(msg, unicode) else b'\n'
if end:
msg += end
if msg:
if not enable_color:
msg = strip_ansi(msg)
_file.write(msg)
_file.flush()
return
[docs]def echo_err(*a, **kw):
"""
A convenience function which works exactly like :func:`echo`, but
always defaults the output *file* to ``sys.stderr``.
"""
kw['err'] = True
return echo(*a, **kw)
# variant-style shortcut to help minimize kwarg noise and imports
echo.err = echo_err
def _get_text(inp):
if not isinstance(inp, unicode):
return inp.decode('utf8')
return inp
[docs]def prompt(label, confirm=None, confirm_label=None, hide_input=False, err=False):
"""A better-behaved :func:`input()` function for command-line applications.
Ask a user for input, confirming if necessary, returns a text
string. Handles Ctrl-C and EOF more gracefully than Python's built-ins.
Args:
label (str): The prompt to display to the user.
confirm (bool): Pass ``True`` to ask the user to retype the input to confirm it.
Defaults to False, unless *confirm_label* is passed.
confirm_label (str): Override the confirmation prompt. Defaults
to "Retype *label*" if *confirm* is ``True``.
hide_input (bool): If ``True``, disables echoing the user's
input as they type. Useful for passwords and other secret
entry. See :func:`prompt_secret` for a more convenient
interface. Defaults to ``False``.
err (bool): If ``True``, prompts are printed on
``sys.stderr``. Defaults to ``False``.
:func:`prompt` is primarily intended for simple plaintext
entry. See :func:`prompt_secret` for handling passwords and other
secret user input.
Raises :exc:`UsageError` if *confirm* is enabled and inputs do not match.
"""
do_confirm = confirm or confirm_label
if do_confirm and not confirm_label:
confirm_label = 'Retype %s' % (label.lower(),)
def prompt_func(label):
func = getpass.getpass if hide_input else raw_input
try:
# Write the prompt separately so that we get nice
# coloring through colorama on Windows (someday)
echo(label, nl=False, err=err)
ret = func('')
except (KeyboardInterrupt, EOFError):
# getpass doesn't print a newline if the user aborts input with ^C.
# Allegedly this behavior is inherited from getpass(3).
# A doc bug has been filed at https://bugs.python.org/issue24711
if hide_input:
echo(None, err=err)
raise
return ret
ret = prompt_func(label)
ret = _get_text(ret)
if do_confirm:
ret2 = prompt_func(confirm_label)
ret2 = _get_text(ret2)
if ret != ret2:
raise face.UsageError('Sorry, inputs did not match.')
return ret
[docs]def prompt_secret(label, **kw):
"""A convenience function around :func:`prompt`, which is
preconfigured for secret user input, like passwords.
All arguments are the same, except *hide_input* is always
``True``, and *err* defaults to ``True``, for consistency with
:func:`getpass.getpass`.
"""
kw['hide_input'] = True
kw.setdefault('err', True) # getpass usually puts prompts on stderr
return prompt(label, **kw)
# variant-style shortcut to help minimize kwarg noise and imports
prompt.secret = prompt_secret