Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/face/utils.py: 42%
202 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:13 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:13 +0000
2import os
3import re
4import sys
5import getpass
6import keyword
8from boltons.strutils import pluralize, strip_ansi
9from boltons.iterutils import split, unique
10from boltons.typeutils import make_sentinel
12import face
14try:
15 unicode
16except NameError:
17 unicode = str
18 raw_input = input
20ERROR = make_sentinel('ERROR') # used for parse_as=ERROR
22# keep it just to subset of valid ASCII python identifiers for now
23VALID_FLAG_RE = re.compile(r"^[A-z][-_A-z0-9]*\Z")
25FRIENDLY_TYPE_NAMES = {int: 'integer',
26 float: 'decimal'}
29def process_command_name(name):
30 """Validate and canonicalize a Command's name, generally on
31 construction or at subcommand addition. Like
32 ``flag_to_identifier()``, only letters, numbers, '-', and/or
33 '_'. Must begin with a letter, and no trailing underscores or
34 dashes.
36 Python keywords are allowed, as subcommands are never used as
37 attributes or variables in injection.
39 """
41 if not name or not isinstance(name, (str, unicode)):
42 raise ValueError('expected non-zero length string for subcommand name, not: %r' % name)
44 if name.endswith('-') or name.endswith('_'):
45 raise ValueError('expected subcommand name without trailing dashes'
46 ' or underscores, not: %r' % name)
48 name_match = VALID_FLAG_RE.match(name)
49 if not name_match:
50 raise ValueError('valid subcommand name must begin with a letter, and'
51 ' consist only of letters, digits, underscores, and'
52 ' dashes, not: %r' % name)
54 subcmd_name = normalize_flag_name(name)
56 return subcmd_name
59def normalize_flag_name(flag):
60 ret = flag.lstrip('-')
61 if (len(flag) - len(ret)) > 1:
62 # only single-character flags are considered case-sensitive (like an initial)
63 ret = ret.lower()
64 ret = ret.replace('-', '_')
65 return ret
68def flag_to_identifier(flag):
69 """Validate and canonicalize a flag name to a valid Python identifier
70 (variable name).
72 Valid input strings include only letters, numbers, '-', and/or
73 '_'. Only single/double leading dash allowed (-/--). No trailing
74 dashes or underscores. Must not be a Python keyword.
76 Input case doesn't matter, output case will always be lower.
77 """
78 orig_flag = flag
79 if not flag or not isinstance(flag, (str, unicode)):
80 raise ValueError('expected non-zero length string for flag, not: %r' % flag)
82 if flag.endswith('-') or flag.endswith('_'):
83 raise ValueError('expected flag without trailing dashes'
84 ' or underscores, not: %r' % orig_flag)
86 if flag[:2] == '--':
87 flag = flag[2:]
89 flag_match = VALID_FLAG_RE.match(flag)
90 if not flag_match:
91 raise ValueError('valid flag names must begin with a letter, optionally'
92 ' prefixed by two dashes, and consist only of letters,'
93 ' digits, underscores, and dashes, not: %r' % orig_flag)
95 flag_name = normalize_flag_name(flag)
97 if keyword.iskeyword(flag_name):
98 raise ValueError('valid flag names must not be Python keywords: %r'
99 % orig_flag)
101 return flag_name
104def identifier_to_flag(identifier):
105 """
106 Turn an identifier back into its flag format (e.g., "Flag" -> --flag).
107 """
108 if identifier.startswith('-'):
109 raise ValueError('expected identifier, not flag name: %r' % identifier)
110 ret = identifier.lower().replace('_', '-')
111 return '--' + ret
114def format_flag_label(flag):
115 "The default flag label formatter, used in help and error formatting"
116 if flag.display.label is not None:
117 return flag.display.label
118 parts = [identifier_to_flag(flag.name)]
119 if flag.char:
120 parts.append('-' + flag.char)
121 ret = ' / '.join(parts)
122 if flag.display.value_name:
123 ret += ' ' + flag.display.value_name
124 return ret
127def format_posargs_label(posargspec):
128 "The default positional argument label formatter, used in help formatting"
129 if posargspec.display.label:
130 return posargspec.display.label
131 if not posargspec.accepts_args:
132 return ''
133 return get_cardinalized_args_label(posargspec.display.name, posargspec.min_count, posargspec.max_count)
136def get_cardinalized_args_label(name, min_count, max_count):
137 '''
138 Examples for parameter values: (min_count, max_count): output for name=arg:
140 1, 1: arg
141 0, 1: [arg]
142 0, None: [args ...]
143 1, 3: args ...
144 '''
145 if min_count == max_count:
146 return ' '.join([name] * min_count)
147 if min_count == 1:
148 return name + ' ' + get_cardinalized_args_label(name,
149 min_count=0,
150 max_count=max_count - 1 if max_count is not None else None)
152 tmpl = '[%s]' if min_count == 0 else '%s'
153 if max_count == 1:
154 return tmpl % name
155 return tmpl % (pluralize(name) + ' ...')
158def format_flag_post_doc(flag):
159 "The default positional argument label formatter, used in help formatting"
160 if flag.display.post_doc is not None:
161 return flag.display.post_doc
162 if flag.missing is face.ERROR:
163 return '(required)'
164 if flag.missing is None or repr(flag.missing) == object.__repr__(flag.missing):
165 # avoid displaying unhelpful defaults
166 return ''
167 return '(defaults to %r)' % (flag.missing,)
170def get_type_desc(parse_as):
171 "Kind of a hacky way to improve message readability around argument types"
172 if not callable(parse_as):
173 raise TypeError('expected parse_as to be callable, not %r' % parse_as)
174 try:
175 return 'as', FRIENDLY_TYPE_NAMES[parse_as]
176 except KeyError:
177 pass
178 try:
179 # return the type name if it looks like a type
180 return 'as', parse_as.__name__
181 except AttributeError:
182 pass
183 try:
184 # return the func name if it looks like a function
185 return 'with', parse_as.func_name
186 except AttributeError:
187 pass
188 # if all else fails
189 return 'with', repr(parse_as)
192def unwrap_text(text):
193 """Turn wrapped text into flowing paragraphs, ready for rewrapping by
194 the console, browser, or textwrap.
195 """
196 all_grafs = []
197 cur_graf = []
198 for line in text.splitlines():
199 line = line.strip()
200 if line:
201 cur_graf.append(line)
202 else:
203 all_grafs.append(' '.join(cur_graf))
204 cur_graf = []
205 if cur_graf:
206 all_grafs.append(' '.join(cur_graf))
207 return '\n\n'.join(all_grafs)
210def get_rdep_map(dep_map):
211 """
212 expects and returns a dict of {item: set([deps])}
214 item can be a string or any other hashable object.
215 """
216 # TODO: the way this is used, this function doesn't receive
217 # information about what functions take what args. this ends up
218 # just being args depending on args, with no mediating middleware
219 # names. this can make circular dependencies harder to debug.
220 ret = {}
221 for key in dep_map:
222 to_proc, rdeps, cur_chain = [key], set(), []
223 while to_proc:
224 cur = to_proc.pop()
225 cur_chain.append(cur)
227 cur_rdeps = dep_map.get(cur, [])
229 if key in cur_rdeps:
230 raise ValueError('dependency cycle: %r recursively depends'
231 ' on itself. full dep chain: %r' % (cur, cur_chain))
233 to_proc.extend([c for c in cur_rdeps if c not in to_proc])
234 rdeps.update(cur_rdeps)
236 ret[key] = rdeps
237 return ret
240def get_minimal_executable(executable=None, path=None, environ=None):
241 """Get the shortest form of a path to an executable,
242 based on the state of the process environment.
244 Args:
245 executable (str): Name or path of an executable
246 path (list): List of directories on the "PATH", or ':'-separated
247 path list, similar to the $PATH env var. Defaults to ``environ['PATH']``.
248 environ (dict): Mapping of environment variables, will be used
249 to retrieve *path* if it is None. Ignored if *path* is
250 set. Defaults to ``os.environ``.
252 Used by face's default help renderer for a more readable usage string.
253 """
254 executable = sys.executable if executable is None else executable
255 environ = os.environ if environ is None else environ
256 path = environ.get('PATH', '') if path is None else path
257 if isinstance(path, (str, unicode)):
258 path = path.split(':')
260 executable_basename = os.path.basename(executable)
261 for p in path:
262 if os.path.relpath(executable, p) == executable_basename:
263 return executable_basename
264 # TODO: support "../python" as a return?
265 return executable
268# prompt and echo owe a decent amount of design to click (and
269# pocket_protector)
270def isatty(stream):
271 "Returns True if *stream* is a tty"
272 try:
273 return stream.isatty()
274 except Exception:
275 return False
278def should_strip_ansi(stream):
279 "Returns True when ANSI color codes should be stripped from output to *stream*."
280 return not isatty(stream)
283def echo(msg, **kw):
284 """A better-behaved :func:`print()` function for command-line applications.
286 Writes text or bytes to a file or stream and flushes. Seamlessly
287 handles stripping ANSI color codes when the output file is not a
288 TTY.
290 >>> echo('test')
291 test
293 Args:
295 msg (str): A text or byte string to echo.
296 err (bool): Set the default output file to ``sys.stderr``
297 file (file): Stream or other file-like object to output
298 to. Defaults to ``sys.stdout``, or ``sys.stderr`` if *err* is
299 True.
300 nl (bool): If ``True``, sets *end* to ``'\\n'``, the newline character.
301 end (str): Explicitly set the line-ending character. Setting this overrides *nl*.
302 color (bool): Set to ``True``/``False`` to always/never echo ANSI color
303 codes. Defaults to inspecting whether *file* is a TTY.
305 """
306 msg = msg or ''
307 if not isinstance(msg, (unicode, bytes)):
308 msg = unicode(msg)
309 is_err = kw.pop('err', False)
310 _file = kw.pop('file', sys.stdout if not is_err else sys.stderr)
311 end = kw.pop('end', None)
312 enable_color = kw.pop('color', None)
314 if enable_color is None:
315 enable_color = not should_strip_ansi(_file)
317 if end is None:
318 if kw.pop('nl', True):
319 end = u'\n' if isinstance(msg, unicode) else b'\n'
320 if end:
321 msg += end
323 if msg:
324 if not enable_color:
325 msg = strip_ansi(msg)
326 _file.write(msg)
328 _file.flush()
330 return
333def echo_err(*a, **kw):
334 """
335 A convenience function which works exactly like :func:`echo`, but
336 always defaults the output *file* to ``sys.stderr``.
337 """
338 kw['err'] = True
339 return echo(*a, **kw)
342# variant-style shortcut to help minimize kwarg noise and imports
343echo.err = echo_err
346def _get_text(inp):
347 if not isinstance(inp, unicode):
348 return inp.decode('utf8')
349 return inp
352def prompt(label, confirm=None, confirm_label=None, hide_input=False, err=False):
353 """A better-behaved :func:`input()` function for command-line applications.
355 Ask a user for input, confirming if necessary, returns a text
356 string. Handles Ctrl-C and EOF more gracefully than Python's built-ins.
358 Args:
360 label (str): The prompt to display to the user.
361 confirm (bool): Pass ``True`` to ask the user to retype the input to confirm it.
362 Defaults to False, unless *confirm_label* is passed.
363 confirm_label (str): Override the confirmation prompt. Defaults
364 to "Retype *label*" if *confirm* is ``True``.
365 hide_input (bool): If ``True``, disables echoing the user's
366 input as they type. Useful for passwords and other secret
367 entry. See :func:`prompt_secret` for a more convenient
368 interface. Defaults to ``False``.
369 err (bool): If ``True``, prompts are printed on
370 ``sys.stderr``. Defaults to ``False``.
372 :func:`prompt` is primarily intended for simple plaintext
373 entry. See :func:`prompt_secret` for handling passwords and other
374 secret user input.
376 Raises :exc:`UsageError` if *confirm* is enabled and inputs do not match.
378 """
379 do_confirm = confirm or confirm_label
380 if do_confirm and not confirm_label:
381 confirm_label = 'Retype %s' % (label.lower(),)
383 def prompt_func(label):
384 func = getpass.getpass if hide_input else raw_input
385 try:
386 # Write the prompt separately so that we get nice
387 # coloring through colorama on Windows (someday)
388 echo(label, nl=False, err=err)
389 ret = func('')
390 except (KeyboardInterrupt, EOFError):
391 # getpass doesn't print a newline if the user aborts input with ^C.
392 # Allegedly this behavior is inherited from getpass(3).
393 # A doc bug has been filed at https://bugs.python.org/issue24711
394 if hide_input:
395 echo(None, err=err)
396 raise
398 return ret
400 ret = prompt_func(label)
401 ret = _get_text(ret)
402 if do_confirm:
403 ret2 = prompt_func(confirm_label)
404 ret2 = _get_text(ret2)
405 if ret != ret2:
406 raise face.UsageError('Sorry, inputs did not match.')
408 return ret
411def prompt_secret(label, **kw):
412 """A convenience function around :func:`prompt`, which is
413 preconfigured for secret user input, like passwords.
415 All arguments are the same, except *hide_input* is always
416 ``True``, and *err* defaults to ``True``, for consistency with
417 :func:`getpass.getpass`.
419 """
420 kw['hide_input'] = True
421 kw.setdefault('err', True) # getpass usually puts prompts on stderr
422 return prompt(label, **kw)
425# variant-style shortcut to help minimize kwarg noise and imports
426prompt.secret = prompt_secret