Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First stab at nicely formatted cli help output using Rich. #1403

Merged
merged 6 commits into from
Feb 9, 2022
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 141 additions & 27 deletions nf_core/__main__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#!/usr/bin/env python
""" nf-core: Helper tools for use with nf-core Nextflow pipelines. """

from nf_core.lint.pipeline_todos import pipeline_todos
from nf_core.modules.bump_versions import ModuleVersionBumper
from click.types import File
from rich import print
from rich.prompt import Confirm
import click
import logging
import os
import re
import rich.console
import rich.logging
import rich.traceback
from rich import print
from rich.text import Text
from rich.highlighter import RegexHighlighter
from rich.align import Align
from rich.padding import Padding
from rich.panel import Panel
from rich.table import Table
from rich.theme import Theme
import sys

import nf_core
Expand Down Expand Up @@ -46,7 +49,10 @@ def run_nf_core():
stderr.print("[blue] |\ | |__ __ / ` / \ |__) |__ [yellow] } {", highlight=False)
stderr.print("[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-,", highlight=False)
stderr.print("[green] `._,._,'\n", highlight=False)
stderr.print("[grey39] nf-core/tools version {}".format(nf_core.__version__), highlight=False)
stderr.print(
f"[grey39] nf-core/tools version {nf_core.__version__} - [link=https://nf-co.re]https://nf-co.re[/]",
highlight=False,
)
try:
is_outdated, current_vers, remote_vers = nf_core.utils.check_if_outdated()
if is_outdated:
Expand All @@ -62,6 +68,103 @@ def run_nf_core():
nf_core_cli()


def rich_format_help(obj, ctx, formatter):
"""
Print nicely formatted help text using rich

This code was shamelessly stolen from rich-cli, the
original author was @willmcgugan - thanks Will!

I've modified it a little to work with click groups
and to spit out output in a style that fits well with our tool.

If this the rich-click plugin gets made, we can probably strip
this out and just use that instead.

Original source:
https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
"""

class OptionHighlighter(RegexHighlighter):
highlights = [
r"(?P<switch>\-\w)(?!\S)",
r"(?P<option>\-\-[\w\-]+)(?!\S)",
r"(?P<metavar>\<[^\>]+\>)",
r"(?P<usage>Usage: )",
]

highlighter = OptionHighlighter()

console = rich.console.Console(
theme=Theme(
{
"option": "bold cyan",
"switch": "bold green",
"metavar": "bold yellow",
"usage": "yellow",
}
),
highlighter=highlighter,
)

# Print usage
console.print(Padding(highlighter(obj.get_usage(ctx)), (0, 1, 1, 1)), style="bold")

# Print command / group help if we have some
if obj.help:
# Get the first line, remove single linebreaks
first_line = obj.help.split("\n\n")[0].replace("\n", " ")
helptext = Text(first_line)

# Get remaining lines, remove single line breaks and format as dim
remaining_lines = obj.help.split("\n\n")[1:]
if len(remaining_lines) > 0:
remaining_lines = "\n" + "\n".join([x.replace("\n", " ") for x in remaining_lines])
helptext.append(remaining_lines, style="dim")

# Print with a max width and some padding
console.print(Padding(Align(helptext, width=100, pad=False), (0, 1, 1, 1)))

# Print the option flags
options_table = Table(highlight=True, box=None, show_header=False)
for param in obj.get_params(ctx)[1:]:
# Short and long form
if len(param.opts) == 2:
opt1 = highlighter(param.opts[1])
opt2 = highlighter(param.opts[0])
# Just one form
else:
opt1 = highlighter(param.opts[0])
opt2 = Text("")

# Column for a metavar, if we have one
metavar = ""
if param.metavar:
metavar = Text(f" {param.metavar}", style="bold yellow")

# Help text
help_record = param.get_help_record(ctx)
if help_record is None:
help = ""
else:
help = Text.from_markup(param.get_help_record(ctx)[-1], emoji=False)

options_table.add_row(highlighter(opt1), highlighter(opt2), metavar, highlighter(help))
console.print(Panel(options_table, border_style="dim", title="Options", title_align="left", width=100))

# List click command groups
if hasattr(obj, "list_commands"):
commands_table = Table(highlight=False, box=None, show_header=False)
# Define formatting in columns, as commands don't match highlighter regex
commands_table.add_column("Command", style="bold cyan", no_wrap=True)
commands_table.add_column("Description")
for command in obj.list_commands(ctx):
cmd = obj.get_command(ctx, command)
commands_table.add_row(command, highlighter(cmd.help.split("\n")[0]))

console.print(Panel(commands_table, border_style="dim", title="Commands", title_align="left", width=100))


# Customise the order of subcommands for --help
# https://stackoverflow.com/a/47984810/713980
class CustomHelpOrder(click.Group):
Expand Down Expand Up @@ -106,6 +209,17 @@ def decorator(f):

return decorator

def format_help(self, ctx, formatter):
"""Override the click help output with nice formatting from Rich."""
rich_format_help(self, ctx, formatter)


class RichCommand(click.Command):
"""Override the click help output with nice formatting from Rich."""

def format_help(self, ctx, formatter):
rich_format_help(self, ctx, formatter)


@click.group(cls=CustomHelpOrder)
@click.version_option(nf_core.__version__)
Expand Down Expand Up @@ -135,7 +249,7 @@ def nf_core_cli(verbose, log_file):


# nf-core list
@nf_core_cli.command(help_priority=1)
@nf_core_cli.command(help_priority=1, cls=RichCommand)
@click.argument("keywords", required=False, nargs=-1, metavar="<filter keywords>")
@click.option(
"-s",
Expand All @@ -157,7 +271,7 @@ def list(keywords, sort, json, show_archived):


# nf-core launch
@nf_core_cli.command(help_priority=2)
@nf_core_cli.command(help_priority=2, cls=RichCommand)
@click.argument("pipeline", required=False, metavar="<pipeline name>")
@click.option("-r", "--revision", help="Release/branch/SHA of the project to run (if remote)")
@click.option("-i", "--id", help="ID for web-gui launch parameter set")
Expand Down Expand Up @@ -207,7 +321,7 @@ def launch(pipeline, id, revision, command_only, params_in, params_out, save_all
# nf-core download


@nf_core_cli.command(help_priority=3)
@nf_core_cli.command(help_priority=3, cls=RichCommand)
@click.argument("pipeline", required=False, metavar="<pipeline name>")
@click.option("-r", "--revision", type=str, help="Pipeline release")
@click.option("-o", "--outdir", type=str, help="Output directory")
Expand Down Expand Up @@ -237,7 +351,7 @@ def download(pipeline, revision, outdir, compress, force, container, singularity


# nf-core licences
@nf_core_cli.command(help_priority=4)
@nf_core_cli.command(help_priority=4, cls=RichCommand)
@click.argument("pipeline", required=True, metavar="<pipeline name>")
@click.option("--json", is_flag=True, default=False, help="Print output in JSON")
def licences(pipeline, json):
Expand Down Expand Up @@ -267,7 +381,7 @@ def validate_wf_name_prompt(ctx, opts, value):
return value


@nf_core_cli.command(help_priority=5)
@nf_core_cli.command(help_priority=5, cls=RichCommand)
@click.option(
"-n",
"--name",
Expand All @@ -294,7 +408,7 @@ def create(name, description, author, version, no_git, force, outdir):
create_obj.init_pipeline()


@nf_core_cli.command(help_priority=6)
@nf_core_cli.command(help_priority=6, cls=RichCommand)
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
@click.option(
"--release",
Expand Down Expand Up @@ -382,7 +496,7 @@ def list(ctx):
pass


@list.command(help_priority=1)
@list.command(help_priority=1, cls=RichCommand)
@click.pass_context
@click.argument("keywords", required=False, nargs=-1, metavar="<filter keywords>")
@click.option("-j", "--json", is_flag=True, help="Print as JSON to stdout")
Expand All @@ -399,7 +513,7 @@ def remote(ctx, keywords, json):
sys.exit(1)


@list.command(help_priority=2)
@list.command(help_priority=2, cls=RichCommand)
@click.pass_context
@click.argument("keywords", required=False, nargs=-1, metavar="<filter keywords>")
@click.option("-j", "--json", is_flag=True, help="Print as JSON to stdout")
Expand All @@ -417,7 +531,7 @@ def local(ctx, keywords, json, dir):
sys.exit(1)


@modules.command(help_priority=2)
@modules.command(help_priority=2, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
Expand All @@ -441,7 +555,7 @@ def install(ctx, tool, dir, prompt, force, sha):
sys.exit(1)


@modules.command(help_priority=3)
@modules.command(help_priority=3, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
Expand Down Expand Up @@ -471,7 +585,7 @@ def update(ctx, tool, dir, force, prompt, sha, all, diff):
sys.exit(1)


@modules.command(help_priority=4)
@modules.command(help_priority=4, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
Expand All @@ -488,7 +602,7 @@ def remove(ctx, dir, tool):
sys.exit(1)


@modules.command("create", help_priority=5)
@modules.command("create", help_priority=5, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", metavar="<directory>")
Expand Down Expand Up @@ -526,7 +640,7 @@ def create_module(ctx, tool, dir, author, label, meta, no_meta, force, conda_nam
sys.exit(1)


@modules.command("create-test-yml", help_priority=6)
@modules.command("create-test-yml", help_priority=6, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-t", "--run-tests", is_flag=True, default=False, help="Run the test workflows")
Expand All @@ -548,7 +662,7 @@ def create_test_yml(ctx, tool, run_tests, output, force, no_prompts):
sys.exit(1)


@modules.command(help_priority=7)
@modules.command(help_priority=7, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", metavar="<pipeline/modules directory>")
Expand Down Expand Up @@ -580,7 +694,7 @@ def lint(ctx, tool, dir, key, all, local, passed):
sys.exit(1)


@modules.command(help_priority=8)
@modules.command(help_priority=8, cls=RichCommand)
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", metavar="<nf-core/modules directory>")
Expand All @@ -592,7 +706,7 @@ def bump_versions(ctx, tool, dir, all, show_all):
the nf-core/modules repo.
"""
try:
version_bumper = ModuleVersionBumper(pipeline_dir=dir)
version_bumper = nf_core.modules.bump_versions.ModuleVersionBumper(pipeline_dir=dir)
version_bumper.bump_versions(module=tool, all_modules=all, show_uptodate=show_all)
except nf_core.modules.module_utils.ModuleException as e:
log.error(e)
Expand All @@ -614,7 +728,7 @@ def schema():
pass


@schema.command(help_priority=1)
@schema.command(help_priority=1, cls=RichCommand)
@click.argument("pipeline", required=True, metavar="<pipeline name>")
@click.argument("params", type=click.Path(exists=True), required=True, metavar="<JSON params file>")
def validate(pipeline, params):
Expand Down Expand Up @@ -642,7 +756,7 @@ def validate(pipeline, params):
sys.exit(1)


@schema.command(help_priority=2)
@schema.command(help_priority=2, cls=RichCommand)
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
@click.option("--no-prompts", is_flag=True, help="Do not confirm changes, just update parameters and exit")
@click.option("--web-only", is_flag=True, help="Skip building using Nextflow config, just launch the web tool")
Expand Down Expand Up @@ -673,7 +787,7 @@ def build(dir, no_prompts, web_only, url):
sys.exit(1)


@schema.command(help_priority=3)
@schema.command(help_priority=3, cls=RichCommand)
@click.argument("schema_path", type=click.Path(exists=True), required=True, metavar="<pipeline schema>")
def lint(schema_path):
"""
Expand All @@ -698,7 +812,7 @@ def lint(schema_path):
sys.exit(1)


@nf_core_cli.command("bump-version", help_priority=9)
@nf_core_cli.command("bump-version", help_priority=9, cls=RichCommand)
@click.argument("new_version", required=True, metavar="<new version>")
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
@click.option(
Expand Down Expand Up @@ -735,7 +849,7 @@ def bump_version(new_version, dir, nextflow):
sys.exit(1)


@nf_core_cli.command("sync", help_priority=10)
@nf_core_cli.command("sync", help_priority=10, cls=RichCommand)
@click.option("-d", "--dir", type=click.Path(exists=True), default=".", help="Pipeline directory. Defaults to CWD")
@click.option("-b", "--from-branch", type=str, help="The git branch to use to fetch workflow vars.")
@click.option("-p", "--pull-request", is_flag=True, default=False, help="Make a GitHub pull-request with the changes.")
Expand Down