diff --git a/CHANGELOG.md b/CHANGELOG.md index bad2bb5..69b2ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 0.32 ### 0.32.0 +- feat: Add `epilog=` field on commands for text which should go after the argument help. +- refactor: All root objects (Command/Arg/Subcommand) to have "Final" variants which represent their post-normalization shape. - feat: Add support for fixed size optional-value num_args arguments (e.g. --foo / --foo 4). - feat: Add `aliases=` support for subcommands. Each entry is either a string (visible alias) or a `cappa.Alias(name, hidden=..., deprecated=...)` for finer control. Hidden aliases dispatch but are omitted from help and completion; deprecated aliases emit a runtime warning when used. Collisions are detected at construction time. - fix: Display Args with the same group identity as alternatives in helptext on the same line. diff --git a/src/cappa/base.py b/src/cappa/base.py index 815871b..f55511b 100644 --- a/src/cappa/base.py +++ b/src/cappa/base.py @@ -501,6 +501,7 @@ def command( aliases: list[str | Alias] | None = None, help: str | None = None, description: str | None = None, + epilog: str | None = None, invoke: InvokeCallableSpec[Any] | None = None, hidden: bool = False, default_short: bool = False, @@ -515,6 +516,7 @@ def command( aliases: list[str | Alias] | None = None, help: str | None = None, description: str | None = None, + epilog: str | None = None, invoke: InvokeCallableSpec[Any] | None = None, hidden: bool = False, default_short: bool = False, @@ -530,6 +532,7 @@ def command( aliases: list[str | Alias] | None = None, help: str | None = None, description: str | None = None, + epilog: str | None = None, invoke: InvokeCallableSpec[Any] | None = None, hidden: bool = False, default_short: bool = False, @@ -547,6 +550,7 @@ def command( aliases: list[str | Alias] | None = None, help: str | None = None, description: str | None = None, + epilog: str | None = None, invoke: InvokeCallableSpec[Any] | None = None, hidden: bool = False, default_short: bool = False, @@ -570,6 +574,7 @@ def command( any params/options. description: Optional extended help text. If omitted, the `cls` docstring will be parsed, and the extended long description section will be used. + epilog: Optional text displayed after the argument list in the help output. invoke: Optional command to be called in the event parsing is successful. In the case of subcommands, it will only call the parsed/selected function to invoke. @@ -597,6 +602,7 @@ def wrapper(_decorated_cls: U) -> U: aliases=list(aliases) if aliases is not None else command.aliases, help=help, description=description, + epilog=epilog, hidden=hidden, default_short=default_short, default_long=default_long, diff --git a/src/cappa/command.py b/src/cappa/command.py index 65dcc92..583a191 100644 --- a/src/cappa/command.py +++ b/src/cappa/command.py @@ -96,6 +96,7 @@ class Command(Generic[T]): any params/options. description: Optional extended help text. If omitted, the `cls` docstring will be parsed, and the extended long description section will be used. + epilog: Optional text displayed after the argument list in the help output. invoke: Optional command to be called in the event parsing is successful. In the case of subcommands, it will only call the parsed/selected function to invoke. The value can **either** be a callable object or @@ -125,6 +126,7 @@ class Command(Generic[T]): aliases: list[str | Alias] = dataclasses.field(default_factory=lambda: []) help: str | None = None description: str | None = None + epilog: str | None = None invoke: Callable[..., Any] | str | None = None hidden: bool = False @@ -270,6 +272,7 @@ def collect( aliases=self.aliases, help=kwargs.get("help", self.help), description=kwargs.get("description", self.description), + epilog=self.epilog, invoke=self.invoke, hidden=self.hidden, default_short=self.default_short, diff --git a/src/cappa/help.py b/src/cappa/help.py index 2c69fa9..14d1c29 100644 --- a/src/cappa/help.py +++ b/src/cappa/help.py @@ -159,6 +159,11 @@ def long(self, command: FinalCommand[Any], prog: str) -> list[Displayable]: console = Console() lines.extend(add_long_args(console, self, arg_groups)) + + if command.epilog: + lines.append(NewLine()) + lines.append(Padding(Markdown(command.epilog), self.left_padding)) + return lines def short(self, command: FinalCommand[Any], prog: str) -> Displayable: diff --git a/tests/command/test_help.py b/tests/command/test_help.py index 9a2299d..d2e348e 100644 --- a/tests/command/test_help.py +++ b/tests/command/test_help.py @@ -2,13 +2,20 @@ import re from dataclasses import dataclass +from textwrap import dedent from typing import Any import pytest import cappa from cappa.output import Exit -from tests.utils import Backend, backends, parse +from tests.utils import ( + Backend, + backends, + parse, + strip_trailing_whitespace, + terminal_width, +) @pytest.mark.help @@ -73,3 +80,26 @@ class Command: stdout = capsys.readouterr().out assert "All the help." in stdout + + +@pytest.mark.help +@backends +def test_epilog(backend: Backend, capsys: Any): + @cappa.command(epilog="See also: https://example.com") + @dataclass + class Command: + pass + + with terminal_width(), pytest.raises(Exit): + parse(Command, "--help", backend=backend, completion=False) + + result = strip_trailing_whitespace(capsys.readouterr().out) + tail = result[result.index("[-h, --help]") :] + assert tail == dedent( + """\ + [-h, --help] Show this message and exit. + + + See also: https://example.com + """ + )