Skip to content

rich_click

Classes

Functions

rich_format_filter_operation_help(api: KiaraAPI, obj: Union[click.Command, click.Group], ctx: click.Context, cmd_help: str, value: Union[None, str] = None) -> None

Print nicely formatted help text using rich.

Source code in /opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/kiara/utils/cli/rich_click.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def rich_format_filter_operation_help(
    api: KiaraAPI,
    obj: Union[click.Command, click.Group],
    ctx: click.Context,
    cmd_help: str,
    value: Union[None, str] = None,
) -> None:
    """Print nicely formatted help text using rich."""
    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))
    d = inspect.getdoc(obj)
    if d is None:
        d = ""
    d = textwrap.dedent(d)
    renderables.append(
        Padding(
            Align(d, width=MAX_WIDTH, pad=False),  # type: ignore
            (0, 1, 1, 1),
        )
    )

    v = None
    if value:
        filter_op_type: FilterOperationType = api.get_operation_type("filter")  # type: ignore
        v = api.get_value(value)
        ops = filter_op_type.find_filter_operations_for_data_type(v.data_type_name)
        ops_info = OperationGroupInfo.create_from_operations(
            kiara=api.context, group_title=f"{v.data_type_name} filters", **ops
        )
        p = Panel(
            ops_info,
            title=f"Available filter operations for type [i]'{v.data_type_name}'[/i]",
            title_align="left",
        )
        renderables.append(p)

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

    terminal_print(Group(*renderables))

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.


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
Source code in /opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/kiara/utils/cli/rich_click.py
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)