From 3c5ff52b0a6951076fb6643e3313f20f4e3badbf Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Wed, 30 Oct 2024 14:52:36 +0100 Subject: [PATCH] feat: add documentation link in help messages Signed-off-by: Callahan Kovacs --- craft_cli/dispatcher.py | 7 +++- craft_cli/helptexts.py | 62 ++++++++++++++++++++-------- tests/unit/test_dispatcher.py | 9 ++++- tests/unit/test_help.py | 76 ++++++++++++++++++++++++++++------- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/craft_cli/dispatcher.py b/craft_cli/dispatcher.py index 9c081d2..3cd339e 100644 --- a/craft_cli/dispatcher.py +++ b/craft_cli/dispatcher.py @@ -209,9 +209,10 @@ class Dispatcher: :param extra_global_args: other automatic global arguments than the ones provided automatically :param default_command: the command to run if none was specified in the command line + :param docs_base_url: The base address of the documentation, for help messages. """ - def __init__( + def __init__( # noqa: PLR0913 (too-many-arguments) self, appname: str, commands_groups: list[CommandGroup], @@ -219,9 +220,11 @@ def __init__( summary: str = "", extra_global_args: list[GlobalArgument] | None = None, default_command: type[BaseCommand] | None = None, + docs_base_url: str | None = None, ) -> None: self._default_command = default_command - self._help_builder = HelpBuilder(appname, summary, commands_groups) + self._docs_base_url = docs_base_url + self._help_builder = HelpBuilder(appname, summary, commands_groups, docs_base_url) self.global_arguments = _DEFAULT_GLOBAL_ARGS[:] if extra_global_args is not None: diff --git a/craft_cli/helptexts.py b/craft_cli/helptexts.py index 77c06bc..98dc57b 100644 --- a/craft_cli/helptexts.py +++ b/craft_cli/helptexts.py @@ -122,11 +122,25 @@ class HelpBuilder: """Produce the different help texts.""" def __init__( - self, appname: str, general_summary: str, command_groups: list[CommandGroup] + self, + appname: str, + general_summary: str, + command_groups: list[CommandGroup], + docs_base_url: str | None = None, ) -> None: + """Initialize the help builder. + + :param appname: The name of the application. + :param general_summary: A summary of the application. + :param command_groups: The CommandGroups for the application. + :param docs_base_url: The base URL for the documentation. + """ self.appname = appname self.general_summary = general_summary self.command_groups = command_groups + self._docs_base_url = docs_base_url + if docs_base_url and docs_base_url.endswith("/"): + self._docs_base_url = docs_base_url[:-1] def get_usage_message(self, error_message: str, command: str = "") -> str: """Build a usage and error message. @@ -156,7 +170,7 @@ def get_full_help(self, global_options: list[tuple[str, str]]) -> str: - summary - common commands listed and described shortly - all commands grouped, just listed - - more help + - more help and documentation """ textblocks = [] @@ -207,11 +221,17 @@ def get_full_help(self, global_options: list[tuple[str, str]]) -> str: textwrap.dedent( f""" For more information about a command, run '{self.appname} help '. - For a summary of all commands, run '{self.appname} help --all'. - """ + For a summary of all commands, run '{self.appname} help --all'.""" ) ) + # append documentation links to block for more help + if self._docs_base_url: + textblocks[-1] += ( + f"\nFor more information about {self.appname}, " + f"check out: {self._docs_base_url}" + ) + # join all stripped blocks, leaving ONE empty blank line between return "\n\n".join(block.strip() for block in textblocks) + "\n" @@ -228,6 +248,7 @@ def get_detailed_help(self, global_options: list[tuple[str, str]]) -> str: - global options - all commands shown with description, grouped - more help + - more help and documentation """ textblocks = [] @@ -264,18 +285,24 @@ def get_detailed_help(self, global_options: list[tuple[str, str]]) -> str: textblocks.append( textwrap.dedent( f""" - For more information about a specific command, run '{self.appname} help '. - """ + For more information about a specific command, run '{self.appname} help '.""" ) ) + # append documentation links to block for more help + if self._docs_base_url: + textblocks[-1] += ( + f"\nFor more information about {self.appname}, " + f"check out: {self._docs_base_url}" + ) + # join all stripped blocks, leaving ONE empty blank line between return "\n\n".join(block.strip() for block in textblocks) + "\n" def _build_plain_command_help( self, + command: BaseCommand, usage: str, - overview: str, parameters: list[tuple[str, str]], options: list[tuple[str, str]], other_command_names: list[str], @@ -289,7 +316,7 @@ def _build_plain_command_help( - positional arguments (only if parameters are not empty) - options - other related commands - - footer + - help for all commands and documentation """ textblocks = [] @@ -302,7 +329,7 @@ def _build_plain_command_help( ) ) - overview = textwrap.indent(overview, " ") + overview = textwrap.indent(command.overview, " ") textblocks.append(f"Summary:{overview}") # column alignment is dictated by longest options title @@ -326,19 +353,23 @@ def _build_plain_command_help( see_also_block.extend((" " + name) for name in sorted(other_command_names)) textblocks.append("\n".join(see_also_block)) - # footer + # help for all commands textblocks.append( f""" - For a summary of all commands, run '{self.appname} help --all'. - """ + For a summary of all commands, run '{self.appname} help --all'.""" ) + # documentation link + if self._docs_base_url: + command_url = f"{self._docs_base_url}/reference/commands/{command.name}" + textblocks[-1] += f"\nFor more information, check out: {command_url}" + return textblocks def _build_markdown_command_help( self, + command: BaseCommand, usage: str, - overview: str, parameters: list[tuple[str, str]], options: list[tuple[str, str]], other_command_names: list[str], @@ -352,7 +383,6 @@ def _build_markdown_command_help( - positional arguments (only if parameters are not empty) - options - other related commands - - footer """ textblocks = [] @@ -367,7 +397,7 @@ def _build_markdown_command_help( ) ) - overview = process_overview_for_markdown(overview) + overview = process_overview_for_markdown(command.overview) textblocks.append(f"## Summary:\n\n{overview}") if parameters: @@ -444,7 +474,7 @@ def get_command_help( builder = self._build_markdown_command_help else: builder = self._build_plain_command_help - textblocks = builder(usage, command.overview, parameters, options, other_command_names) + textblocks = builder(command, usage, parameters, options, other_command_names) # join all stripped blocks, leaving ONE empty blank line between return "\n\n".join(block.strip() for block in textblocks) + "\n" diff --git a/tests/unit/test_dispatcher.py b/tests/unit/test_dispatcher.py index 983fe9c..ff26a3d 100644 --- a/tests/unit/test_dispatcher.py +++ b/tests/unit/test_dispatcher.py @@ -33,12 +33,17 @@ # --- Tests for the Dispatcher -def test_dispatcher_help_init(): +@pytest.mark.parametrize("docs_base_url", [None, "www.craft-app.com/docs/3.14159"]) +def test_dispatcher_help_init(docs_base_url): """Init the help infrastructure properly.""" groups = [CommandGroup("title", [create_command("somecommand")])] - dispatcher = Dispatcher("test-appname", groups, summary="test summary") + dispatcher = Dispatcher( + "test-appname", groups, summary="test summary", docs_base_url=docs_base_url + ) + assert dispatcher._help_builder.appname == "test-appname" assert dispatcher._help_builder.general_summary == "test summary" + assert dispatcher._help_builder._docs_base_url == docs_base_url def test_dispatcher_pre_parsing(): diff --git a/tests/unit/test_help.py b/tests/unit/test_help.py index f978e64..da858ec 100644 --- a/tests/unit/test_help.py +++ b/tests/unit/test_help.py @@ -17,7 +17,6 @@ import re import textwrap from argparse import ArgumentParser -from typing import Any, Dict from unittest.mock import patch import pytest @@ -64,7 +63,23 @@ def test_get_usage_message_no_command(): # -- building of the big help text outputs -def test_default_help_text(): +@pytest.mark.parametrize( + ("docs_url", "expected"), + [ + (None, None), + ("www.craft-app.com/docs/3.14159", "www.craft-app.com/docs/3.14159"), + ("www.craft-app.com/docs/3.14159/", "www.craft-app.com/docs/3.14159"), + ], +) +def test_trim_url(docs_url, expected): + """Remove the trailing slash for docs url.""" + help_builder = HelpBuilder("testapp", "general summary", [], docs_url) + + assert help_builder._docs_base_url == expected + + +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) +def test_default_help_text(docs_url): """All different parts for the default help.""" cmd1 = create_command("cmd1", "Cmd help which is very long but whatever.", common=True) cmd2 = create_command("command-2", "Cmd help.", common=True) @@ -93,11 +108,11 @@ def test_default_help_text(): ("--experimental-2", argparse.SUPPRESS), ] - help_builder = HelpBuilder("testapp", fake_summary, command_groups) + help_builder = HelpBuilder("testapp", fake_summary, command_groups, docs_url) text = help_builder.get_full_help(global_options) expected = textwrap.dedent( - """\ + f"""\ Usage: testapp [help] @@ -126,10 +141,15 @@ def test_default_help_text(): For a summary of all commands, run 'testapp help --all'. """ ) + if docs_url: + expected += ( + "For more information about testapp, check out: www.craft-app.com/docs/3.14159\n" + ) assert text == expected -def test_detailed_help_text(): +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) +def test_detailed_help_text(docs_url): """All different parts for the detailed help, showing all commands.""" cmd1 = create_command("cmd1", "Cmd help which is very long but whatever.", common=True) cmd2 = create_command("command-2", "Cmd help.", common=True) @@ -158,7 +178,7 @@ def test_detailed_help_text(): ("--experimental-2", argparse.SUPPRESS), ] - help_builder = HelpBuilder("testapp", fake_summary, command_groups) + help_builder = HelpBuilder("testapp", fake_summary, command_groups, docs_url) text = help_builder.get_detailed_help(global_options) expected = textwrap.dedent( @@ -194,6 +214,10 @@ def test_detailed_help_text(): For more information about a specific command, run 'testapp help '. """ ) + if docs_url: + expected += ( + "For more information about testapp, check out: www.craft-app.com/docs/3.14159\n" + ) assert text == expected @@ -304,8 +328,9 @@ def test_detailed_help_text_command_order(command_groups, expected_output): assert actual_output == expected_output +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) @pytest.mark.parametrize("output_format", list(OutputFormat)) -def test_command_help_text_no_parameters(output_format): +def test_command_help_text_no_parameters(docs_url, output_format): """All different parts for a specific command help that doesn't have parameters.""" overview = textwrap.dedent( """ @@ -330,7 +355,7 @@ def test_command_help_text_no_parameters(output_format): ("--revision", "The revision to release (defaults to latest)."), ] - help_builder = HelpBuilder("testapp", "general summary", command_groups) + help_builder = HelpBuilder("testapp", "general summary", command_groups, docs_url) text = help_builder.get_command_help(cmd1(None), options, output_format) expected_plain = textwrap.dedent( @@ -356,6 +381,11 @@ def test_command_help_text_no_parameters(output_format): For a summary of all commands, run 'testapp help --all'. """ ) + if docs_url: + expected_plain += ( + "For more information, check out: " + "www.craft-app.com/docs/3.14159/reference/commands/somecommand\n" + ) expected_markdown = textwrap.dedent( """\ ## Usage: @@ -385,8 +415,9 @@ def test_command_help_text_no_parameters(output_format): assert text == (expected_plain if output_format == OutputFormat.plain else expected_markdown) +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) @pytest.mark.parametrize("output_format", list(OutputFormat)) -def test_command_help_text_with_parameters(output_format): +def test_command_help_text_with_parameters(docs_url, output_format): """All different parts for a specific command help that has parameters.""" overview = textwrap.dedent( """ @@ -409,7 +440,7 @@ def test_command_help_text_with_parameters(output_format): ("--experimental-2", argparse.SUPPRESS), ] - help_builder = HelpBuilder("testapp", "general summary", command_groups) + help_builder = HelpBuilder("testapp", "general summary", command_groups, docs_url) text = help_builder.get_command_help(cmd1(None), options, output_format) expected_plain = textwrap.dedent( @@ -435,6 +466,11 @@ def test_command_help_text_with_parameters(output_format): For a summary of all commands, run 'testapp help --all'. """ ) + if docs_url: + expected_plain += ( + "For more information, check out: " + "www.craft-app.com/docs/3.14159/reference/commands/somecommand\n" + ) expected_markdown = textwrap.dedent( """\ ## Usage: @@ -466,8 +502,9 @@ def test_command_help_text_with_parameters(output_format): assert text == (expected_plain if output_format == OutputFormat.plain else expected_markdown) +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) @pytest.mark.parametrize("output_format", list(OutputFormat)) -def test_command_help_text_complex_overview(output_format): +def test_command_help_text_complex_overview(docs_url, output_format): """The overviews are processed in different ways.""" overview = textwrap.dedent( """ @@ -499,7 +536,7 @@ def test_command_help_text_complex_overview(output_format): ("-q, --quiet", "Only show warnings and errors, not progress."), ] - help_builder = HelpBuilder("testapp", "general summary", command_groups) + help_builder = HelpBuilder("testapp", "general summary", command_groups, docs_url) text = help_builder.get_command_help(cmd1(None), options, output_format) expected_plain = textwrap.dedent( @@ -530,6 +567,11 @@ def test_command_help_text_complex_overview(output_format): For a summary of all commands, run 'testapp help --all'. """ ) + if docs_url: + expected_plain += ( + "For more information, check out: " + "www.craft-app.com/docs/3.14159/reference/commands/somecommand\n" + ) expected_markdown = textwrap.dedent( """\ ## Usage: @@ -565,8 +607,9 @@ def test_command_help_text_complex_overview(output_format): assert text == (expected_plain if output_format == OutputFormat.plain else expected_markdown) +@pytest.mark.parametrize("docs_url", [None, "www.craft-app.com/docs/3.14159/"]) @pytest.mark.parametrize("output_format", list(OutputFormat)) -def test_command_help_text_loneranger(output_format): +def test_command_help_text_loneranger(docs_url, output_format): """All different parts for a specific command that's the only one in its group.""" overview = textwrap.dedent( """ @@ -585,7 +628,7 @@ def test_command_help_text_loneranger(output_format): ("-q, --quiet", "Only show warnings and errors, not progress."), ] - help_builder = HelpBuilder("testapp", "general summary", command_groups) + help_builder = HelpBuilder("testapp", "general summary", command_groups, docs_url) text = help_builder.get_command_help(cmd1(None), options, output_format) expected_plain = textwrap.dedent( @@ -603,6 +646,11 @@ def test_command_help_text_loneranger(output_format): For a summary of all commands, run 'testapp help --all'. """ ) + if docs_url: + expected_plain += ( + "For more information, check out: " + "www.craft-app.com/docs/3.14159/reference/commands/somecommand\n" + ) expected_markdown = textwrap.dedent( """\ ## Usage: