Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/helpers.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

175 statements  

1 

2import os 

3import sys 

4import array 

5import textwrap 

6 

7from boltons.iterutils import unique, split 

8 

9from face.utils import format_flag_label, format_flag_post_doc, format_posargs_label, echo 

10from face.parser import Flag 

11 

12DEFAULT_HELP_FLAG = Flag('--help', parse_as=True, char='-h', doc='show this help message and exit') 

13DEFAULT_MAX_WIDTH = 120 

14 

15 

16def _get_termios_winsize(): 

17 # TLPI, 62.9 (p. 1319) 

18 import fcntl 

19 import termios 

20 

21 winsize = array.array('H', [0, 0, 0, 0]) 

22 

23 assert not fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, winsize) 

24 

25 ws_row, ws_col, _, _ = winsize 

26 

27 return ws_row, ws_col 

28 

29 

30def _get_environ_winsize(): 

31 # the argparse approach. not sure which systems this works or 

32 # worked on, if any. ROWS/COLUMNS are special shell variables. 

33 try: 

34 rows, columns = int(os.environ['ROWS']), int(os.environ['COLUMNS']) 

35 except (KeyError, ValueError): 

36 rows, columns = None, None 

37 return rows, columns 

38 

39 

40def get_winsize(): 

41 rows, cols = None, None 

42 try: 

43 rows, cols = _get_termios_winsize() 

44 except Exception: 

45 try: 

46 rows, cols = _get_environ_winsize() 

47 except Exception: 

48 pass 

49 return rows, cols 

50 

51 

52def get_wrap_width(max_width=DEFAULT_MAX_WIDTH): 

53 _, width = get_winsize() 

54 if width is None: 

55 width = 80 

56 width = min(width, max_width) 

57 width -= 2 

58 return width 

59 

60 

61def _wrap_stout_pair(indent, label, sep, doc, doc_start, max_doc_width): 

62 # TODO: consider making the fill character configurable (ljust 

63 # uses space by default, the just() methods can only take 

64 # characters, might be a useful bolton to take a repeating 

65 # sequence) 

66 ret = [] 

67 append = ret.append 

68 lhs = indent + label 

69 

70 if not doc: 

71 append(lhs) 

72 return ret 

73 

74 len_sep = len(sep) 

75 wrapped_doc = textwrap.wrap(doc, max_doc_width) 

76 if len(lhs) <= doc_start: 

77 lhs_f = lhs.ljust(doc_start - len(sep)) + sep 

78 append(lhs_f + wrapped_doc[0]) 

79 else: 

80 append(lhs) 

81 append((' ' * (doc_start - len_sep)) + sep + wrapped_doc[0]) 

82 

83 for line in wrapped_doc[1:]: 

84 append(' ' * doc_start + line) 

85 

86 return ret 

87 

88 

89def _wrap_stout_cmd_doc(indent, doc, max_width): 

90 """Function for wrapping command description.""" 

91 parts = [] 

92 paras = ['\n'.join(para) for para in 

93 split(doc.splitlines(), lambda l: not l.lstrip()) 

94 if para] 

95 for para in paras: 

96 part = textwrap.fill(text=para, 

97 width=(max_width - len(indent)), 

98 initial_indent=indent, 

99 subsequent_indent=indent) 

100 parts.append(part) 

101 return '\n\n'.join(parts) 

102 

103 

104def get_stout_layout(labels, indent, sep, width=None, max_width=DEFAULT_MAX_WIDTH, 

105 min_doc_width=40): 

106 width = width or get_wrap_width(max_width=max_width) 

107 

108 len_sep = len(sep) 

109 len_indent = len(indent) 

110 

111 max_label_width = 0 

112 max_doc_width = min_doc_width 

113 doc_start = width - min_doc_width 

114 for label in labels: 

115 cur_len = len(label) 

116 if cur_len < max_label_width: 

117 continue 

118 max_label_width = cur_len 

119 if (len_indent + cur_len + len_sep + min_doc_width) < width: 

120 max_doc_width = width - max_label_width - len_sep - len_indent 

121 doc_start = len_indent + cur_len + len_sep 

122 

123 return {'width': width, 

124 'label_width': max_label_width, 

125 'doc_width': max_doc_width, 

126 'doc_start': doc_start} 

127 

128 

129DEFAULT_CONTEXT = { 

130 'usage_label': 'Usage:', 

131 'subcmd_section_heading': 'Subcommands: ', 

132 'flags_section_heading': 'Flags: ', 

133 'posargs_section_heading': 'Positional arguments:', 

134 'section_break': '\n', 

135 'group_break': '', 

136 'subcmd_example': 'subcommand', 

137 'width': None, 

138 'max_width': 120, 

139 'min_doc_width': 50, 

140 'format_posargs_label': format_posargs_label, 

141 'format_flag_label': format_flag_label, 

142 'format_flag_post_doc': format_flag_post_doc, 

143 'doc_separator': ' ', # ' + ' is pretty classy as bullet points, too 

144 'section_indent': ' ', 

145 'pre_doc': '', # TODO: these should go on CommandDisplay 

146 'post_doc': '\n', 

147} 

148 

149 

150class StoutHelpFormatter(object): 

151 """This formatter takes :class:`Parser` and :class:`Command` instances 

152 and generates help text. The output style is inspired by, but not 

153 the same as, argparse's automatic help formatting. 

154 

155 Probably what most Pythonists expect, this help text is slightly 

156 stouter (conservative with vertical space) than other conventional 

157 help messages. 

158 

159 The default output looks like:: 

160 

161 Usage: example.py subcommand [FLAGS] 

162 

163 Does a bit of busy work 

164 

165 

166 Subcommands: 

167 

168 sum Just a lil fun in the sum 

169 subtract 

170 print 

171 

172 

173 Flags: 

174 

175 --help / -h show this help message and exit 

176 --verbose / -V 

177 

178 

179 Due to customizability, the constructor takes a large number of 

180 keyword arguments, the most important of which are highlighted 

181 here. 

182 

183 Args: 

184 width (int): The width of the help output in 

185 columns/characters. Defaults to the width of the terminal, 

186 with a max of *max_width*. 

187 max_width (int): The widest the help output will get. Too wide 

188 and it can be hard to visually scan. Defaults to 120 columns. 

189 min_doc_width (int): The text documentation's minimum width in 

190 columns/characters. Puts flags and subcommands on their own 

191 lines when they're long or the terminal is narrow. Defaults to 

192 50. 

193 doc_separator (str): The string to put between a 

194 flag/subcommand and its documentation. Defaults to `' '`. (Try 

195 `' + '` for a classy bulleted doc style. 

196 

197 An instance of StoutHelpFormatter can be passed to 

198 :class:`HelpHandler`, which can in turn be passed to 

199 :class:`Command` for maximum command customizability. 

200 

201 Alternatively, when using :class:`Parser` object directly, you can 

202 instantiate this type and pass a :class:`Parser` object to 

203 :meth:`get_help_text()` or :meth:`get_usage_line()` to get 

204 identically formatted text without sacrificing flow control. 

205 

206 HelpFormatters are stateless, in that they can be used more than 

207 once, with different Parsers and Commands without needing to be 

208 recreated or otherwise reset. 

209 

210 """ 

211 default_context = dict(DEFAULT_CONTEXT) 

212 

213 def __init__(self, **kwargs): 

214 self.ctx = {} 

215 for key, val in self.default_context.items(): 

216 self.ctx[key] = kwargs.pop(key, val) 

217 if kwargs: 

218 raise TypeError('unexpected formatter arguments: %r' % list(kwargs.keys())) 

219 

220 def _get_layout(self, labels): 

221 ctx = self.ctx 

222 return get_stout_layout(labels=labels, 

223 indent=ctx['section_indent'], 

224 sep=ctx['doc_separator'], 

225 width=ctx['width'], 

226 max_width=ctx['max_width'], 

227 min_doc_width=ctx['min_doc_width']) 

228 

229 def get_help_text(self, parser, subcmds=(), program_name=None): 

230 """Turn a :class:`Parser` or :class:`Command` into a multiline 

231 formatted help string, suitable for printing. Includes the 

232 usage line and trailing newline by default. 

233 

234 Args: 

235 parser (Parser): A :class:`Parser` or :class:`Command` 

236 object to generate help text for. 

237 subcmds (tuple): A sequence of subcommand strings 

238 specifying the subcommand to generate help text for. 

239 Defaults to ``()``. 

240 program_name (str): The program name, if it differs from 

241 the default ``sys.argv[0]``. (For example, 

242 ``example.py``, when running the command ``python 

243 example.py --flag val arg``.) 

244 

245 """ 

246 # TODO: incorporate "Arguments" section if posargs has a doc set 

247 ctx = self.ctx 

248 

249 ret = [self.get_usage_line(parser, subcmds=subcmds, program_name=program_name)] 

250 append = ret.append 

251 append(ctx['group_break']) 

252 

253 shown_flags = parser.get_flags(path=subcmds, with_hidden=False) 

254 if subcmds: 

255 parser = parser.subprs_map[subcmds] 

256 

257 if parser.doc: 

258 append(_wrap_stout_cmd_doc(indent=ctx['section_indent'], 

259 doc=parser.doc, 

260 max_width=ctx['width'] or get_wrap_width( 

261 max_width=ctx['max_width']))) 

262 append(ctx['section_break']) 

263 

264 if parser.subprs_map: 

265 subcmd_names = unique([sp[0] for sp in parser.subprs_map if sp]) 

266 subcmd_layout = self._get_layout(labels=subcmd_names) 

267 

268 append(ctx['subcmd_section_heading']) 

269 append(ctx['group_break']) 

270 for sub_name in unique([sp[0] for sp in parser.subprs_map if sp]): 

271 subprs = parser.subprs_map[(sub_name,)] 

272 # TODO: sub_name.replace('_', '-') = _cmd -> -cmd (need to skip replacing leading underscores) 

273 subcmd_lines = _wrap_stout_pair(indent=ctx['section_indent'], 

274 label=sub_name.replace('_', '-'), 

275 sep=ctx['doc_separator'], 

276 doc=subprs.doc, 

277 doc_start=subcmd_layout['doc_start'], 

278 max_doc_width=subcmd_layout['doc_width']) 

279 ret.extend(subcmd_lines) 

280 

281 append(ctx['section_break']) 

282 

283 if not shown_flags: 

284 return '\n'.join(ret) 

285 

286 fmt_flag_label = ctx['format_flag_label'] 

287 flag_labels = [fmt_flag_label(flag) for flag in shown_flags] 

288 flag_layout = self._get_layout(labels=flag_labels) 

289 

290 fmt_flag_post_doc = ctx['format_flag_post_doc'] 

291 append(ctx['flags_section_heading']) 

292 append(ctx['group_break']) 

293 for flag in shown_flags: 

294 disp = flag.display 

295 if disp.full_doc is not None: 

296 doc = disp.full_doc 

297 else: 

298 _parts = [disp.doc] if disp.doc else [] 

299 post_doc = disp.post_doc if disp.post_doc else fmt_flag_post_doc(flag) 

300 if post_doc: 

301 _parts.append(post_doc) 

302 doc = ' '.join(_parts) 

303 

304 flag_lines = _wrap_stout_pair(indent=ctx['section_indent'], 

305 label=fmt_flag_label(flag), 

306 sep=ctx['doc_separator'], 

307 doc=doc, 

308 doc_start=flag_layout['doc_start'], 

309 max_doc_width=flag_layout['doc_width']) 

310 

311 ret.extend(flag_lines) 

312 

313 return ctx['pre_doc'] + '\n'.join(ret) + ctx['post_doc'] 

314 

315 def get_usage_line(self, parser, subcmds=(), program_name=None): 

316 """Get just the top line of automated text output. Arguments are the 

317 same as :meth:`get_help_text()`. Basic info about running the 

318 command, such as: 

319 

320 Usage: example.py subcommand [FLAGS] [args ...] 

321 

322 """ 

323 ctx = self.ctx 

324 subcmds = tuple(subcmds or ()) 

325 parts = [ctx['usage_label']] if ctx['usage_label'] else [] 

326 append = parts.append 

327 

328 program_name = program_name or parser.name 

329 

330 append(' '.join((program_name,) + subcmds)) 

331 

332 # TODO: put () in subprs_map to handle some of this sorta thing 

333 if not subcmds and parser.subprs_map: 

334 append('subcommand') 

335 elif subcmds and parser.subprs_map[subcmds].subprs_map: 

336 append('subcommand') 

337 

338 # with subcommands out of the way, look up the parser for flags and args 

339 if subcmds: 

340 parser = parser.subprs_map[subcmds] 

341 

342 flags = parser.get_flags(with_hidden=False) 

343 

344 if flags: 

345 append('[FLAGS]') 

346 

347 if not parser.posargs.display.hidden: 

348 fmt_posargs_label = ctx['format_posargs_label'] 

349 append(fmt_posargs_label(parser.posargs)) 

350 

351 return ' '.join(parts) 

352 

353 

354 

355''' 

356class AiryHelpFormatter(object): 

357 """No wrapping a doc onto the same line as the label. Just left 

358 aligned labels + newline, then right align doc. No complicated 

359 width calculations either. See https://github.com/kbknapp/clap-rs 

360 """ 

361 pass # TBI 

362''' 

363 

364 

365class HelpHandler(object): 

366 """The HelpHandler is a one-stop object for that all-important CLI 

367 feature: automatic help generation. It ties together the actual 

368 help handler with the optional flag and subcommand such that it 

369 can be added to any :class:`Command` instance. 

370 

371 The :class:`Command` creates a HelpHandler instance by default, 

372 and its constructor also accepts an instance of this type to 

373 customize a variety of helpful features. 

374 

375 Args: 

376 flag (face.Flag): The Flag instance to use for triggering a 

377 help output in a Command setting. Defaults to the standard 

378 ``--help / -h`` flag. Pass ``False`` to disable. 

379 subcmd (str): A subcommand name to be added to any 

380 :class:`Command` using this HelpHandler. Defaults to 

381 ``None``. 

382 formatter: A help formatter instance or type. Type will be 

383 instantiated with keyword arguments passed to this 

384 constructor. Defaults to :class:`StoutHelpFormatter`. 

385 func (callable): The actual handler function called on flag 

386 presence or subcommand invocation. Defaults to 

387 :meth:`HelpHandler.default_help_func()`. 

388 

389 All other remaining keyword arguments are used to construct the 

390 HelpFormatter, if *formatter* is a type (as is the default). For 

391 an example of a formatter, see :class:`StoutHelpFormatter`, the 

392 default help formatter. 

393 """ 

394 # Other hooks (besides the help function itself): 

395 # * Callbacks for unhandled exceptions 

396 # * Callbacks for formatting errors (add a "see --help for more options") 

397 

398 def __init__(self, flag=DEFAULT_HELP_FLAG, subcmd=None, 

399 formatter=StoutHelpFormatter, func=None, **formatter_kwargs): 

400 # subcmd expects a string 

401 self.flag = flag 

402 self.subcmd = subcmd 

403 self.func = func if func is not None else self.default_help_func 

404 if not callable(self.func): 

405 raise TypeError('expected help handler func to be callable, not %r' % func) 

406 

407 self.formatter = formatter 

408 if not formatter: 

409 raise TypeError('expected Formatter type or instance, not: %r' % formatter) 

410 if isinstance(formatter, type): 

411 self.formatter = formatter(**formatter_kwargs) 

412 elif formatter_kwargs: 

413 raise TypeError('only accepts extra formatter kwargs (%r) if' 

414 ' formatter argument is a Formatter type, not: %r' 

415 % (sorted(formatter_kwargs.keys()), formatter)) 

416 _has_get_help_text = callable(getattr(self.formatter, 'get_help_text', None)) 

417 if not _has_get_help_text: 

418 raise TypeError('expected valid formatter, or other object with a' 

419 ' get_help_text() method, not %r' % (self.formatter,)) 

420 return 

421 

422 def default_help_func(self, cmd_, subcmds_, args_, command_): 

423 """The default help handler function. Called when either the help flag 

424 or subcommand is passed. 

425 

426 Prints the output of the help formatter instance attached to 

427 this HelpHandler and exits with exit code 0. 

428 

429 """ 

430 echo(self.formatter.get_help_text(command_, subcmds=subcmds_, program_name=cmd_)) 

431 

432 

433"""Usage: cmd_name sub_cmd [..as many subcommands as the max] --flags args ... 

434 

435Possible commands: 

436 

437(One of the possible styles below) 

438 

439Flags: 

440 Group name (if grouped): 

441 -F, --flag VALUE Help text goes here. (integer, defaults to 3) 

442 

443Flag help notes: 

444 

445* don't display parenthetical if it's string/None 

446* Also need to indicate required and mutual exclusion ("not with") 

447* Maybe experimental / deprecated support 

448* General flag listing should also include flags up the chain 

449 

450Subcommand listing styles: 

451 

452* Grouped, one-deep, flag overview on each 

453* One-deep, grouped or alphabetical, help string next to each 

454* Grouped by tree (new group whenever a subtree of more than one 

455 member finishes), with help next to each. 

456 

457What about extra lines in the help (like zfs) (maybe each individual 

458line can be a template string?) 

459 

460TODO: does face need built-in support for version subcommand/flag, 

461basically identical to help? 

462 

463Group names can be ints or strings. When, group names are strings, 

464flags are indented under a heading consisting of the string followed 

465by a colon. All ungrouped flags go under a 'General Flags' group 

466name. When group names are ints, groups are not indented, but a 

467newline is still emitted by each group. 

468 

469Alphabetize should be an option, otherwise everything stays in 

470insertion order. 

471 

472Subcommands without handlers should not be displayed in help. Also, 

473their implicit handler prints the help. 

474 

475Subcommand groups could be Commands with name='', and they can only be 

476added to other commands, where they would embed as siblings instead of 

477as subcommands. Similar to how clastic subapplications can be mounted 

478without necessarily adding to the path. 

479 

480Is it better to delegate representations out or keep them all within 

481the help builder? 

482 

483--- 

484 

485Help needs: a flag (and a way to disable it), as well as a renderer. 

486 

487Usage: 

488 

489Doc 

490 

491Subcommands: 

492 

493... ... 

494 

495Flags: 

496 

497... 

498 

499Postdoc 

500 

501 

502{usage_label} {cmd_name} {subcmd_path} {subcmd_blank} {flags_blank} {posargs_label} 

503 

504{cmd.doc} 

505 

506{subcmd_heading} 

507 

508 {subcmd.name} {subcmd.doc} {subcmd.post_doc} 

509 

510{flags_heading} 

511 

512 {group_name}: 

513 

514 {flag_label} {flag.doc} {flag.post_doc} 

515 

516{cmd.post_doc} 

517 

518 

519-------- 

520 

521# Grouping 

522 

523Effectively sorted on: (group_name, group_index, sort_order, label) 

524 

525But group names should be based on insertion order, with the 

526default-grouped/ungrouped items showing up in the last group. 

527 

528# Wrapping / Alignment 

529 

530Docs start at the position after the longest "left-hand side" 

531(LHS/"key") item that would not cause the first line of the docs to be 

532narrower than the minimum doc width. 

533 

534LHSes which do extend beyond this point will be on their own line, 

535with the doc starting on the line below. 

536 

537# Window width considerations 

538 

539With better termios-based logic in place to get window size, there are 

540going to be a lot of wider-than-80-char help messages. 

541 

542The goal of help message alignment is to help eyes track across from a 

543flag or subcommand to its corresponding doc. Rather than maximizing 

544width usage or topping out at a max width limit, we should be 

545balancing or at least limiting the amount of whitespace between the 

546shortest flag and its doc. (TODO) 

547 

548A width limit might still make sense because reading all the way 

549across the screen can be tiresome, too. 

550 

551TODO: padding_top and padding_bottom attributes on various displays 

552(esp FlagDisplay) to enable finer grained whitespace control without 

553complicated group setups. 

554 

555"""