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
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
2import os
3import sys
4import array
5import textwrap
7from boltons.iterutils import unique, split
9from face.utils import format_flag_label, format_flag_post_doc, format_posargs_label, echo
10from face.parser import Flag
12DEFAULT_HELP_FLAG = Flag('--help', parse_as=True, char='-h', doc='show this help message and exit')
13DEFAULT_MAX_WIDTH = 120
16def _get_termios_winsize():
17 # TLPI, 62.9 (p. 1319)
18 import fcntl
19 import termios
21 winsize = array.array('H', [0, 0, 0, 0])
23 assert not fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, winsize)
25 ws_row, ws_col, _, _ = winsize
27 return ws_row, ws_col
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
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
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
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
70 if not doc:
71 append(lhs)
72 return ret
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])
83 for line in wrapped_doc[1:]:
84 append(' ' * doc_start + line)
86 return ret
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)
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)
108 len_sep = len(sep)
109 len_indent = len(indent)
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
123 return {'width': width,
124 'label_width': max_label_width,
125 'doc_width': max_doc_width,
126 'doc_start': doc_start}
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}
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.
155 Probably what most Pythonists expect, this help text is slightly
156 stouter (conservative with vertical space) than other conventional
157 help messages.
159 The default output looks like::
161 Usage: example.py subcommand [FLAGS]
163 Does a bit of busy work
166 Subcommands:
168 sum Just a lil fun in the sum
169 subtract
170 print
173 Flags:
175 --help / -h show this help message and exit
176 --verbose / -V
179 Due to customizability, the constructor takes a large number of
180 keyword arguments, the most important of which are highlighted
181 here.
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.
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.
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.
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.
210 """
211 default_context = dict(DEFAULT_CONTEXT)
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()))
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'])
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.
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``.)
245 """
246 # TODO: incorporate "Arguments" section if posargs has a doc set
247 ctx = self.ctx
249 ret = [self.get_usage_line(parser, subcmds=subcmds, program_name=program_name)]
250 append = ret.append
251 append(ctx['group_break'])
253 shown_flags = parser.get_flags(path=subcmds, with_hidden=False)
254 if subcmds:
255 parser = parser.subprs_map[subcmds]
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'])
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)
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)
281 append(ctx['section_break'])
283 if not shown_flags:
284 return '\n'.join(ret)
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)
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)
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'])
311 ret.extend(flag_lines)
313 return ctx['pre_doc'] + '\n'.join(ret) + ctx['post_doc']
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:
320 Usage: example.py subcommand [FLAGS] [args ...]
322 """
323 ctx = self.ctx
324 subcmds = tuple(subcmds or ())
325 parts = [ctx['usage_label']] if ctx['usage_label'] else []
326 append = parts.append
328 program_name = program_name or parser.name
330 append(' '.join((program_name,) + subcmds))
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')
338 # with subcommands out of the way, look up the parser for flags and args
339 if subcmds:
340 parser = parser.subprs_map[subcmds]
342 flags = parser.get_flags(with_hidden=False)
344 if flags:
345 append('[FLAGS]')
347 if not parser.posargs.display.hidden:
348 fmt_posargs_label = ctx['format_posargs_label']
349 append(fmt_posargs_label(parser.posargs))
351 return ' '.join(parts)
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'''
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.
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.
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()`.
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")
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)
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
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.
426 Prints the output of the help formatter instance attached to
427 this HelpHandler and exits with exit code 0.
429 """
430 echo(self.formatter.get_help_text(command_, subcmds=subcmds_, program_name=cmd_))
433"""Usage: cmd_name sub_cmd [..as many subcommands as the max] --flags args ...
435Possible commands:
437(One of the possible styles below)
439Flags:
440 Group name (if grouped):
441 -F, --flag VALUE Help text goes here. (integer, defaults to 3)
443Flag help notes:
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
450Subcommand listing styles:
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.
457What about extra lines in the help (like zfs) (maybe each individual
458line can be a template string?)
460TODO: does face need built-in support for version subcommand/flag,
461basically identical to help?
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.
469Alphabetize should be an option, otherwise everything stays in
470insertion order.
472Subcommands without handlers should not be displayed in help. Also,
473their implicit handler prints the help.
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.
480Is it better to delegate representations out or keep them all within
481the help builder?
483---
485Help needs: a flag (and a way to disable it), as well as a renderer.
487Usage:
489Doc
491Subcommands:
493... ...
495Flags:
497...
499Postdoc
502{usage_label} {cmd_name} {subcmd_path} {subcmd_blank} {flags_blank} {posargs_label}
504{cmd.doc}
506{subcmd_heading}
508 {subcmd.name} {subcmd.doc} {subcmd.post_doc}
510{flags_heading}
512 {group_name}:
514 {flag_label} {flag.doc} {flag.post_doc}
516{cmd.post_doc}
519--------
521# Grouping
523Effectively sorted on: (group_name, group_index, sort_order, label)
525But group names should be based on insertion order, with the
526default-grouped/ungrouped items showing up in the last group.
528# Wrapping / Alignment
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.
534LHSes which do extend beyond this point will be on their own line,
535with the doc starting on the line below.
537# Window width considerations
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.
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)
548A width limit might still make sense because reading all the way
549across the screen can be tiresome, too.
551TODO: padding_top and padding_bottom attributes on various displays
552(esp FlagDisplay) to enable finer grained whitespace control without
553complicated group setups.
555"""