rich-click
CLI tool¶
Overview¶
rich-click comes with a CLI tool that allows you to format the Click help output for any CLI that uses Click.
To use, simply prefix rich-click
to the command. Here are a few real world examples:
If the CLI is not installed as a script, you can also pass the location with:
<module_name>:<click_command_name>
<module_name>
<path>
For example, if you have a file located at path/to/my/cli.py
, and the Click Command
object is named main
, then you can run:
rich-click path.to.my.cli:main
rich-click path.to.my.cli
rich-click path/to/my/cli.py
In the first case, the object will be imported and called, i.e. from path.to.my.cli import main
then main()
.
In the other two cases, the file will be run with __name__
as "__main__"
.
Render help text as HTML or SVG¶
You can also use rich-click --output=html [command]
to render rich HTML for help text, or rich-click --output=svg [command]
to generate an SVG.
This works for RichCommands as well as normal click Commands.
SVG example:
HTML example:
SVG and HTML generated from docs/code_snippets/rich_click_cli/app.py
Typer support¶
Experimental
This feature is still experimental. Please report any bugs or issues you run into!
As of 1.9.0, the rich-click
CLI supports patching Typer.
You don't need to do anything special to patch Typer CLIs; it works out of the box.
The main reason to patch Typer CLIs is to get access to rich-click's themes. Another reason is to print HTML and SVG.
Here is an example of overriding a Typer CLI with a rich-click theme:
# /// script
# dependencies = ["rich-click>=1.9", "typer>=0.15"]
# ///
# This example comes from the Typer docs:
# https://typer.tiangolo.com/tutorial/commands/help/#help-panels-for-commands
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(username: str):
"""[green]Create[/green] a new user. :sparkles:"""
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
"""[red]Delete[/red] a user. :fire:"""
print(f"Deleting user: {username}")
@app.command(rich_help_panel="Utils and Configs")
def config(configuration: str):
"""[blue]Configure[/blue] the system. :wrench:"""
print(f"Configuring the system with: {configuration}")
@app.command(rich_help_panel="Utils and Configs")
def sync():
"""[blue]Synchronize[/blue] the system or something fancy like that. :recycle:"""
print("Syncing the system")
@app.command(rich_help_panel="Help and Others")
def help():
"""Get [yellow]help[/yellow] with the system. :question:"""
print("Opening help portal...")
@app.command(rich_help_panel="Help and Others")
def report():
"""[yellow]Report[/yellow] an issue. :bug:"""
print("Please open a new issue online, not a direct message")
if __name__ == "__main__":
app()
Notes on how the rich-click
CLI works¶
Note
The rest of this document contains technical details most users will not need to know.
Under the hood, the rich-click
CLI is patching the click
module, and replacing the Click decorators and click.Command
, click.Group
, etc. objects with their equivalent rich-click versions.
Sometimes, a subclassed click.Command
will overwrite one of these methods:
click.Command.format_usage
click.Command.format_help_text
click.Command.format_options
click.MultiCommand.format_commands
click.Command.format_epilog
Patching Click internals can mess with method resolution order,
since by the time the downstream library subclasses the click.Command
, it will be a RichCommand
, and the subclass's method will take precedence over the RichCommand
's methods.
The problem is that rich-click's methods can be incompatible or at least stylistically incongruous with the base Click help text rendering.
To solve this, rich-click
checks whether a method comes from a "true" RichCommand
subclass or if it just looks that way due to patching.
If RichCommand
is "properly" subclassed, the override is allowed.
If the subclass is only a result of the patching operation, we ignore the aforementioned methods and use the rich-click implementation.
Long story short, the rich-click
CLI is safe to subclassing when it is the user's intent to subclass a rich-click object. (This is so that you can use other nifty features of the CLI such as the --output
option on your own rich-click CLIs)
That said, custom, non-rich-click implementations are ignored.
Additional hacks are implemented to provide first-class support for Typer.
When a Click object subclass is defined, we detect whether it is a Typer subclass during the call to the metaclass __init__
.
When Typer is detected, we do additional overrides to resolve differences between Typer's and rich-click's APIs.
Using patch()
as an end user¶
The functionality that rich-click
uses to patch Click internals is available for use by rich-click end users,
and it occasionally comes in handy outside of the rich-click
CLI.
In some situations, you might be registering a command from another Click CLI that does not use rich-click:
import rich_click as click
from some_library import another_cli
@click.group("my-cli")
def cli():
pass
# `another_cli` will NOT have rich-click markup. :(
cli.add_command(another_cli)
In this situation, another_cli
retains its original help text behavior.
In order to make another_cli
work with rich-click, you need to patch click
before you import another_cli
.
You can patch Click with rich_click.patch.patch
like this:
import rich_click as click
from rich_click.patch import patch
patch()
from some_library import another_cli # noqa: E402
@click.group("my-cli")
def cli():
pass
# `another_cli` will have rich-click markup. :)
cli.add_command(another_cli)
Patching Typer
You can patch Typer using the same patch()
function as above, but only if it occurs before any Typer imports.
It is recommended instead that you use patch_typer()
, which has no timing restrictions for when the patch can be applied:
import typer
from rich_click.patch import patch_typer
patch_typer()