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

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

210 statements  

1 

2import os 

3import re 

4import sys 

5import getpass 

6import keyword 

7import textwrap 

8 

9from boltons.strutils import pluralize, strip_ansi 

10from boltons.iterutils import split, unique 

11from boltons.typeutils import make_sentinel 

12 

13import face 

14 

15try: 

16 unicode 

17except NameError: 

18 unicode = str 

19 raw_input = input 

20 

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

22 

23# keep it just to subset of valid ASCII python identifiers for now 

24VALID_FLAG_RE = re.compile(r"^[A-z][-_A-z0-9]*\Z") 

25 

26FRIENDLY_TYPE_NAMES = {int: 'integer', 

27 float: 'decimal'} 

28 

29 

30def process_command_name(name): 

31 """Validate and canonicalize a Command's name, generally on 

32 construction or at subcommand addition. Like 

33 ``flag_to_identifier()``, only letters, numbers, '-', and/or 

34 '_'. Must begin with a letter, and no trailing underscores or 

35 dashes. 

36 

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

38 attributes or variables in injection. 

39 

40 """ 

41 

42 if not name or not isinstance(name, (str, unicode)): 

43 raise ValueError('expected non-zero length string for subcommand name, not: %r' % name) 

44 

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

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

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

48 

49 name_match = VALID_FLAG_RE.match(name) 

50 if not name_match: 

51 raise ValueError('valid subcommand name must begin with a letter, and' 

52 ' consist only of letters, digits, underscores, and' 

53 ' dashes, not: %r' % name) 

54 

55 subcmd_name = normalize_flag_name(name) 

56 

57 return subcmd_name 

58 

59 

60def normalize_flag_name(flag): 

61 ret = flag.lstrip('-') 

62 if (len(flag) - len(ret)) > 1: 

63 # only single-character flags are considered case-sensitive (like an initial) 

64 ret = ret.lower() 

65 ret = ret.replace('-', '_') 

66 return ret 

67 

68 

69def flag_to_identifier(flag): 

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

71 (variable name). 

72 

73 Valid input strings include only letters, numbers, '-', and/or 

74 '_'. Only single/double leading dash allowed (-/--). No trailing 

75 dashes or underscores. Must not be a Python keyword. 

76 

77 Input case doesn't matter, output case will always be lower. 

78 """ 

79 orig_flag = flag 

80 if not flag or not isinstance(flag, (str, unicode)): 

81 raise ValueError('expected non-zero length string for flag, not: %r' % flag) 

82 

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

84 raise ValueError('expected flag without trailing dashes' 

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

86 

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

88 flag = flag[2:] 

89 

90 flag_match = VALID_FLAG_RE.match(flag) 

91 if not flag_match: 

92 raise ValueError('valid flag names must begin with a letter, optionally' 

93 ' prefixed by two dashes, and consist only of letters,' 

94 ' digits, underscores, and dashes, not: %r' % orig_flag) 

95 

96 flag_name = normalize_flag_name(flag) 

97 

98 if keyword.iskeyword(flag_name): 

99 raise ValueError('valid flag names must not be Python keywords: %r' 

100 % orig_flag) 

101 

102 return flag_name 

103 

104 

105def identifier_to_flag(identifier): 

106 """ 

107 Turn an identifier back into its flag format (e.g., "Flag" -> --flag). 

108 """ 

109 if identifier.startswith('-'): 

110 raise ValueError('expected identifier, not flag name: %r' % identifier) 

111 ret = identifier.lower().replace('_', '-') 

112 return '--' + ret 

113 

114 

115def format_flag_label(flag): 

116 "The default flag label formatter, used in help and error formatting" 

117 if flag.display.label is not None: 

118 return flag.display.label 

119 parts = [identifier_to_flag(flag.name)] 

120 if flag.char: 

121 parts.append('-' + flag.char) 

122 ret = ' / '.join(parts) 

123 if flag.display.value_name: 

124 ret += ' ' + flag.display.value_name 

125 return ret 

126 

127 

128def format_posargs_label(posargspec): 

129 "The default positional argument label formatter, used in help formatting" 

130 if posargspec.display.label: 

131 return posargspec.display.label 

132 if not posargspec.accepts_args: 

133 return '' 

134 return get_cardinalized_args_label(posargspec.display.name, posargspec.min_count, posargspec.max_count) 

135 

136 

137def get_cardinalized_args_label(name, min_count, max_count): 

138 ''' 

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

140 

141 1, 1: arg 

142 0, 1: [arg] 

143 0, None: [args ...] 

144 1, 3: args ... 

145 ''' 

146 if min_count == max_count: 

147 return ' '.join([name] * min_count) 

148 if min_count == 1: 

149 return name + ' ' + get_cardinalized_args_label(name, 

150 min_count=0, 

151 max_count=max_count - 1 if max_count is not None else None) 

152 

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

154 if max_count == 1: 

155 return tmpl % name 

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

157 

158 

159def format_flag_post_doc(flag): 

160 "The default positional argument label formatter, used in help formatting" 

161 if flag.display.post_doc is not None: 

162 return flag.display.post_doc 

163 if flag.missing is face.ERROR: 

164 return '(required)' 

165 if flag.missing is None or repr(flag.missing) == object.__repr__(flag.missing): 

166 # avoid displaying unhelpful defaults 

167 return '' 

168 return '(defaults to %r)' % (flag.missing,) 

169 

170 

171def get_type_desc(parse_as): 

172 "Kind of a hacky way to improve message readability around argument types" 

173 if not callable(parse_as): 

174 raise TypeError('expected parse_as to be callable, not %r' % parse_as) 

175 try: 

176 return 'as', FRIENDLY_TYPE_NAMES[parse_as] 

177 except KeyError: 

178 pass 

179 try: 

180 # return the type name if it looks like a type 

181 return 'as', parse_as.__name__ 

182 except AttributeError: 

183 pass 

184 try: 

185 # return the func name if it looks like a function 

186 return 'with', parse_as.func_name 

187 except AttributeError: 

188 pass 

189 # if all else fails 

190 return 'with', repr(parse_as) 

191 

192 

193def unwrap_text(text): 

194 """Turn wrapped text into flowing paragraphs, ready for rewrapping by 

195 the console, browser, or textwrap. 

196 """ 

197 all_grafs = [] 

198 cur_graf = [] 

199 for line in text.splitlines(): 

200 line = line.strip() 

201 if line: 

202 cur_graf.append(line) 

203 else: 

204 all_grafs.append(' '.join(cur_graf)) 

205 cur_graf = [] 

206 if cur_graf: 

207 all_grafs.append(' '.join(cur_graf)) 

208 return '\n\n'.join(all_grafs) 

209 

210 

211def get_rdep_map(dep_map): 

212 """ 

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

214 

215 item can be a string or any other hashable object. 

216 """ 

217 # TODO: the way this is used, this function doesn't receive 

218 # information about what functions take what args. this ends up 

219 # just being args depending on args, with no mediating middleware 

220 # names. this can make circular dependencies harder to debug. 

221 ret = {} 

222 for key in dep_map: 

223 to_proc, rdeps, cur_chain = [key], set(), [] 

224 while to_proc: 

225 cur = to_proc.pop() 

226 cur_chain.append(cur) 

227 

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

229 

230 if key in cur_rdeps: 

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

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

233 

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

235 rdeps.update(cur_rdeps) 

236 

237 ret[key] = rdeps 

238 return ret 

239 

240 

241def get_minimal_executable(executable=None, path=None, environ=None): 

242 """Get the shortest form of a path to an executable, 

243 based on the state of the process environment. 

244 

245 Args: 

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

247 path (list): List of directories on the "PATH", or ':'-separated 

248 path list, similar to the $PATH env var. Defaults to ``environ['PATH']``. 

249 environ (dict): Mapping of environment variables, will be used 

250 to retrieve *path* if it is None. Ignored if *path* is 

251 set. Defaults to ``os.environ``. 

252 

253 Used by face's default help renderer for a more readable usage string. 

254 """ 

255 executable = sys.executable if executable is None else executable 

256 environ = os.environ if environ is None else environ 

257 path = environ.get('PATH', '') if path is None else path 

258 if isinstance(path, (str, unicode)): 

259 path = path.split(':') 

260 

261 executable_basename = os.path.basename(executable) 

262 for p in path: 

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

264 return executable_basename 

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

266 return executable 

267 

268 

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

270# pocket_protector) 

271def isatty(stream): 

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

273 try: 

274 return stream.isatty() 

275 except Exception: 

276 return False 

277 

278 

279def should_strip_ansi(stream): 

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

281 return not isatty(stream) 

282 

283 

284def echo(msg, **kw): 

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

286 

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

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

289 TTY. 

290 

291 >>> echo('test') 

292 test 

293 

294 Args: 

295 

296 msg (str): A text or byte string to echo. 

297 err (bool): Set the default output file to ``sys.stderr`` 

298 file (file): Stream or other file-like object to output 

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

300 True. 

301 nl (bool): If ``True``, sets *end* to ``'\\n'``, the newline character. 

302 end (str): Explicitly set the line-ending character. Setting this overrides *nl*. 

303 color (bool): Set to ``True``/``False`` to always/never echo ANSI color 

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

305 

306 """ 

307 msg = msg or '' 

308 if not isinstance(msg, (unicode, bytes)): 

309 msg = unicode(msg) 

310 is_err = kw.pop('err', False) 

311 _file = kw.pop('file', sys.stdout if not is_err else sys.stderr) 

312 end = kw.pop('end', None) 

313 enable_color = kw.pop('color', None) 

314 indent = kw.pop('indent', '') 

315 space: str = ' ' 

316 if isinstance(indent, int): 

317 indent = space * indent 

318 

319 if enable_color is None: 

320 enable_color = not should_strip_ansi(_file) 

321 

322 if end is None: 

323 if kw.pop('nl', True): 

324 end = u'\n' if isinstance(msg, unicode) else b'\n' 

325 if end: 

326 msg += end 

327 if indent: 

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

329 

330 if msg: 

331 if not enable_color: 

332 msg = strip_ansi(msg) 

333 _file.write(msg) 

334 

335 _file.flush() 

336 

337 return 

338 

339 

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) 

347 

348 

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

350echo.err = echo_err 

351 

352 

353def _get_text(inp): 

354 if not isinstance(inp, unicode): 

355 return inp.decode('utf8') 

356 return inp 

357 

358 

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

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

361 

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. 

364 

365 Args: 

366 

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``. 

378 

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

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

381 secret user input. 

382 

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

384 

385 """ 

386 do_confirm = confirm or confirm_label 

387 if do_confirm and not confirm_label: 

388 confirm_label = 'Retype %s' % (label.lower(),) 

389 

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 

404 

405 return ret 

406 

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

414 

415 return ret 

416 

417 

418def prompt_secret(label, **kw): 

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

420 preconfigured for secret user input, like passwords. 

421 

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`. 

425 

426 """ 

427 kw['hide_input'] = True 

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

429 return prompt(label, **kw) 

430 

431 

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

433prompt.secret = prompt_secret