Skip to content

Commit

Permalink
Make help pages prettier. (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasraabe authored Feb 7, 2022
1 parent fb432e5 commit 0641f39
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 21 deletions.
11 changes: 9 additions & 2 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
`Anaconda.org <https://anaconda.org/conda-forge/pytask>`_.


0.1.8 - 2022-02-07
------------------

- :gh:`210` allows ``__tracebackhide__`` to be a callable which accepts the current
exception as an input. Closes :gh:`145`.
- :gh:`213` improves coverage and reporting.
- :gh:`215` makes the help pages of the CLI prettier.


0.1.7 - 2022-01-28
------------------

- :gh:`153` adds support for Python 3.10 which requires pony >= 0.7.15.
- :gh:`192` deprecates Python 3.6.
- :gh:`209` cancels previous CI jobs when a new job is started.
- :gh:`210` allows ``__tracebackhide__`` to be a callable which accepts the current
exception as an input. Closes :gh:`145`.


0.1.6 - 2022-01-27
Expand Down
9 changes: 5 additions & 4 deletions src/_pytask/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING

import click
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.console import console
from _pytask.exceptions import CollectionError
Expand Down Expand Up @@ -92,7 +93,7 @@ def main(config_from_cli: dict[str, Any]) -> Session:
return session


@click.command()
@click.command(cls=ColoredCommand)
@click.option(
"--debug-pytask",
is_flag=True,
Expand All @@ -119,10 +120,10 @@ def main(config_from_cli: dict[str, Any]) -> Session:
help="Choose whether tracebacks should be displayed or not. [default: yes]",
)
def build(**config_from_cli: Any) -> NoReturn:
"""Collect and execute tasks and report the results.
"""Collect tasks, execute them and report the results.
This is the default command of pytask which searches given paths or the current
working directory for tasks to execute them. A report informs you on the results.
This is pytask's default command. pytask collects tasks from the given paths or the
current working directory, executes them and reports the results.
"""
config_from_cli["command"] = "build"
Expand Down
5 changes: 3 additions & 2 deletions src/_pytask/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import attr
import click
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.config import IGNORED_TEMPORARY_FILES_AND_FOLDERS
from _pytask.console import console
Expand Down Expand Up @@ -69,7 +70,7 @@ def pytask_post_parse(config: dict[str, Any]) -> None:
]


@click.command()
@click.command(cls=ColoredCommand)
@click.option(
"--mode",
type=click.Choice(["dry-run", "interactive", "force"]),
Expand All @@ -80,7 +81,7 @@ def pytask_post_parse(config: dict[str, Any]) -> None:
"-q", "--quiet", is_flag=True, help="Do not print the names of the removed paths."
)
def clean(**config_from_cli: Any) -> NoReturn:
"""Clean provided paths by removing files unknown to pytask."""
"""Clean the provided paths by removing files unknown to pytask."""
config_from_cli["command"] = "clean"

try:
Expand Down
8 changes: 4 additions & 4 deletions src/_pytask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

import click
import pluggy
from _pytask.click import ColoredGroup
from _pytask.config import hookimpl
from _pytask.pluginmanager import get_plugin_manager
from click_default_group import DefaultGroup
from packaging.version import parse as parse_version


_CONTEXT_SETTINGS: dict[str, Any] = {"help_option_names": ["-h", "--help"]}
_CONTEXT_SETTINGS: dict[str, Any] = {"help_option_names": ("-h", "--help")}


if parse_version(click.__version__) < parse_version("8"):
Expand Down Expand Up @@ -92,14 +92,14 @@ def pytask_add_hooks(pm: pluggy.PluginManager) -> None:


@click.group(
cls=DefaultGroup,
cls=ColoredGroup,
context_settings=_CONTEXT_SETTINGS,
default="build",
default_if_no_args=True,
)
@click.version_option(**_VERSION_OPTION_KWARGS)
def cli() -> None:
"""The command line interface of pytask."""
"""Manage your tasks with pytask."""
pass


Expand Down
132 changes: 132 additions & 0 deletions src/_pytask/click.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from __future__ import annotations

from typing import Any

import click
from _pytask import __version__ as version
from _pytask.console import console
from click_default_group import DefaultGroup
from rich.highlighter import RegexHighlighter
from rich.panel import Panel
from rich.table import Table
from rich.text import Text


_SWITCH_REGEX = r"(?P<switch>\-\w)\b"
_OPTION_REGEX = r"(?P<option>\-\-[\w\-]+)"
_METAVAR_REGEX = r"\-\-[\w\-]+(?P<metavar>[ |=][\w\.:]+)"


class OptionHighlighter(RegexHighlighter):
highlights = [_SWITCH_REGEX, _OPTION_REGEX, _METAVAR_REGEX]


class ColoredGroup(DefaultGroup):
def format_help(self: DefaultGroup, ctx: Any, formatter: Any) -> None: # noqa: U100
highlighter = OptionHighlighter()

console.print(
f"[b]pytask[/b] [dim]v{version}[/]\n", justify="center", highlight=False
)

console.print(
"Usage: [b]pytask[/b] [b][OPTIONS][/b] [b][COMMAND][/b] [b][PATHS][/b]\n"
)

console.print(self.help, style="dim")
console.print()

commands_table = Table(highlight=True, box=None, show_header=False)

for command_name in sorted(self.commands):
command = self.commands[command_name]

if command_name == self.default_cmd_name:
formatted_name = Text(command_name + " *", style="command")
else:
formatted_name = Text(command_name, style="command")

commands_table.add_row(formatted_name, highlighter(command.help))

console.print(
Panel(
commands_table,
title="[bold #ffffff]Commands[/bold #ffffff]",
title_align="left",
border_style="grey37",
)
)

print_options(self, ctx)

console.print(
"[bold red]♥[/bold red] [white]https://pytask-dev.readthedocs.io[/]",
justify="right",
)


class ColoredCommand(click.Command):
"""Override Clicks help with a Richer version."""

def format_help(
self: click.Command, ctx: Any, formatter: Any # noqa: U100
) -> None:
console.print(
f"[b]pytask[/b] [dim]v{version}[/]\n", justify="center", highlight=False
)

console.print(
f"Usage: [b]pytask[/b] [b]{self.name}[/b] [b][OPTIONS][/b] [b][PATHS][/b]\n"
)

console.print(self.help, style="dim")
console.print()

print_options(self, ctx)

console.print(
"[bold red]♥[/bold red] [white]https://pytask-dev.readthedocs.io[/]",
justify="right",
)


def print_options(group_or_command: click.Command | DefaultGroup, ctx: Any) -> None:
highlighter = OptionHighlighter()

options_table = Table(highlight=True, box=None, show_header=False)

for param in group_or_command.get_params(ctx):

if isinstance(param, click.Argument):
continue

# The ordering of -h and --help is not fixed.
if param.name == "help":
opt1 = highlighter("-h")
opt2 = highlighter("--help")
elif len(param.opts) == 2:
opt1 = highlighter(param.opts[0])
opt2 = highlighter(param.opts[1])
else:
opt1 = Text("")
opt2 = highlighter(param.opts[0])

if param.metavar:
opt2 += Text(f" {param.metavar}", style="metavar")

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

options_table.add_row(opt1, opt2, highlighter(help_text))

console.print(
Panel(
options_table,
title="[bold #ffffff]Options[/bold #ffffff]",
title_align="left",
border_style="grey37",
)
)
5 changes: 3 additions & 2 deletions src/_pytask/collect_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import TYPE_CHECKING

import click
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.console import console
from _pytask.console import create_url_style_for_path
Expand Down Expand Up @@ -47,10 +48,10 @@ def pytask_parse_config(
config["nodes"] = config_from_cli.get("nodes", False)


@click.command()
@click.command(cls=ColoredCommand)
@click.option("--nodes", is_flag=True, help="Show a task's dependencies and products.")
def collect(**config_from_cli: Any | None) -> NoReturn:
"""Collect tasks from paths."""
"""Collect tasks and report information about them."""
config_from_cli["command"] = "collect"

try:
Expand Down
6 changes: 6 additions & 0 deletions src/_pytask/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@

theme = Theme(
{
# Statuses
"failed": "#BF2D2D",
"failed.textonly": "#ffffff on #BF2D2D",
"neutral": "",
Expand All @@ -78,6 +79,11 @@
"success": "#137C39",
"success.textonly": "#ffffff on #137C39",
"warning": "#F4C041",
# Help page.
"command": "bold #137C39",
"option": "bold #F4C041",
"switch": "bold #D54523",
"metavar": "bold yellow",
}
)

Expand Down
5 changes: 3 additions & 2 deletions src/_pytask/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import click
import networkx as nx
from _pytask.click import ColoredCommand
from _pytask.compat import check_for_optional_program
from _pytask.compat import import_optional_dependency
from _pytask.config import hookimpl
Expand Down Expand Up @@ -104,7 +105,7 @@ def _rank_direction_callback(
)


@click.command()
@click.command(cls=ColoredCommand)
@click.option("-l", "--layout", type=str, default=None, help=_HELP_TEXT_LAYOUT)
@click.option("-o", "--output-path", type=str, default=None, help=_HELP_TEXT_OUTPUT)
@click.option(
Expand All @@ -113,7 +114,7 @@ def _rank_direction_callback(
help=_HELP_TEXT_RANK_DIRECTION,
)
def dag(**config_from_cli: Any) -> NoReturn:
"""Create a visualization of the project's DAG."""
"""Create a visualization of the project's directed acyclic graph."""
try:
pm = get_plugin_manager()
from _pytask import cli
Expand Down
3 changes: 2 additions & 1 deletion src/_pytask/mark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import attr
import click
import networkx as nx
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.console import console
from _pytask.dag import task_and_preceding_tasks
Expand Down Expand Up @@ -42,7 +43,7 @@
]


@click.command()
@click.command(cls=ColoredCommand)
def markers(**config_from_cli: Any) -> NoReturn:
"""Show all registered markers."""
config_from_cli["command"] = "markers"
Expand Down
4 changes: 2 additions & 2 deletions src/_pytask/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
type=str,
multiple=True,
help=(
"A pattern to ignore files or directories. For example, ``task_example.py`` or "
"``src/*``."
"A pattern to ignore files or directories. For example, task_example.py or "
"src/*."
),
callback=falsy_to_none_callback,
)
Expand Down
5 changes: 3 additions & 2 deletions src/_pytask/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import TYPE_CHECKING

import click
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.console import console
from _pytask.console import format_task_id
Expand Down Expand Up @@ -97,15 +98,15 @@ def _create_or_update_runtime(task_name: str, start: float, end: float) -> None:
setattr(runtime, attr, val)


@click.command()
@click.command(cls=ColoredCommand)
@click.option(
"--export",
type=str,
default=None,
help="Export the profile in the specified format.",
)
def profile(**config_from_cli: Any) -> NoReturn:
"""Show profile information on collected tasks."""
"""Show information about tasks like runtime and memory consumption of products."""
config_from_cli["command"] = "profile"

try:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,31 @@
import subprocess

import pytest
from _pytask.outcomes import ExitCode
from pytask import __version__
from pytask import cli


@pytest.mark.end_to_end
def test_version_option():
process = subprocess.run(["pytask", "--version"], capture_output=True)
assert "pytask, version " + __version__ in process.stdout.decode("utf-8")


@pytest.mark.end_to_end
@pytest.mark.parametrize("help_option", ["-h", "--help"])
@pytest.mark.parametrize(
"commands",
[
("pytask",),
("pytask", "build"),
("pytask", "clean"),
("pytask", "collect"),
("pytask", "dag"),
("pytask", "markers"),
("pytask", "profile"),
],
)
def test_help_pages(runner, commands, help_option):
result = runner.invoke(cli, [*commands, help_option])
assert result.exit_code == ExitCode.OK

0 comments on commit 0641f39

Please sign in to comment.