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

1 

2import os 

3import re 

4import sys 

5import getpass 

6import keyword 

7 

8from boltons.strutils import pluralize, strip_ansi 

9from boltons.iterutils import split, unique 

10from boltons.typeutils import make_sentinel 

11 

12import face 

13 

14try: 

15 unicode 

16except NameError: 

17 unicode = str 

18 raw_input = input 

19 

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

21 

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

24 

25FRIENDLY_TYPE_NAMES = {int: 'integer', 

26 float: 'decimal'} 

27 

28 

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. 

35 

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

37 attributes or variables in injection. 

38 

39 """ 

40 

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

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

43 

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

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

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

47 

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) 

53 

54 subcmd_name = normalize_flag_name(name) 

55 

56 return subcmd_name 

57 

58 

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 

66 

67 

68def flag_to_identifier(flag): 

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

70 (variable name). 

71 

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. 

75 

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) 

81 

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

83 raise ValueError('expected flag without trailing dashes' 

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

85 

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

87 flag = flag[2:] 

88 

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) 

94 

95 flag_name = normalize_flag_name(flag) 

96 

97 if keyword.iskeyword(flag_name): 

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

99 % orig_flag) 

100 

101 return flag_name 

102 

103 

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 

112 

113 

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 

125 

126 

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) 

134 

135 

136def get_cardinalized_args_label(name, min_count, max_count): 

137 ''' 

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

139 

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) 

151 

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

153 if max_count == 1: 

154 return tmpl % name 

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

156 

157 

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

168 

169 

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) 

190 

191 

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) 

208 

209 

210def get_rdep_map(dep_map): 

211 """ 

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

213 

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) 

226 

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

228 

229 if key in cur_rdeps: 

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

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

232 

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

234 rdeps.update(cur_rdeps) 

235 

236 ret[key] = rdeps 

237 return ret 

238 

239 

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. 

243 

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

251 

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(':') 

259 

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 

266 

267 

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 

276 

277 

278def should_strip_ansi(stream): 

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

280 return not isatty(stream) 

281 

282 

283def echo(msg, **kw): 

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

285 

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. 

289 

290 >>> echo('test') 

291 test 

292 

293 Args: 

294 

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. 

304 

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) 

313 

314 if enable_color is None: 

315 enable_color = not should_strip_ansi(_file) 

316 

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 

322 

323 if msg: 

324 if not enable_color: 

325 msg = strip_ansi(msg) 

326 _file.write(msg) 

327 

328 _file.flush() 

329 

330 return 

331 

332 

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) 

340 

341 

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

343echo.err = echo_err 

344 

345 

346def _get_text(inp): 

347 if not isinstance(inp, unicode): 

348 return inp.decode('utf8') 

349 return inp 

350 

351 

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

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

354 

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. 

357 

358 Args: 

359 

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

371 

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

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

374 secret user input. 

375 

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

377 

378 """ 

379 do_confirm = confirm or confirm_label 

380 if do_confirm and not confirm_label: 

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

382 

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 

397 

398 return ret 

399 

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

407 

408 return ret 

409 

410 

411def prompt_secret(label, **kw): 

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

413 preconfigured for secret user input, like passwords. 

414 

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

418 

419 """ 

420 kw['hide_input'] = True 

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

422 return prompt(label, **kw) 

423 

424 

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

426prompt.secret = prompt_secret