Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/utils.py: 41%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import os
4import re
5import sys
6import getpass
7import keyword
8import textwrap
9import typing
11from boltons.strutils import pluralize, strip_ansi
12from boltons.iterutils import split, unique
13from boltons.typeutils import make_sentinel
15import face
17raw_input = input
19ERROR = make_sentinel('ERROR') # used for parse_as=ERROR
21# keep it just to subset of valid ASCII python identifiers for now
22VALID_FLAG_RE = re.compile(r"^[A-z][-_A-z0-9]*\Z")
24FRIENDLY_TYPE_NAMES = {int: 'integer',
25 float: 'decimal'}
28def process_command_name(name):
29 """Validate and canonicalize a Command's name, generally on
30 construction or at subcommand addition. Like
31 ``flag_to_identifier()``, only letters, numbers, '-', and/or
32 '_'. Must begin with a letter, and no trailing underscores or
33 dashes.
35 Python keywords are allowed, as subcommands are never used as
36 attributes or variables in injection.
38 """
40 if not name or not isinstance(name, str):
41 raise ValueError(f'expected non-zero length string for subcommand name, not: {name!r}')
43 if name.endswith('-') or name.endswith('_'):
44 raise ValueError('expected subcommand name without trailing dashes'
45 ' or underscores, not: %r' % name)
47 name_match = VALID_FLAG_RE.match(name)
48 if not name_match:
49 raise ValueError('valid subcommand name must begin with a letter, and'
50 ' consist only of letters, digits, underscores, and'
51 ' dashes, not: %r' % name)
53 subcmd_name = normalize_flag_name(name)
55 return subcmd_name
58def normalize_flag_name(flag):
59 ret = flag.lstrip('-')
60 if (len(flag) - len(ret)) > 1:
61 # only single-character flags are considered case-sensitive (like an initial)
62 ret = ret.lower()
63 ret = ret.replace('-', '_')
64 return ret
67def flag_to_identifier(flag):
68 """Validate and canonicalize a flag name to a valid Python identifier
69 (variable name).
71 Valid input strings include only letters, numbers, '-', and/or
72 '_'. Only single/double leading dash allowed (-/--). No trailing
73 dashes or underscores. Must not be a Python keyword.
75 Input case doesn't matter, output case will always be lower.
76 """
77 orig_flag = flag
78 if not flag or not isinstance(flag, str):
79 raise ValueError(f'expected non-zero length string for flag, not: {flag!r}')
81 if flag.endswith('-') or flag.endswith('_'):
82 raise ValueError('expected flag without trailing dashes'
83 ' or underscores, not: %r' % orig_flag)
85 if flag[:2] == '--':
86 flag = flag[2:]
88 flag_match = VALID_FLAG_RE.match(flag)
89 if not flag_match:
90 raise ValueError('valid flag names must begin with a letter, optionally'
91 ' prefixed by two dashes, and consist only of letters,'
92 ' digits, underscores, and dashes, not: %r' % orig_flag)
94 flag_name = normalize_flag_name(flag)
96 if keyword.iskeyword(flag_name):
97 raise ValueError(f'valid flag names must not be Python keywords: {orig_flag!r}')
99 return flag_name
102def identifier_to_flag(identifier):
103 """
104 Turn an identifier back into its flag format (e.g., "Flag" -> --flag).
105 """
106 if identifier.startswith('-'):
107 raise ValueError(f'expected identifier, not flag name: {identifier!r}')
108 ret = identifier.lower().replace('_', '-')
109 return '--' + ret
112def format_flag_label(flag):
113 "The default flag label formatter, used in help and error formatting"
114 if flag.display.label is not None:
115 return flag.display.label
116 parts = [identifier_to_flag(flag.name)]
117 if flag.char:
118 parts.append('-' + flag.char)
119 ret = ' / '.join(parts)
120 if flag.display.value_name:
121 ret += ' ' + flag.display.value_name
122 return ret
125def format_posargs_label(posargspec):
126 "The default positional argument label formatter, used in help formatting"
127 if posargspec.display.label:
128 return posargspec.display.label
129 if not posargspec.accepts_args:
130 return ''
131 return get_cardinalized_args_label(posargspec.display.name, posargspec.min_count, posargspec.max_count)
134def get_cardinalized_args_label(name, min_count, max_count):
135 '''
136 Examples for parameter values: (min_count, max_count): output for name=arg:
138 1, 1: arg
139 0, 1: [arg]
140 0, None: [args ...]
141 1, 3: args ...
142 '''
143 if min_count == max_count:
144 return ' '.join([name] * min_count)
145 if min_count == 1:
146 return name + ' ' + get_cardinalized_args_label(name,
147 min_count=0,
148 max_count=max_count - 1 if max_count is not None else None)
150 tmpl = '[%s]' if min_count == 0 else '%s'
151 if max_count == 1:
152 return tmpl % name
153 return tmpl % (pluralize(name) + ' ...')
156def format_flag_post_doc(flag):
157 "The default positional argument label formatter, used in help formatting"
158 if flag.display.post_doc is not None:
159 return flag.display.post_doc
160 if flag.missing is face.ERROR:
161 return '(required)'
162 if flag.missing is None or repr(flag.missing) == object.__repr__(flag.missing):
163 # avoid displaying unhelpful defaults
164 return ''
165 return f'(defaults to {flag.missing!r})'
168def get_type_desc(parse_as):
169 "Kind of a hacky way to improve message readability around argument types"
170 if not callable(parse_as):
171 raise TypeError(f'expected parse_as to be callable, not {parse_as!r}')
172 try:
173 return 'as', FRIENDLY_TYPE_NAMES[parse_as]
174 except KeyError:
175 pass
176 try:
177 # return the type name if it looks like a type
178 return 'as', parse_as.__name__
179 except AttributeError:
180 pass
181 try:
182 # return the func name if it looks like a function
183 return 'with', parse_as.func_name
184 except AttributeError:
185 pass
186 # if all else fails
187 return 'with', repr(parse_as)
190def unwrap_text(text):
191 """Turn wrapped text into flowing paragraphs, ready for rewrapping by
192 the console, browser, or textwrap.
193 """
194 all_grafs = []
195 cur_graf = []
196 for line in text.splitlines():
197 line = line.strip()
198 if line:
199 cur_graf.append(line)
200 else:
201 all_grafs.append(' '.join(cur_graf))
202 cur_graf = []
203 if cur_graf:
204 all_grafs.append(' '.join(cur_graf))
205 return '\n\n'.join(all_grafs)
208def get_rdep_map(dep_map):
209 """
210 expects and returns a dict of {item: set([deps])}
212 item can be a string or any other hashable object.
213 """
214 # TODO: the way this is used, this function doesn't receive
215 # information about what functions take what args. this ends up
216 # just being args depending on args, with no mediating middleware
217 # names. this can make circular dependencies harder to debug.
218 ret = {}
219 for key in dep_map:
220 to_proc, rdeps, cur_chain = [key], set(), []
221 while to_proc:
222 cur = to_proc.pop()
223 cur_chain.append(cur)
225 cur_rdeps = dep_map.get(cur, [])
227 if key in cur_rdeps:
228 raise ValueError('dependency cycle: %r recursively depends'
229 ' on itself. full dep chain: %r' % (cur, cur_chain))
231 to_proc.extend([c for c in cur_rdeps if c not in to_proc])
232 rdeps.update(cur_rdeps)
234 ret[key] = rdeps
235 return ret
238def get_minimal_executable(executable=None, path=None, environ=None):
239 """Get the shortest form of a path to an executable,
240 based on the state of the process environment.
242 Args:
243 executable (str): Name or path of an executable
244 path (list): List of directories on the "PATH", or ':'-separated
245 path list, similar to the $PATH env var. Defaults to ``environ['PATH']``.
246 environ (dict): Mapping of environment variables, will be used
247 to retrieve *path* if it is None. Ignored if *path* is
248 set. Defaults to ``os.environ``.
250 Used by face's default help renderer for a more readable usage string.
251 """
252 executable = sys.executable if executable is None else executable
253 environ = os.environ if environ is None else environ
254 path = environ.get('PATH', '') if path is None else path
255 if isinstance(path, str):
256 path = path.split(':')
258 executable_basename = os.path.basename(executable)
259 for p in path:
260 if os.path.relpath(executable, p) == executable_basename:
261 return executable_basename
262 # TODO: support "../python" as a return?
263 return executable
266# prompt and echo owe a decent amount of design to click (and
267# pocket_protector)
268def isatty(stream):
269 "Returns True if *stream* is a tty"
270 try:
271 return stream.isatty()
272 except Exception:
273 return False
276def should_strip_ansi(stream):
277 "Returns True when ANSI color codes should be stripped from output to *stream*."
278 return not isatty(stream)
281def echo(msg: str | bytes | object, *,
282 err: bool = False,
283 file: typing.TextIO | None = None,
284 nl: bool = True,
285 end: str | None = None,
286 color: bool | None = None,
287 indent: str | int = '') -> None:
288 """A better-behaved :func:`print()` function for command-line applications.
290 Writes text or bytes to a file or stream and flushes. Seamlessly
291 handles stripping ANSI color codes when the output file is not a
292 TTY.
294 >>> echo('test')
295 test
297 Args:
298 msg: A text or byte string to echo.
299 err: Set the default output file to ``sys.stderr``
300 file: Stream or other file-like object to output
301 to. Defaults to ``sys.stdout``, or ``sys.stderr`` if *err* is
302 True.
303 nl: If ``True``, sets *end* to ``'\\n'``, the newline character.
304 end: Explicitly set the line-ending character. Setting this overrides *nl*.
305 color: Set to ``True``/``False`` to always/never echo ANSI color
306 codes. Defaults to inspecting whether *file* is a TTY.
307 indent: String prefix or number of spaces to indent the output.
308 """
309 msg = msg or ''
310 if not isinstance(msg, (str, bytes)):
311 msg = str(msg)
313 _file = file or (sys.stderr if err else sys.stdout)
314 enable_color = color
315 space: str = ' '
316 if isinstance(indent, int):
317 indent = space * indent
319 if enable_color is None:
320 enable_color = not should_strip_ansi(_file)
322 if end is None:
323 if nl:
324 end = '\n' if isinstance(msg, str) else b'\n'
325 if end:
326 msg += end
327 if indent:
328 msg = textwrap.indent(msg, prefix=indent)
330 if msg:
331 if not enable_color:
332 msg = strip_ansi(msg)
333 _file.write(msg)
335 _file.flush()
337 return
340def echo_err(*a, **kw):
341 """
342 A convenience function which works exactly like :func:`echo`, but
343 always defaults the output *file* to ``sys.stderr``.
344 """
345 kw['err'] = True
346 return echo(*a, **kw)
349# variant-style shortcut to help minimize kwarg noise and imports
350echo.err = echo_err
353def _get_text(inp):
354 if not isinstance(inp, str):
355 return inp.decode('utf8')
356 return inp
359def prompt(label, confirm=None, confirm_label=None, hide_input=False, err=False):
360 """A better-behaved :func:`input()` function for command-line applications.
362 Ask a user for input, confirming if necessary, returns a text
363 string. Handles Ctrl-C and EOF more gracefully than Python's built-ins.
365 Args:
367 label (str): The prompt to display to the user.
368 confirm (bool): Pass ``True`` to ask the user to retype the input to confirm it.
369 Defaults to False, unless *confirm_label* is passed.
370 confirm_label (str): Override the confirmation prompt. Defaults
371 to "Retype *label*" if *confirm* is ``True``.
372 hide_input (bool): If ``True``, disables echoing the user's
373 input as they type. Useful for passwords and other secret
374 entry. See :func:`prompt_secret` for a more convenient
375 interface. Defaults to ``False``.
376 err (bool): If ``True``, prompts are printed on
377 ``sys.stderr``. Defaults to ``False``.
379 :func:`prompt` is primarily intended for simple plaintext
380 entry. See :func:`prompt_secret` for handling passwords and other
381 secret user input.
383 Raises :exc:`UsageError` if *confirm* is enabled and inputs do not match.
385 """
386 do_confirm = confirm or confirm_label
387 if do_confirm and not confirm_label:
388 confirm_label = f'Retype {label.lower()}'
390 def prompt_func(label):
391 func = getpass.getpass if hide_input else raw_input
392 try:
393 # Write the prompt separately so that we get nice
394 # coloring through colorama on Windows (someday)
395 echo(label, nl=False, err=err)
396 ret = func('')
397 except (KeyboardInterrupt, EOFError):
398 # getpass doesn't print a newline if the user aborts input with ^C.
399 # Allegedly this behavior is inherited from getpass(3).
400 # A doc bug has been filed at https://bugs.python.org/issue24711
401 if hide_input:
402 echo(None, err=err)
403 raise
405 return ret
407 ret = prompt_func(label)
408 ret = _get_text(ret)
409 if do_confirm:
410 ret2 = prompt_func(confirm_label)
411 ret2 = _get_text(ret2)
412 if ret != ret2:
413 raise face.UsageError('Sorry, inputs did not match.')
415 return ret
418def prompt_secret(label, **kw):
419 """A convenience function around :func:`prompt`, which is
420 preconfigured for secret user input, like passwords.
422 All arguments are the same, except *hide_input* is always
423 ``True``, and *err* defaults to ``True``, for consistency with
424 :func:`getpass.getpass`.
426 """
427 kw['hide_input'] = True
428 kw.setdefault('err', True) # getpass usually puts prompts on stderr
429 return prompt(label, **kw)
432# variant-style shortcut to help minimize kwarg noise and imports
433prompt.secret = prompt_secret