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

201 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:23 +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 all_grafs = [] 

194 cur_graf = [] 

195 for line in text.splitlines(): 

196 line = line.strip() 

197 if line: 

198 cur_graf.append(line) 

199 else: 

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

201 cur_graf = [] 

202 if cur_graf: 

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

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

205 

206 

207def get_rdep_map(dep_map): 

208 """ 

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

210 

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

212 """ 

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

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

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

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

217 ret = {} 

218 for key in dep_map: 

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

220 while to_proc: 

221 cur = to_proc.pop() 

222 cur_chain.append(cur) 

223 

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

225 

226 if key in cur_rdeps: 

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

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

229 

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

231 rdeps.update(cur_rdeps) 

232 

233 ret[key] = rdeps 

234 return ret 

235 

236 

237def get_minimal_executable(executable=None, path=None, environ=os.environ): 

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

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

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

241 path = path.split(':') 

242 

243 executable_basename = os.path.basename(executable) 

244 for p in path: 

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

246 return executable_basename 

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

248 return executable 

249 

250 

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

252# pocket_protector) 

253def isatty(stream): 

254 try: 

255 return stream.isatty() 

256 except Exception: 

257 return False 

258 

259 

260def should_strip_ansi(stream=None): 

261 if stream is None: 

262 stream = sys.stdin 

263 return not isatty(stream) 

264 

265 

266def echo(msg, **kw): 

267 msg = msg or '' 

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

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

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

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

272 

273 if enable_color is None: 

274 enable_color = not should_strip_ansi(_file) 

275 

276 if end is None: 

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

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

279 if end: 

280 msg += end 

281 

282 if msg: 

283 if not enable_color: 

284 msg = strip_ansi(msg) 

285 _file.write(msg) 

286 

287 _file.flush() 

288 

289 return 

290 

291 

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

293 kw['err'] = True 

294 return echo(*a, **kw) 

295 

296 

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

298echo.err = echo_err 

299 

300 

301def _get_text(inp): 

302 if not isinstance(inp, unicode): 

303 return inp.decode('utf8') 

304 return inp 

305 

306 

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

308 do_confirm = confirm or confirm_label 

309 if do_confirm and not confirm_label: 

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

311 

312 def prompt_func(label): 

313 func = getpass.getpass if hide_input else raw_input 

314 try: 

315 # Write the prompt separately so that we get nice 

316 # coloring through colorama on Windows (someday) 

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

318 ret = func('') 

319 except (KeyboardInterrupt, EOFError): 

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

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

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

323 if hide_input: 

324 echo(None, err=err) 

325 raise 

326 

327 return ret 

328 

329 ret = prompt_func(label) 

330 ret = _get_text(ret) 

331 if do_confirm: 

332 ret2 = prompt_func(confirm_label) 

333 ret2 = _get_text(ret2) 

334 if ret != ret2: 

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

336 

337 return ret 

338 

339 

340def prompt_secret(label, **kw): 

341 kw['hide_input'] = True 

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

343 return prompt(label, **kw) 

344 

345 

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

347prompt.secret = prompt_secret