Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/utils.py: 37%

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

231 statements  

1from __future__ import annotations 

2 

3import os 

4import re 

5import sys 

6import getpass 

7import keyword 

8import textwrap 

9import typing 

10 

11from boltons.strutils import pluralize, strip_ansi 

12from boltons.iterutils import split, unique 

13from boltons.typeutils import make_sentinel 

14 

15import face 

16 

17raw_input = input 

18 

19ERROR = make_sentinel('ERROR') # used for parse_as=ERROR 

20 

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") 

23 

24FRIENDLY_TYPE_NAMES = {int: 'integer', 

25 float: 'decimal'} 

26 

27 

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. 

34 

35 Python keywords are allowed, as subcommands are never used as 

36 attributes or variables in injection. 

37 

38 """ 

39 

40 if not name or not isinstance(name, str): 

41 raise ValueError(f'expected non-zero length string for subcommand name, not: {name!r}') 

42 

43 if name.endswith('-') or name.endswith('_'): 

44 raise ValueError('expected subcommand name without trailing dashes' 

45 ' or underscores, not: %r' % name) 

46 

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) 

52 

53 subcmd_name = normalize_flag_name(name) 

54 

55 return subcmd_name 

56 

57 

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 

65 

66 

67def flag_to_identifier(flag): 

68 """Validate and canonicalize a flag name to a valid Python identifier 

69 (variable name). 

70 

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. 

74 

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}') 

80 

81 if flag.endswith('-') or flag.endswith('_'): 

82 raise ValueError('expected flag without trailing dashes' 

83 ' or underscores, not: %r' % orig_flag) 

84 

85 if flag[:2] == '--': 

86 flag = flag[2:] 

87 

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) 

93 

94 flag_name = normalize_flag_name(flag) 

95 

96 if keyword.iskeyword(flag_name): 

97 raise ValueError(f'valid flag names must not be Python keywords: {orig_flag!r}') 

98 

99 return flag_name 

100 

101 

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 

110 

111 

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 

123 

124 

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) 

132 

133 

134def get_cardinalized_args_label(name, min_count, max_count): 

135 ''' 

136 Examples for parameter values: (min_count, max_count): output for name=arg: 

137 

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) 

149 

150 tmpl = '[%s]' if min_count == 0 else '%s' 

151 if max_count == 1: 

152 return tmpl % name 

153 return tmpl % (pluralize(name) + ' ...') 

154 

155 

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})' 

166 

167 

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) 

188 

189 

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) 

206 

207 

208def get_rdep_map(dep_map): 

209 """ 

210 expects and returns a dict of {item: set([deps])} 

211 

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) 

224 

225 cur_rdeps = dep_map.get(cur, []) 

226 

227 if key in cur_rdeps: 

228 raise ValueError('dependency cycle: %r recursively depends' 

229 ' on itself. full dep chain: %r' % (cur, cur_chain)) 

230 

231 to_proc.extend([c for c in cur_rdeps if c not in to_proc]) 

232 rdeps.update(cur_rdeps) 

233 

234 ret[key] = rdeps 

235 return ret 

236 

237 

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. 

241 

242 Args: 

243 executable (str): Name or path of an executable 

244 path (list): List of directories on the "PATH", or a string using 

245 the platform path separator (os.pathsep). 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``. 

249 

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(os.pathsep) 

257 

258 executable_basename = os.path.basename(executable) 

259 for p in path: 

260 try: 

261 if os.path.relpath(executable, p) == executable_basename: 

262 return executable_basename 

263 except ValueError: 

264 # On Windows, relpath raises ValueError when paths are on 

265 # different drives (e.g., D:\exe vs C:\path). Skip these. 

266 continue 

267 # TODO: support "../python" as a return? 

268 return executable 

269 

270 

271# prompt and echo owe a decent amount of design to click (and 

272# pocket_protector) 

273def isatty(stream): 

274 "Returns True if *stream* is a tty" 

275 try: 

276 return stream.isatty() 

277 except Exception: 

278 return False 

279 

280 

281def should_strip_ansi(stream): 

282 "Returns True when ANSI color codes should be stripped from output to *stream*." 

283 return not isatty(stream) 

284 

285 

286def echo(msg: str | bytes | object, *, 

287 err: bool = False, 

288 file: typing.TextIO | None = None, 

289 nl: bool = True, 

290 end: str | None = None, 

291 color: bool | None = None, 

292 indent: str | int = '') -> None: 

293 """A better-behaved :func:`print()` function for command-line applications. 

294 

295 Writes text or bytes to a file or stream and flushes. Seamlessly 

296 handles stripping ANSI color codes when the output file is not a 

297 TTY. 

298 

299 >>> echo('test') 

300 test 

301 

302 Args: 

303 msg: A text or byte string to echo. 

304 err: Set the default output file to ``sys.stderr`` 

305 file: Stream or other file-like object to output 

306 to. Defaults to ``sys.stdout``, or ``sys.stderr`` if *err* is 

307 True. 

308 nl: If ``True``, sets *end* to ``'\\n'``, the newline character. 

309 end: Explicitly set the line-ending character. Setting this overrides *nl*. 

310 color: Set to ``True``/``False`` to always/never echo ANSI color 

311 codes. Defaults to inspecting whether *file* is a TTY. 

312 indent: String prefix or number of spaces to indent the output. 

313 """ 

314 msg = msg or '' 

315 if not isinstance(msg, (str, bytes)): 

316 msg = str(msg) 

317 

318 _file = file or (sys.stderr if err else sys.stdout) 

319 enable_color = color 

320 space: str = ' ' 

321 if isinstance(indent, int): 

322 indent = space * indent 

323 

324 if enable_color is None: 

325 enable_color = not should_strip_ansi(_file) 

326 

327 if end is None: 

328 if nl: 

329 end = '\n' if isinstance(msg, str) else b'\n' 

330 if end: 

331 msg += end 

332 if indent: 

333 msg = textwrap.indent(msg, prefix=indent) 

334 

335 if msg: 

336 if not enable_color: 

337 msg = strip_ansi(msg) 

338 _file.write(msg) 

339 

340 _file.flush() 

341 

342 return 

343 

344 

345def echo_err(*a, **kw): 

346 """ 

347 A convenience function which works exactly like :func:`echo`, but 

348 always defaults the output *file* to ``sys.stderr``. 

349 """ 

350 kw['err'] = True 

351 return echo(*a, **kw) 

352 

353 

354# variant-style shortcut to help minimize kwarg noise and imports 

355echo.err = echo_err 

356 

357 

358def _get_text(inp): 

359 if not isinstance(inp, str): 

360 return inp.decode('utf8') 

361 return inp 

362 

363 

364 

365def flush_stdin() -> None: 

366 """Drain stale keystrokes buffered in stdin. 

367 

368 During long processing phases, accidental keypresses accumulate in the 

369 terminal input buffer. Without flushing, the next ``input()`` call 

370 silently consumes that garbage, producing a wrong answer. 

371 

372 Silently no-ops on non-Unix platforms or when stdin is not a tty (e.g. 

373 in tests or piped input). 

374 """ 

375 try: 

376 import termios 

377 except ImportError: 

378 return # non-Unix 

379 try: 

380 termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH) 

381 except (OSError, termios.error): 

382 pass # non-tty or pipe 

383 

384 

385def prompt(label, confirm=None, confirm_label=None, hide_input=False, err=False): 

386 """A better-behaved :func:`input()` function for command-line applications. 

387 

388 Ask a user for input, confirming if necessary, returns a text 

389 string. Handles Ctrl-C and EOF more gracefully than Python's built-ins. 

390 

391 Args: 

392 

393 label (str): The prompt to display to the user. 

394 confirm (bool): Pass ``True`` to ask the user to retype the input to confirm it. 

395 Defaults to False, unless *confirm_label* is passed. 

396 confirm_label (str): Override the confirmation prompt. Defaults 

397 to "Retype *label*" if *confirm* is ``True``. 

398 hide_input (bool): If ``True``, disables echoing the user's 

399 input as they type. Useful for passwords and other secret 

400 entry. See :func:`prompt_secret` for a more convenient 

401 interface. Defaults to ``False``. 

402 err (bool): If ``True``, prompts are printed on 

403 ``sys.stderr``. Defaults to ``False``. 

404 

405 :func:`prompt` is primarily intended for simple plaintext 

406 entry. See :func:`prompt_secret` for handling passwords and other 

407 secret user input. 

408 

409 Raises :exc:`UsageError` if *confirm* is enabled and inputs do not match. 

410 

411 """ 

412 do_confirm = confirm or confirm_label 

413 if do_confirm and not confirm_label: 

414 confirm_label = f'Retype {label.lower()}' 

415 

416 def prompt_func(label): 

417 func = getpass.getpass if hide_input else raw_input 

418 try: 

419 # Write the prompt separately so that we get nice 

420 # coloring through colorama on Windows (someday) 

421 echo(label, nl=False, err=err) 

422 if func is raw_input: 

423 flush_stdin() 

424 ret = func('') 

425 except (KeyboardInterrupt, EOFError): 

426 # getpass doesn't print a newline if the user aborts input with ^C. 

427 # Allegedly this behavior is inherited from getpass(3). 

428 # A doc bug has been filed at https://bugs.python.org/issue24711 

429 if hide_input: 

430 echo(None, err=err) 

431 raise 

432 

433 return ret 

434 

435 ret = prompt_func(label) 

436 ret = _get_text(ret) 

437 if do_confirm: 

438 ret2 = prompt_func(confirm_label) 

439 ret2 = _get_text(ret2) 

440 if ret != ret2: 

441 raise face.UsageError('Sorry, inputs did not match.') 

442 

443 return ret 

444 

445 

446def prompt_secret(label, **kw): 

447 """A convenience function around :func:`prompt`, which is 

448 preconfigured for secret user input, like passwords. 

449 

450 All arguments are the same, except *hide_input* is always 

451 ``True``, and *err* defaults to ``True``, for consistency with 

452 :func:`getpass.getpass`. 

453 

454 """ 

455 kw['hide_input'] = True 

456 kw.setdefault('err', True) # getpass usually puts prompts on stderr 

457 return prompt(label, **kw) 

458 

459 

460# variant-style shortcut to help minimize kwarg noise and imports 

461prompt.secret = prompt_secret 

462 

463 

464 

465def prompt_yn(message: str, *, default: bool = True) -> bool: 

466 """Prompt for a yes/no confirmation, re-prompting on unrecognized input. 

467 

468 The *default* controls what happens when the user presses Enter with no 

469 input, and determines the ``[Y/n]`` vs ``[y/N]`` hint. 

470 

471 Returns ``True`` for yes, ``False`` for no. 

472 Raises :exc:`KeyboardInterrupt` on Ctrl-C and :exc:`EOFError` on Ctrl-D. 

473 """ 

474 hint = '[Y/n]' if default else '[y/N]' 

475 prompt_str = f'{message} {hint}: ' 

476 

477 while True: 

478 flush_stdin() 

479 answer = raw_input(prompt_str).strip().lower() 

480 

481 if answer in ('y', 'yes'): 

482 return True 

483 if answer in ('n', 'no'): 

484 return False 

485 if answer == '': 

486 return default 

487 

488 echo(' Please enter y or n.', err=True)