138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459 | def rich_format_operation_help(
obj: Union[click.Command, click.Group],
ctx: click.Context,
operation: Operation,
op_inputs: ValueMap,
cmd_help: str,
) -> None:
"""
Print nicely formatted help text using rich.
Based on original code from rich-cli, by @willmcgugan.
https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
Replacement for the click function format_help().
Takes a command or group and builds the help text output.
Args:
----
obj (click.Command or click.Group): Command or group to build help text for
ctx (click.Context): Click Context object
table: a rich table, including all the inputs of the current operation
"""
renderables: List[RenderableType] = []
# Header text if we have it
if HEADER_TEXT:
renderables.append(
Padding(_make_rich_rext(HEADER_TEXT, STYLE_HEADER_TEXT), (1, 1, 0, 1))
)
# Print usage
_cmd = cmd_help
renderables.append(Padding(_cmd, 1))
# renderables.append(obj.get_usage(ctx))
# renderables.append(Panel(Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND, box=box.MINIMAL))
# Print command / group help if we have some
desc = operation.doc.full_doc
renderables.append(
Padding(
Align(Markdown(desc), width=MAX_WIDTH, pad=False),
(0, 1, 1, 1),
)
)
# if obj.help:
#
# # Print with a max width and some padding
# renderables.append(
# Padding(
# Align(_get_help_text(obj), width=MAX_WIDTH, pad=False),
# (0, 1, 1, 1),
# )
# )
# Look through OPTION_GROUPS for this command
# stick anything unmatched into a default group at the end
option_groups = OPTION_GROUPS.get(ctx.command_path, []).copy()
option_groups.append({"options": []})
argument_group_options = []
for param in obj.get_params(ctx):
# Skip positional arguments - they don't have opts or helptext and are covered in usage
# See https://click.palletsprojects.com/en/8.0.x/documentation/#documenting-arguments
if type(param) is click.core.Argument and not SHOW_ARGUMENTS:
continue
# Skip if option is hidden
if getattr(param, "hidden", False):
continue
# Already mentioned in a config option group
for option_group in option_groups:
if any(opt in option_group.get("options", []) for opt in param.opts):
break
# No break, no mention - add to the default group
else:
if type(param) is click.core.Argument and not GROUP_ARGUMENTS_OPTIONS:
argument_group_options.append(param.opts[0])
else:
list_of_option_groups: List = option_groups[-1]["options"] # type: ignore
list_of_option_groups.append(param.opts[0])
# If we're not grouping arguments and we got some, prepend before default options
if len(argument_group_options) > 0:
extra_option_group = {
"name": ARGUMENTS_PANEL_TITLE,
"options": argument_group_options,
}
option_groups.insert(len(option_groups) - 1, extra_option_group) # type: ignore
# Print each option group panel
for option_group in option_groups:
options_rows = []
for opt in option_group.get("options", []):
# Get the param
for param in obj.get_params(ctx):
if any([opt in param.opts]):
break
# Skip if option is not listed in this group
else:
continue
# Short and long form
opt_long_strs = []
opt_short_strs = []
for idx, opt in enumerate(param.opts):
opt_str = opt
try:
opt_str += "/" + param.secondary_opts[idx]
except IndexError:
pass
if "--" in opt:
opt_long_strs.append(opt_str)
else:
opt_short_strs.append(opt_str)
# Column for a metavar, if we have one
metavar = Text(style=STYLE_METAVAR, overflow="fold")
metavar_str = param.make_metavar()
# Do it ourselves if this is a positional argument
if type(param) is click.core.Argument and metavar_str == param.name.upper(): # type: ignore
metavar_str = param.type.name.upper()
# Skip booleans and choices (handled above)
if metavar_str != "BOOLEAN":
metavar.append(metavar_str)
# Range - from
# https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706
try:
# skip count with default range type
if isinstance(param.type, click.types._NumberRangeBase) and not (
param.count and param.type.min == 0 and param.type.max is None # type: ignore
):
range_str = param.type._describe_range()
if range_str:
metavar.append(RANGE_STRING.format(range_str))
except AttributeError:
# click.types._NumberRangeBase is only in Click 8x onwards
pass
# Required asterisk
required: RenderableType = ""
if param.required:
required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT)
# Highlighter to make [ | ] and <> dim
class MetavarHighlighter(RegexHighlighter):
highlights = [
r"^(?P<metavar_sep>(\[|<))",
r"(?P<metavar_sep>\|)",
r"(?P<metavar_sep>(\]|>)$)",
]
metavar_highlighter = MetavarHighlighter()
rows = [
required,
highlighter(highlighter(",".join(opt_long_strs))),
highlighter(highlighter(",".join(opt_short_strs))),
metavar_highlighter(metavar),
_get_parameter_help(param, ctx), # type: ignore
]
# Remove metavar if specified in config
if not SHOW_METAVARS_COLUMN:
rows.pop(3)
options_rows.append(rows)
if len(options_rows) > 0:
t_styles = {
"show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES,
"leading": STYLE_OPTIONS_TABLE_LEADING,
"box": STYLE_OPTIONS_TABLE_BOX,
"border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE,
"row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES,
"pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE,
"padding": STYLE_OPTIONS_TABLE_PADDING,
}
t_styles.update(option_group.get("table_styles", {})) # type: ignore
box_style = getattr(box, t_styles.pop("box"), None) # type: ignore
options_table = Table(
highlight=True,
show_header=False,
expand=True,
box=box_style,
**t_styles, # type: ignore
)
# Strip the required column if none are required
if all(not x[0] for x in options_rows):
options_rows = [x[1:] for x in options_rows]
for row in options_rows:
options_table.add_row(*row)
renderables.append(
Panel(
options_table,
border_style=STYLE_OPTIONS_PANEL_BORDER, # type: ignore
title=option_group.get("name", OPTIONS_PANEL_TITLE), # type: ignore
title_align=ALIGN_OPTIONS_PANEL, # type: ignore
width=MAX_WIDTH, # type: ignore
)
)
#
# Groups only:
# List click command groups
#
if hasattr(obj, "list_commands"):
# Look through COMMAND_GROUPS for this command
# stick anything unmatched into a default group at the end
cmd_groups = COMMAND_GROUPS.get(ctx.command_path, []).copy()
cmd_groups.append({"commands": []})
for command in obj.list_commands(ctx): # type: ignore
for cmd_group in cmd_groups:
if command in cmd_group.get("commands", []):
break
else:
commands: List = cmd_groups[-1]["commands"] # type: ignore
commands.append(command)
# Print each command group panel
for cmd_group in cmd_groups:
t_styles = {
"show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES,
"leading": STYLE_COMMANDS_TABLE_LEADING,
"box": STYLE_COMMANDS_TABLE_BOX,
"border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE,
"row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES,
"pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE,
"padding": STYLE_COMMANDS_TABLE_PADDING,
}
t_styles.update(cmd_group.get("table_styles", {})) # type: ignore
box_style = getattr(box, t_styles.pop("box"), None) # type: ignore
commands_table = Table(
highlight=False,
show_header=False,
expand=True,
box=box_style, # type: ignore
**t_styles, # type: ignore
)
# Define formatting in first column, as commands don't match highlighter regex
commands_table.add_column(style="bold cyan", no_wrap=True)
for command in cmd_group.get("commands", []):
# Skip if command does not exist
if command not in obj.list_commands(ctx): # type: ignore
continue
cmd = obj.get_command(ctx, command) # type: ignore
assert cmd is not None
if cmd.hidden:
continue
# Use the truncated short text as with vanilla text if requested
if USE_CLICK_SHORT_HELP:
helptext = cmd.get_short_help_str()
else:
# Use short_help function argument if used, or the full help
helptext = cmd.short_help or cmd.help or ""
commands_table.add_row(command, _make_command_help(helptext))
if commands_table.row_count > 0:
renderables.append(
Panel(
commands_table,
border_style=STYLE_COMMANDS_PANEL_BORDER, # type: ignore
title=cmd_group.get("name", COMMANDS_PANEL_TITLE), # type: ignore
title_align=ALIGN_COMMANDS_PANEL, # type: ignore
width=MAX_WIDTH, # type: ignore
)
)
inputs_table = create_operation_status_renderable(
operation=operation,
inputs=op_inputs,
render_config={
"show_operation_name": False,
"show_inputs": True,
"show_outputs_schema": False,
"show_headers": False,
"show_operation_doc": False,
},
)
# inputs_table = operation.create_renderable(
# show_operation_name=False,
# show_operation_doc=False,
# show_inputs=True,
# show_outputs_schema=False,
# show_headers=False,
# )
inputs_panel = Panel(
inputs_table,
title="Inputs",
border_style=STYLE_COMMANDS_PANEL_BORDER, # type: ignore
title_align=ALIGN_COMMANDS_PANEL, # type: ignore
width=MAX_WIDTH, # type: ignore
)
renderables.append(inputs_panel)
# Epilogue if we have it
if obj.epilog:
# Remove single linebreaks, replace double with single
lines = obj.epilog.split("\n\n")
epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines])
renderables.append(
Padding(Align(highlighter(epilogue), width=MAX_WIDTH, pad=False), 1)
)
# Footer text if we have it
if FOOTER_TEXT:
renderables.append(
Padding(_make_rich_rext(FOOTER_TEXT, STYLE_FOOTER_TEXT), (1, 1, 0, 1))
)
group = Group(*renderables)
terminal_print(group)
|